Android Coroutines and Flow: A Detailed Guide

举报
shenlan9755 发表于 2026/09/03 09:33:16 2026/09/03
【摘要】 Android Coroutines and Flow: A Detailed Guide IntroductionAsynchronous programming is unavoidable on Android: network calls, database queries, file I/O, and animations all happen off the main thre...

Android Coroutines and Flow: A Detailed Guide

Introduction

Asynchronous programming is unavoidable on Android: network calls, database queries, file I/O, and animations all happen off the main thread. Historically, Android developers wrestled with callbacks, AsyncTask, RxJava, and thread handlers. Kotlin Coroutines and Flow provide a modern, lightweight, and structured alternative that is now the de facto standard in Android development.

This article covers coroutines fundamentals, structured concurrency, dispatchers, cancellation, error handling, Flow for cold streams, StateFlow/SharedFlow for hot streams, integration with Room and Retrofit, and best practices for production code.


1. What Are Coroutines?

1.1 Definition

A coroutine is a suspendable computation. Unlike a thread, it does not block the underlying thread while suspended — it yields control, allowing other coroutines to run on the same thread. Coroutines are cheap: you can launch hundreds of thousands of them concurrently without exhausting memory.

1.2 Suspending Functions

A function marked suspend can pause and resume without blocking the thread.

suspend fun fetchUser(id: String): User {
    delay(1000) // non-blocking suspension
    return User(id, "Alice")
}

delay is a suspending function — it does not sleep the thread. The thread is free to do other work during the delay.

1.3 Why Not Threads?

  • Creating a thread costs ~1 MB of stack memory.
  • Context switching between threads is expensive.
  • Threads cannot be suspended mid-execution and resumed later without complex synchronization.

Coroutines run on a thread pool and multiplex many coroutines onto few threads, achieving concurrency with minimal overhead.


2. Coroutine Builders and Scopes

2.1 CoroutineScope

Every coroutine runs inside a CoroutineScope. The scope controls the lifetime of all coroutines launched within it. When the scope is cancelled, all child coroutines are cancelled too.

2.2 launch

launch starts a fire-and-forget coroutine that returns a Job. Use it when you do not need a result.

scope.launch {
    val data = fetchData()
    updateUi(data)
}

2.3 async

async starts a coroutine that produces a result, accessed via await(). Use it for parallel decomposition.

suspend fun loadDashboard(): Dashboard = coroutineScope {
    val profile = async { fetchProfile() }
    val feed = async { fetchFeed() }
    val notifications = async { fetchNotifications() }
    Dashboard(profile.await(), feed.await(), notifications.await())
}

All three requests run concurrently. await() suspends until each result is ready.

2.4 runBlocking

runBlocking bridges the blocking and suspending worlds. It blocks the current thread until the coroutine completes. Use it in main functions and unit tests — never in production Android code.

fun main() = runBlocking {
    val result = fetchUser("1")
    println(result)
}

2.5 withContext

withContext switches the coroutine to a different dispatcher and returns a result. It is the idiomatic way to hop between threads.

suspend fun processImage(bitmap: Bitmap): Bitmap {
    return withContext(Dispatchers.Default) {
        heavyFilter(bitmap)
    }
}

3. Dispatchers

3.1 The Built-in Dispatchers

Dispatcher Use Case
Dispatchers.Main UI work on the main thread (Android UI toolkit access).
Dispatchers.Default CPU-intensive work (parsing, sorting, image processing).
Dispatchers.IO Blocking I/O (network, database, file system).
Dispatchers.Unconfined Advanced; runs the coroutine on the current thread until first suspension. Rarely needed.

3.2 Choosing a Dispatcher

  • UI updates: Dispatchers.Main
  • Network/DB: Dispatchers.IO (Room and Retrofit with suspend functions handle this automatically, but explicit use is safe.)
  • Math/heavy computation: Dispatchers.Default

3.3 Custom Dispatchers

For fine-grained control, create a dispatcher backed by a named thread pool:

val singleThreadDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()

Remember to shut it down when no longer needed to avoid thread leaks.


4. Structured Concurrency

4.1 The Principle

Structured concurrency means coroutines form a parent-child hierarchy. A parent cannot complete until all its children complete. Cancelling a parent cancels all children. This prevents coroutine leaks — a common bug where coroutines outlive the component that launched them.

