A guide to mastering asynchronous programming with Kotlin Coroutines. Covers advanced topics like structured concurrency, Flow transformations, exception handling, and testing strategies.
Flow.Always launch coroutines within a defined CoroutineScope. Use coroutineScope or supervisorScope to group concurrent tasks.
suspend fun loadDashboardData(): DashboardData = coroutineScope {
val userDeferred = async { userRepo.getUser() }
val settingsDeferred = async { settingsRepo.getSettings() }
DashboardData(
user = userDeferred.await(),
settings = settingsDeferred.await()
)
}
Use CoroutineExceptionHandler for top-level scopes, but rely on try-catch within suspending functions for granular control.
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
viewModelScope.launch(handler) {
try {
riskyOperation()
} catch (e: IOException) {
// Handle network error specifically
}
}
Use StateFlow for state that needs to be retained, and SharedFlow for events.
// Cold Flow (Lazy)
val searchResults: Flow<List<Item>> = searchQuery
.debounce(300)
.flatMapLatest { query -> searchRepo.search(query) }
.flowOn(Dispatchers.IO)
// Hot Flow (State)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
suspend fun fetchDataWithErrorHandling() = supervisorScope {
val task1 = async {
try { api.fetchA() } catch (e: Exception) { null }
}
val task2 = async { api.fetchB() }
// If task2 fails, task1 is NOT cancelled because of supervisorScope
val result1 = task1.await()
val result2 = task2.await() // May throw
}
Dispatchers.IO for blocking I/O operations.ViewModel.onCleared).TestScope and runTest for unit testing coroutines.GlobalScope. It breaks structured concurrency and can lead to leaks.CancellationException unless you rethrow it.Problem: Coroutine test hangs or fails unpredictably.
Solution: Ensure you are using runTest and injecting TestDispatcher into your classes so you can control virtual time.