# Kotlin Concurrency

> 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: `thetruong1099/kotlin-concurrency` (Agent Skill)
- Install (CLI): `npx skillmds@latest add thetruong1099/kotlin-concurrency`
- Raw SKILL.md: https://api.skillmd.com/api/skills/thetruong1099/kotlin-concurrency/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: thetruong1099 (https://skillmd.com/u/thetruong1099)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/thetruong1099/kotlin-concurrency

---


# 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

```kotlin
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

```kotlin
// 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

```kotlin
// 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

```kotlin
// 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

```kotlin
// 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

```kotlin
// 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(...)
    }
}
```