4.2 coroutineScope vs supervisorScope

  • coroutineScope { }: If any child fails, the entire scope fails and all siblings are cancelled.
  • supervisorScope { }: A child failure does not cancel siblings. Use when children are independent.
suspend fun syncAll() = supervisorScope {
    val a = async { syncContacts() }
    val b = async { syncCalendar() }
    // if syncContacts throws, syncCalendar still completes
    a.await(); b.await()
}

4.3 Android Lifecycle Scopes

The lifecycle-runtime-ktx library provides two ready-made scopes:

  • lifecycleScope (on a LifecycleOwner): cancelled when the lifecycle reaches DESTROYED.
  • viewModelScope (on a ViewModel): cancelled when the ViewModel is cleared.
class MyViewModel : ViewModel() {
    fun load() {
        viewModelScope.launch {
            val data = repository.fetch()
            _uiState.value = data
        }
    }
}

viewModelScope is the most common scope in Android UI-layer code. It guarantees that in-flight work is cancelled when the user navigates away, preventing UI updates to a destroyed screen.


5. Cancellation

5.1 Cooperative Cancellation

Coroutines are cancelled cooperatively. A suspension point checks for cancellation and throws CancellationException. CPU-bound loops that do not suspend must check manually:

suspend fun processLargeList(items: List<Item>) {
    for (item in items) {
        ensureActive() // throws CancellationException if cancelled
        process(item)
    }
}

Alternatively, use yield() which both checks cancellation and yields control to other coroutines.

5.2 Cleanup with finally

Use try/finally to release resources on cancellation. finally blocks run even on CancellationException.

suspend fun downloadFile(url: String): File {
    val connection = openConnection(url)
    try {
        return writeToFile(connection.inputStream)
    } finally {
        connection.close()
    }
}

5.3 NonCancellable

If you must perform a suspending operation during cleanup that should not itself be cancelled, wrap it in withContext(NonCancellable):

try {
    doWork()
} finally {
    withContext(NonCancellable) {
        rollbackTransaction()
    }
}

6. Error Handling

6.1 try/catch

Suspending functions propagate exceptions normally. Wrap calls in try/catch:

viewModelScope.launch {
    try {
        val user = repository.fetchUser()
        _state.value = Success(user)
    } catch (e: IOException) {
        _state.value = Error("Network failure")
    } catch (e: HttpException) {
        _state.value = Error("Server error: ${e.code()}")
    }
}

6.2 CoroutineExceptionHandler

For fire-and-forget coroutines where you want a global handler:

val handler = CoroutineExceptionHandler { _, throwable ->
    Log.e("Coroutine", "Uncaught", throwable)
    crashReporter.report(throwable)
}

scope.launch(handler) {
    riskyOperation()
}

This only works for uncaught exceptions in launch (not async, since async defers exception delivery to await()).

6.3 Result and Either Wrappers

For explicit error modeling without exceptions, use kotlin.Result or a sealed Either type:

sealed class ApiResult<out T> {
    data class Success<T>(val data: T) : ApiResult<T>()
    data class Failure(val message: String) : ApiResult<Nothing>()
}

suspend fun fetchUser(): ApiResult<User> = try {
    ApiResult.Success(api.getUser())
} catch (e: Exception) {
    ApiResult.Failure(e.message ?: "Unknown error")
}

This forces the caller to handle both branches, eliminating unhandled-exception bugs.


7. Flow: Cold Async Streams

7.1 What Is Flow?

Flow<T> is a cold asynchronous stream of values. Cold means the producer code does not run until a collector collects. Each collection runs the producer independently.

fun numbers(): Flow<Int> = flow {
    for (i in 1..3) {
        delay(500)
        emit(i)
    }
}

// Usage
numbers().collect { println(it) } // prints 1, 2, 3 with 500ms gaps

7.2 Flow Builders

  • flow { emit(...) }: Manual emission.
  • flowOf(1, 2, 3): Fixed values.
  • asFlow(): Convert a collection or range.
  • channelFlow { send(...) }: For flows that need concurrency between producer and consumer.

7.3 Flow Operators

fun observePrices(): Flow<Double> = flow {
    var price = 100.0
    while (true) {
        delay(1000)
        price += (Math.random() - 0.5) * 5
        emit(price)
    }
}

