Kotlin concurrency
Skill thetruong1099/android-mvi-base-code/.claude/skills/kotlin-concurrency
npx -y skills add thetruong1099/android-mvi-base-code --skill kotlin-concurrencyAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Kotlin coroutine and Flow patterns for this project. Covers when to use Flow vs suspend in BaseViewModel, Flow operators (map/filter/combine/flatMapLatest/debounce), StateFlow vs Channel vs SharedFlow, Dispatcher selection, structured concurrency with coroutineScope and supervisorScope, and cancellation patterns.
SKILL.md
4.2 KB, as published. Nobody here has run it
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() },
onError = { 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)),
onSuccess = { setState { copy(detail = it) } },
onError = { showErrorToast(it) },
)
}
}
// One-shot suspend
private fun saveItem(id: String) {
viewModelScope.launch {
callSuspendWithInternet(
operation = { saveItemUseCase(SaveParam(id)) },
onSuccess = { showSuccessToast(R.string.saved) },
onError = { 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(...)
}
}