Structured concurrency: the parent cancels the children. Flow is a cold stream; StateFlow is state. Language: kotlin. Android ViewModels: android-development / android-unit-testing.
Stack and scope
| Item | Role |
|---|---|
| kotlinx.coroutines | launch, async, scopes |
| Flow | Streams (map, retry, sharing) |
| StateFlow / SharedFlow | State / UI events |
| Dispatchers | Main, IO, Default |
Do not mix in new RxJava if the project already uses Flow — preserve legacy Rx.
Non-negotiable rules
- Every
launchhas scope (viewModelScope,lifecycleScope,coroutineScopeof the use case). WithoutGlobalScope. - Propagate
CoroutineContext/CancellationExceptionupward — do notcatch (e: Exception)swallowing cancellation. - I/O on
Dispatchers.IO(or the injected dispatcher). CPU onDefault. UI onMain. withContextto change dispatcher, notlaunch(IO)nested without need.- Timeout on external I/O (
withTimeout). - Tests:
runTest+StandardTestDispatcher— skillandroid-unit-testing.
Structured concurrency
suspend fun loadProfile(id: UserId): Profile = coroutineScope {
val user = async { users.fetch(id) }
val flags = async { flags.fetch(id) }
Profile(user.await(), flags.await())
}
asynconly withawaitin the same scope. Avoidasyncfire-and-forget.supervisorScopewhen a child can fail without cancelling its siblings — document it.NonCancellableonly in thefinallyof cleanup (closing a resource).
Flow
- Builder:
flow { }withemit. Cold by default. stateIn/shareInwith explicitSharingStartedandscopeof the ViewModel.catchat the end of the UI chain; log + stateErr.flatMapLatestfor queries tied to input (cancels the previous one).- Backpressure: do not do heavy work in
mapwithoutflowOn. SharedFlowfor one-shot events (snackbar); do not replace the screen's StateFlow.
val ui: StateFlow<LoadState<Profile>> = id
.flatMapLatest { repo.profile(it) }
.map<Profile, LoadState<Profile>> { LoadState.Ready(it) }
.onStart { emit(LoadState.Loading) }
.catch { emit(LoadState.Err(it.message ?: "error")) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), LoadState.Loading)
Android
viewModelScope+SupervisorJobalready included. Do not launch oninitwork that the test cannot advance.lifecycle.repeatOnLifecycleto collect on the UI — do not collect Flow without lifecycle.- Room/DataStore already expose Flow:
android-room.
Errors and cancellation
CancellationExceptionrethrows. Others: map to the domain.ensureActive()in long loops.- Combine
Jobof the request: a new search cancels the old one.
Anti-patterns
GlobalScope.launchrunBlockingin production / on MainFlowthat emits from Main with I/O inside- Converting callback hell into
callbackFlowwithoutawaitClose delayas synchronization between product testsSharedFlow(replay=1)mimicking StateFlow
Integration with other skills
- Kotlin idiom:
kotlin - Android UI:
jetpack-compose/android-development - Tests:
android-unit-testing - Persistence:
android-room
Completion criteria
- Clear scopes; no GlobalScope
- Cooperative cancellation; timeouts on I/O
- Flow/StateFlow with explicit started
- Correct Dispatchers
runTestour ViewModels/use cases touched