$npx -y skills add hanamizuki/solopreneur --skill kotlin-concurrency-expertKotlin Coroutines review and remediation for Android. Use when asked to review concurrency usage, fix coroutine-related bugs, improve thread safety, or resolve lifecycle issues in Kotlin/Android code.
| 1 | # Kotlin Concurrency Expert |
| 2 | |
| 3 | ## Overview |
| 4 | |
| 5 | Review and fix Kotlin Coroutines issues in Android codebases by applying structured concurrency, lifecycle safety, proper scoping, and modern best practices with minimal behavior changes. |
| 6 | |
| 7 | ## Workflow |
| 8 | |
| 9 | ### 1. Triage the Issue |
| 10 | |
| 11 | - Capture the exact error, crash, or symptom (ANR, memory leak, race condition, incorrect state). |
| 12 | - Check project coroutines setup: `kotlinx-coroutines-android` version, `lifecycle-runtime-ktx` version. |
| 13 | - Identify the current scope context (`viewModelScope`, `lifecycleScope`, custom scope, or none). |
| 14 | - Confirm whether the code is UI-bound (`Dispatchers.Main`) or intended to run off the main thread (`Dispatchers.IO`, `Dispatchers.Default`). |
| 15 | - Verify Dispatcher injection patterns for testability. |
| 16 | |
| 17 | ### 2. Apply the Smallest Safe Fix |
| 18 | |
| 19 | Prefer edits that preserve existing behavior while satisfying structured concurrency and lifecycle safety. |
| 20 | |
| 21 | Common fixes: |
| 22 | |
| 23 | - **ANR / Main thread blocking**: Move heavy work to `withContext(Dispatchers.IO)` or `Dispatchers.Default`; ensure suspend functions are main-safe. |
| 24 | - **Memory leaks / zombie coroutines**: Replace `GlobalScope` with a lifecycle-bound scope (`viewModelScope`, `lifecycleScope`, or injected `applicationScope`). |
| 25 | - **Lifecycle collection issues**: Replace deprecated `launchWhenStarted` with `repeatOnLifecycle(Lifecycle.State.STARTED)`. |
| 26 | - **State exposure**: Encapsulate `MutableStateFlow` / `MutableSharedFlow`; expose read-only `StateFlow` or `Flow`. |
| 27 | - **CancellationException swallowing**: Ensure generic `catch (e: Exception)` blocks rethrow `CancellationException`. |
| 28 | - **Non-cooperative cancellation**: Add `ensureActive()` or `yield()` in tight loops for cooperative cancellation. |
| 29 | - **Callback APIs**: Convert listeners to `callbackFlow` with proper `awaitClose` cleanup. |
| 30 | - **Hardcoded Dispatchers**: Inject `CoroutineDispatcher` via constructor for testability. |
| 31 | |
| 32 | ## Critical Rules |
| 33 | |
| 34 | ### Dispatcher Injection (Testability) |
| 35 | |
| 36 | ```kotlin |
| 37 | // CORRECT: Inject dispatcher |
| 38 | class UserRepository( |
| 39 | private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO |
| 40 | ) { |
| 41 | suspend fun fetchUser() = withContext(ioDispatcher) { ... } |
| 42 | } |
| 43 | |
| 44 | // INCORRECT: Hardcoded dispatcher |
| 45 | class UserRepository { |
| 46 | suspend fun fetchUser() = withContext(Dispatchers.IO) { ... } |
| 47 | } |
| 48 | ``` |
| 49 | |
| 50 | ### Lifecycle-Aware Collection |
| 51 | |
| 52 | ```kotlin |
| 53 | // CORRECT: Use repeatOnLifecycle |
| 54 | viewLifecycleOwner.lifecycleScope.launch { |
| 55 | viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { |
| 56 | viewModel.uiState.collect { state -> updateUI(state) } |
| 57 | } |
| 58 | } |
| 59 | |
| 60 | // INCORRECT: Direct collection (unsafe, deprecated) |
| 61 | lifecycleScope.launchWhenStarted { |
| 62 | viewModel.uiState.collect { state -> updateUI(state) } |
| 63 | } |
| 64 | ``` |
| 65 | |
| 66 | ### State Encapsulation |
| 67 | |
| 68 | ```kotlin |
| 69 | // CORRECT: Expose read-only StateFlow |
| 70 | class MyViewModel : ViewModel() { |
| 71 | private val _uiState = MutableStateFlow(UiState()) |
| 72 | val uiState: StateFlow<UiState> = _uiState.asStateFlow() |
| 73 | } |
| 74 | |
| 75 | // INCORRECT: Exposed mutable state |
| 76 | class MyViewModel : ViewModel() { |
| 77 | val uiState = MutableStateFlow(UiState()) // Leaks mutability |
| 78 | } |
| 79 | ``` |
| 80 | |
| 81 | ### Exception Handling |
| 82 | |
| 83 | ```kotlin |
| 84 | // CORRECT: Rethrow CancellationException |
| 85 | try { |
| 86 | doSuspendWork() |
| 87 | } catch (e: CancellationException) { |
| 88 | throw e // Must rethrow! |
| 89 | } catch (e: Exception) { |
| 90 | handleError(e) |
| 91 | } |
| 92 | |
| 93 | // INCORRECT: Swallows cancellation |
| 94 | try { |
| 95 | doSuspendWork() |
| 96 | } catch (e: Exception) { |
| 97 | handleError(e) // CancellationException swallowed! |
| 98 | } |
| 99 | ``` |
| 100 | |
| 101 | ### Cooperative Cancellation |
| 102 | |
| 103 | ```kotlin |
| 104 | // CORRECT: Check for cancellation in tight loops |
| 105 | suspend fun processLargeList(items: List<Item>) { |
| 106 | items.forEach { item -> |
| 107 | ensureActive() // Check cancellation |
| 108 | processItem(item) |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | // INCORRECT: Non-cooperative (ignores cancellation) |
| 113 | suspend fun processLargeList(items: List<Item>) { |
| 114 | items.forEach { item -> |
| 115 | processItem(item) // Never checks cancellation |
| 116 | } |
| 117 | } |
| 118 | ``` |
| 119 | |
| 120 | ### Callback Conversion |
| 121 | |
| 122 | ```kotlin |
| 123 | // CORRECT: callbackFlow with awaitClose |
| 124 | fun locationUpdates(): Flow<Location> = callbackFlow { |
| 125 | val listener = LocationListener { location -> |
| 126 | trySend(location) |
| 127 | } |
| 128 | locationManager.requestLocationUpdates(listener) |
| 129 | |
| 130 | awaitClose { locationManager.removeUpdates(listener) } |
| 131 | } |
| 132 | ``` |
| 133 | |
| 134 | ## Scope Guidelines |
| 135 | |
| 136 | | Scope | Use When | Lifecycle | |
| 137 | |-------|----------|-----------| |
| 138 | | `viewModelScope` | ViewModel operations | Cleared with ViewModel | |
| 139 | | `lifecycleScope` | UI operations in Activity/Fragment | Destroyed with lifecycle owner | |
| 140 | | `repeatOnLifecycle` | Flow collection in UI | Started/Stopped with lifecycle state | |
| 141 | | `applicationScope` (injected) | App-wide background work | Application lifetime | |
| 142 | | `GlobalScope` | **NEVER USE** | Breaks structured concurrency | |
| 143 | |
| 144 | ## Testing Pattern |
| 145 | |
| 146 | ```kotlin |
| 147 | @Test |
| 148 | fun `loa |