Coroutines and Flow are now the standard async foundation for Android apps. This guide focuses on the practical patterns you will use every day — not theory, but the actual code that solves real problems.
ViewModel and viewModelScope
Always launch coroutines from viewModelScope inside ViewModels. This scope is automatically
cancelled when the ViewModel is cleared, preventing leaks.
class SearchViewModel(private val repo: SearchRepo) : ViewModel() {
private val _results = MutableStateFlow<List<Result>>(emptyList())
val results: StateFlow<List<Result>> = _results.asStateFlow()
fun search(query: String) {
viewModelScope.launch {
_results.value = repo.search(query)
}
}
}
StateFlow vs SharedFlow
- StateFlow — holds the current value, replays it to new collectors. Use for UI state.
- SharedFlow — does not hold state, can replay N events. Use for one-shot events (navigation, toasts).
// One-shot events via SharedFlow
private val _events = MutableSharedFlow<UiEvent>()
val events: SharedFlow<UiEvent> = _events.asSharedFlow()
fun onSubmit() = viewModelScope.launch {
_events.emit(UiEvent.NavigateToHome)
}
Combining Flows
combine merges multiple flows and emits whenever any source emits. Essential for
UIs that depend on multiple data sources.
val uiState: StateFlow<HomeUiState> = combine(
repo.user,
repo.feed,
repo.notifications
) { user, feed, notifs ->
HomeUiState(user = user, feed = feed, unreadCount = notifs.size)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), HomeUiState())
Error Handling
Never let exceptions silently swallow in coroutines. Use runCatching or a
CoroutineExceptionHandler at the launch site.
viewModelScope.launch {
runCatching { repo.refresh() }
.onFailure { e -> _error.value = e.message }
}
Testing Flows with Turbine
The Turbine library makes Flow testing concise and readable.
@Test
fun `search returns results`() = runTest {
viewModel.results.test {
viewModel.search("android")
assertEquals(expectedList, awaitItem())
cancelAndIgnoreRemainingEvents()
}
}
Key Rules
- Use
Dispatchers.IOfor network and database work. - Prefer
stateIn(WhileSubscribed(5000))on StateFlows shared to UI — the 5-second grace period survives screen rotations. - Avoid
GlobalScope— it does not cancel and causes leaks. - Use
flowOnto change the context of cold flows, notwithContext.