observePrices()
    .filter { it > 0 }
    .map { "%.2f".format(it) }
    .distinctUntilChanged()
    .take(10)
    .collect { println("Price: $it") }

Key operators:

  • map, filter, flatMapLatest, flatMapConcat — transformation.
  • debounce, distinctUntilChanged — deduplication.
  • take, drop — slicing.
  • combine, zip — combining flows.
  • catch, retry, retryWhen — error handling.
  • flowOn — dispatcher for upstream.

7.4 flowOn

flowOn shifts the upstream execution to a different dispatcher:

flow {
    emit(readFromDisk()) // runs on IO
}.flowOn(Dispatchers.IO).collect { value ->
    updateUi(value) // runs on the collector's dispatcher
}

7.5 Terminal Operators

collect is the terminal operator that starts the flow. Others:

  • toList() / toSet() — collect into a collection.
  • first() / last() — take a single value.
  • single() — expect exactly one value.
  • count() — count emissions.
  • fold() — reduce.

8. StateFlow and SharedFlow: Hot Streams

8.1 Hot vs Cold

A hot stream emits values regardless of whether anyone is collecting. Late collectors receive only future emissions (or the current state, for StateFlow).

8.2 StateFlow

StateFlow<T> always has a current value and conflates emissions — if multiple values are set rapidly, only the latest is delivered. It is the standard way to expose UI state from a ViewModel.

class UserViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()

    fun load() {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            try {
                val user = repository.fetch()
                _uiState.value = UiState.Success(user)
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e.message)
            }
        }
    }
}

8.3 Converting Flow to StateFlow

val users: StateFlow<List<User>> = repository.observeUsers()
    .stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = emptyList()
    )

WhileSubscribed(5000) keeps the upstream active while there is at least one collector, and keeps it alive for 5 seconds after the last collector disappears (to tolerate configuration changes).

8.4 SharedFlow

SharedFlow<T> is a more general hot stream — it has no initial value and supports replay buffers and buffering strategies.

private val _events = MutableSharedFlow<UiEvent>()
val events: SharedFlow<UiEvent> = _events.asSharedFlow()

fun showSnackbar(message: String) {
    _events.tryEmit(UiEvent.Snackbar(message))
}

Use SharedFlow for one-shot events (snackbars, navigation, toasts) that should not be conflated or replayed to new collectors.

8.5 Replay Buffer

MutableSharedFlow<Int>(replay = 3)

A replay of 3 means new collectors immediately receive the last 3 emitted values, then continue receiving new ones.


9. Collecting Flows on Android

9.1 collectAsState in Compose

In Jetpack Compose:

@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    when (uiState) {
        is UiState.Loading -> CircularProgressIndicator()
        is UiState.Success -> UserContent((uiState as UiState.Success).user)
        is UiState.Error -> ErrorText((uiState as UiState.Error).message)
    }
}

collectAsStateWithLifecycle automatically stops collecting when the lifecycle drops below STARTED, saving resources when the app is backgrounded.

9.2 collect in Fragments

viewLifecycleOwner.lifecycleScope.launch {
    viewModel.uiState
        .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
        .collect { state -> render(state) }
}

flowWithLifecycle pauses collection when the lifecycle drops below the given state, preventing UI updates to a non-visible fragment.


10. Integration with Room

Room’s @Query methods can return Flow, enabling reactive database observation:

@Dao
interface UserDao {
    @Query("SELECT * FROM users ORDER BY name")
    fun observeAll(): Flow<List<User>>
}

Every write to the users table causes the flow to re-emit with fresh data. Combine this with stateIn in the ViewModel for a fully reactive UI that updates automatically on data changes — no manual refresh logic.


11. Integration with Retrofit

Retrofit supports suspend functions natively:

interface UserApi {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: String): User
}

Call from a coroutine scope; exceptions (IOException, HttpException) propagate normally and can be caught with try/catch.

For streaming responses, return Flow:

@GET("events")
    fun events(): Flow<ServerEvent>

12. Testing Coroutines and Flow

12.1 The Test Dispatcher

Use kotlinx-coroutines-test to control virtual time:

