Kotlin Concurrency
Coroutine Scopes
| Scope |
Where |
Lifecycle |
viewModelScope |
BaseViewModel subclasses |
ViewModel lifecycle |
lifecycleScope |
Activity/Fragment |
Lifecycle owner |
GlobalScope |
NEVER use |
N/A |
Flow vs Suspend in BaseViewModel
Use Flow (non-suspend) for PagingData
private fun loadItems() {
val result = callPagingDataWithInternet(
callFlow = { useCase() },
showErrorToast(it) },
).cachedIn(viewModelScope)
setState { copy(items = result) }
}
Use suspend for DataState and one-shot operations
// DataState - terminal operation
private fun loadDetail(id: String) {
viewModelScope.launch {
collectDataStateWithInternet(
callFlow = useCase(GetDetailParam(id)),
setState { copy(detail = it) } },
showErrorToast(it) },
)
}
}
// One-shot suspend
private fun saveItem(id: String) {
viewModelScope.launch {
callSuspendWithInternet(
operation = { saveItemUseCase(SaveParam(id)) },
showSuccessToast(R.string.saved) },
showErrorToast(it) },
)
}
}
Flow Operators
// map - transform emissions
repository.getItem(id).map { dto -> mapper.toDomain(dto) }
// combine - merge multiple flows
combine(userFlow, settingsFlow) { user, settings -> UserWithSettings(user, settings) }
// flatMapLatest - cancel previous when new arrives (search)
searchQueryFlow.flatMapLatest { query -> repository.search(query) }
// debounce + distinctUntilChanged - rate limit (search input)
searchQueryFlow
.debounce(300)
.distinctUntilChanged()
.flatMapLatest { repository.search(it) }
// catch - handle errors in flow
dataFlow.catch { e -> emit(DataState.Error(exceptionMapper.mapToAppError(e))) }
// onStart/onCompletion - lifecycle hooks
dataFlow
.onStart { emit(DataState.Loading()) }
.onCompletion { onLoading(false) }
StateFlow vs Channel vs SharedFlow
| Type |
Use case |
This project |
StateFlow |
UI state (always has value) |
uiState, loadingState, toastState |
Channel |
One-time events (consume once) |
effectFlow |
SharedFlow |
Events to multiple collectors |
Not currently used |
Dispatchers
// IO - Network, database, file operations
withContext(Dispatchers.IO) { /* network/db call */ } // Used in BaseDataSource strategies
// Default - CPU-intensive work
withContext(Dispatchers.Default) { parseHtmlContent(html) }
// Main - UI updates (viewModelScope uses Main by default)
// Tests: MainDispatcherRule replaces Main with TestDispatcher
Structured Concurrency
// Parallel operations
suspend fun loadDashboard() = coroutineScope {
val items = async { repository.getItems() }
val categories = async { repository.getCategories() }
setState { copy(items = items.await(), categories = categories.await()) }
}
// SupervisorScope - one failure doesn't cancel others
supervisorScope {
launch { syncItems() } // Can fail independently
launch { syncUserData() } // Not affected by syncItems failure
}
Cancellation
// viewModelScope auto-cancels when ViewModel is cleared - no manual cancellation needed
// Manual cancellation for search debounce pattern:
private var searchJob: Job? = null
fun search(query: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
collectDataStateWithInternet(...)
}
}