$npx -y skills add hanamizuki/solopreneur --skill coroutinesAuthoritative rules and patterns for production-quality Kotlin Coroutines onto Android. Covers structured concurrency, lifecycle integration, and reactive streams.
| 1 | # Android Coroutines Expert Skill |
| 2 | |
| 3 | This skill provides authoritative rules and patterns for writing production-quality Kotlin Coroutines code on Android. It enforces structured concurrency, lifecycle safety, and modern best practices (2025 standards). |
| 4 | |
| 5 | ## Responsibilities |
| 6 | |
| 7 | * **Asynchronous Logic**: Implementing suspend functions, Dispatcher management, and parallel execution. |
| 8 | * **Reactive Streams**: Implementing `Flow`, `StateFlow`, `SharedFlow`, and `callbackFlow`. |
| 9 | * **Lifecycle Integration**: Managing scopes (`viewModelScope`, `lifecycleScope`) and safe collection (`repeatOnLifecycle`). |
| 10 | * **Error Handling**: Implementing `CoroutineExceptionHandler`, `SupervisorJob`, and proper `try-catch` hierarchies. |
| 11 | * **Cancellability**: Ensuring long-running operations are cooperative using `ensureActive()`. |
| 12 | * **Testing**: Setting up `TestDispatcher` and `runTest`. |
| 13 | |
| 14 | ## Applicability |
| 15 | |
| 16 | Activate this skill when the user asks to: |
| 17 | * "Fetch data from an API/Database." |
| 18 | * "Perform background processing." |
| 19 | * "Fix a memory leak" related to threads/tasks. |
| 20 | * "Convert a listener/callback to Coroutines." |
| 21 | * "Implement a ViewModel." |
| 22 | * "Handle UI state updates." |
| 23 | |
| 24 | ## Critical Rules & Constraints |
| 25 | |
| 26 | ### 1. Dispatcher Injection (Testability) |
| 27 | * **NEVER** hardcode Dispatchers (e.g., `Dispatchers.IO`, `Dispatchers.Default`) inside classes. |
| 28 | * **ALWAYS** inject a `CoroutineDispatcher` via the constructor. |
| 29 | * **DEFAULT** to `Dispatchers.IO` in the constructor argument for convenience, but allow it to be overridden. |
| 30 | |
| 31 | ```kotlin |
| 32 | // CORRECT |
| 33 | class UserRepository( |
| 34 | private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO |
| 35 | ) { ... } |
| 36 | |
| 37 | // INCORRECT |
| 38 | class UserRepository { |
| 39 | fun getData() = withContext(Dispatchers.IO) { ... } |
| 40 | } |
| 41 | ``` |
| 42 | |
| 43 | ### 2. Main-Safety |
| 44 | * All suspend functions defined in the Data or Domain layer must be **main-safe**. |
| 45 | * **One-shot calls** should be exposed as `suspend` functions. |
| 46 | * **Data changes** should be exposed as `Flow`. |
| 47 | * The caller (ViewModel) should be able to call them from `Dispatchers.Main` without blocking the UI. |
| 48 | * Use `withContext(dispatcher)` inside the repository implementation to move execution to the background. |
| 49 | |
| 50 | ### 3. Lifecycle-Aware Collection |
| 51 | * **NEVER** collect a flow directly in `lifecycleScope.launch` or `launchWhenStarted` (deprecated/unsafe). |
| 52 | * **ALWAYS** use `repeatOnLifecycle(Lifecycle.State.STARTED)` for collecting flows in Activities or Fragments. |
| 53 | |
| 54 | ```kotlin |
| 55 | // CORRECT |
| 56 | viewLifecycleOwner.lifecycleScope.launch { |
| 57 | viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { |
| 58 | viewModel.uiState.collect { ... } |
| 59 | } |
| 60 | } |
| 61 | ``` |
| 62 | |
| 63 | ### 4. ViewModel Scope Usage |
| 64 | * Use `viewModelScope` for initiating coroutines in ViewModels. |
| 65 | * Do not expose suspend functions from the ViewModel to the View. The ViewModel should expose `StateFlow` or `SharedFlow` that the View observes. |
| 66 | |
| 67 | ### 5. Mutable State Encapsulation |
| 68 | * **NEVER** expose `MutableStateFlow` or `MutableSharedFlow` publicly. |
| 69 | * Expose them as read-only `StateFlow` or `Flow` using `.asStateFlow()` or upcasting. |
| 70 | |
| 71 | ### 6. GlobalScope Prohibition |
| 72 | * **NEVER** use `GlobalScope`. It breaks structured concurrency and leads to leaks. |
| 73 | * If a task must survive the current scope, use an injected `applicationScope` (a custom scope tied to the Application lifecycle). |
| 74 | |
| 75 | ### 7. Exception Handling |
| 76 | * **NEVER** catch `CancellationException` in a generic `catch (e: Exception)` block without rethrowing it. |
| 77 | * Use `runCatching` only if you explicitly rethrow `CancellationException`. |
| 78 | * Use `CoroutineExceptionHandler` only for top-level coroutines (inside `launch`). It has no effect inside `async` or child coroutines. |
| 79 | |
| 80 | ### 8. Cancellability |
| 81 | * Coroutines feature **cooperative cancellation**. They don't stop immediately unless they check for cancellation. |
| 82 | * **ALWAYS** call `ensureActive()` or `yield()` in tight loops (e.g., processing a large list, reading files) to check for cancellation. |
| 83 | * Standard functions like `delay()` and `withContext()` are already cancellable. |
| 84 | |
| 85 | ### 9. Callback Conversion |
| 86 | * Use `callbackFlow` to convert callback-based APIs to Flow. |
| 87 | * **ALWAYS** use `awaitClose` at the end of the `callbackFlow` block to unregister listeners. |
| 88 | |
| 89 | ## Code Patterns |
| 90 | |
| 91 | ### Repository Pattern with Flow |
| 92 | |
| 93 | ```kotlin |
| 94 | class NewsRepository( |
| 95 | private val remoteDataSource: NewsRemoteDataSource, |
| 96 | private val externalScope: CoroutineScope, // For app-wide events |
| 97 | private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO |
| 98 | ) { |
| 99 | val newsUpdates: Flow<List<News>> = flow { |
| 100 | val news = remoteDataSource.fetchLatestNews() |
| 101 | emit(news) |
| 102 | }.flowOn(ioDispatcher) // Upstream executes on IO |
| 103 | } |
| 104 | ``` |
| 105 | |
| 106 | ### Parallel Execution |
| 107 | |
| 108 | ```kotlin |
| 109 | suspend fun loadDashboardData() = coroutineScope { |
| 110 | val userDeferred = async { |