@OptIn(ExperimentalCoroutinesApi::class)
class UserViewModelTest {

    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    @Test
    fun load_success_emitsSuccessState() = runTest {
        val viewModel = UserViewModel(FakeRepository())
        viewModel.load()
        advanceUntilIdle()

        val state = viewModel.uiState.value
        assertTrue(state is UiState.Success)
    }
}

runTest replaces real dispatchers with a StandardTestDispatcher and provides virtual time — delay(1000) completes instantly.

12.2 A Main Dispatcher Rule

@OptIn(ExperimentalCoroutinesApi::class)
class MainDispatcherRule : TestWatcher() {
    private val dispatcher = UnconfinedTestDispatcher()
    override fun starting(description: Description) {
        Dispatchers.setMain(dispatcher)
    }
    override fun finished(description: Description) {
        Dispatchers.resetMain()
    }
}

This replaces Dispatchers.Main with a test dispatcher so viewModelScope works in tests.

12.3 Testing Flow

Use Turbine (a Flow testing library) for concise assertions:

@Test
fun observeUsers_emitsInOrder() = runTest {
    repository.observeUsers().test {
        assertEquals(listOf<User>(), awaitItem())
        repository.insert(User("1", "Alice"))
        assertEquals(listOf(User("1", "Alice")), awaitItem())
        cancelAndIgnoreRemainingEvents()
    }
}

13. Common Pitfalls

  1. Using GlobalScope: It is never tied to a lifecycle and causes leaks. Always use viewModelScope, lifecycleScope, or a custom scoped CoroutineScope.
  2. Calling runBlocking in production: Blocks the thread. Use launch/async with a proper scope.
  3. Forgetting ensureActive() in tight loops: Long CPU loops that never suspend ignore cancellation.
  4. Catching CancellationException and not rethrowing: Breaks structured concurrency. If you catch it, rethrow it, or let it propagate.
  5. Using MutableStateFlow publicly: Expose StateFlow (read-only) and keep MutableStateFlow private.
  6. Collecting in onCreate without lifecycle awareness: Updates can crash when the view is destroyed. Use flowWithLifecycle or collectAsStateWithLifecycle.
  7. Mixing async without await: An exception in an unawaited async is silently dropped until the scope completes — easy to miss.
  8. Using Dispatchers.Default for I/O: Saturates the CPU pool with blocking calls. Use Dispatchers.IO.
  9. Creating a new CoroutineScope per operation: Loses structured concurrency. Reuse a single scope tied to a lifecycle.
  10. Treating Flow as hot: A cold Flow runs the producer per collector. If you need shared state, convert to StateFlow/SharedFlow.

14. Best Practices Summary

  1. Always use a structured scopeviewModelScope for ViewModels, lifecycleScope for lifecycle-bound work.
  2. Use suspend functions for one-shot operations and Flow for streams.
  3. Expose StateFlow for UI state and SharedFlow for one-shot events.
  4. Collect with lifecycle awarenesscollectAsStateWithLifecycle in Compose, flowWithLifecycle in fragments.
  5. Pick the right dispatcher — IO for blocking, Default for CPU, Main for UI.
  6. Handle errors explicitly — either try/catch, Result wrappers, or CoroutineExceptionHandler.
  7. Make cancellation cooperative — call ensureActive() or yield() in long loops.
  8. Test with runTest and a main dispatcher rule — never real delays or real dispatchers in unit tests.
  9. Never use GlobalScope or runBlocking in production code.
  10. Prefer coroutineScope/supervisorScope over ad-hoc launch for parallel decomposition with proper error propagation.

Conclusion

Coroutines and Flow replace the tangle of callbacks, threads, and reactive frameworks that Android developers once endured. Their power comes not from being yet another async primitive, but from structured concurrency — a model where every coroutine has a well-defined parent, lifetime, and cancellation path. Master the scopes (viewModelScope, lifecycleScope), the dispatchers (IO, Default, Main), the stream types (Flow cold, StateFlow/SharedFlow hot), and the testing utilities (runTest, Turbine), and your asynchronous code will be both concise and robust.

The investment in learning coroutines pays off across the entire stack: Room returns Flow, Retrofit supports suspend, Compose consumes StateFlow, and lifecycle libraries integrate natively. There is no part of modern Android development where coroutines are not the recommended tool.


Written as a technical reference for Android developers adopting coroutines and Flow in production code.

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。