android-sdd 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/dist/index.js +143 -0
  2. package/package.json +27 -0
  3. package/skills/Android Ecosystem/Baseline Profile Generator/SKILL.md +277 -0
  4. package/skills/Android Ecosystem/Glance/SKILL.md +315 -0
  5. package/skills/Android Platform/Configuration/SKILL.md +201 -0
  6. package/skills/Android Platform/Filesystem/SKILL.md +216 -0
  7. package/skills/Android Platform/Lifecycle/SKILL.md +233 -0
  8. package/skills/Android Platform/Manifest/SKILL.md +226 -0
  9. package/skills/Android Platform/Process Death Recovery/SKILL.md +214 -0
  10. package/skills/Android Platform/Resources/SKILL.md +234 -0
  11. package/skills/Android Platform/SavedStateHandle/SKILL.md +217 -0
  12. package/skills/Android Platform/State Restoration/SKILL.md +210 -0
  13. package/skills/Architecture/Bounded Context/SKILL.md +207 -0
  14. package/skills/Architecture/Clean Architecture/SKILL.md +229 -0
  15. package/skills/Architecture/Domain Modeling/SKILL.md +236 -0
  16. package/skills/Architecture/Entity Design/SKILL.md +243 -0
  17. package/skills/Architecture/Feature Isolation/SKILL.md +216 -0
  18. package/skills/Architecture/MVI/SKILL.md +224 -0
  19. package/skills/Architecture/MVVM/SKILL.md +198 -0
  20. package/skills/Architecture/Modularization/SKILL.md +194 -0
  21. package/skills/Architecture/Offline First/SKILL.md +249 -0
  22. package/skills/Architecture/Repository Pattern/SKILL.md +216 -0
  23. package/skills/Architecture/Side Effect Management/SKILL.md +278 -0
  24. package/skills/Architecture/State Management/SKILL.md +229 -0
  25. package/skills/Architecture/Unidirectional Data Flow/SKILL.md +196 -0
  26. package/skills/Architecture/Use Case Design/SKILL.md +244 -0
  27. package/skills/Architecture/Value Object/SKILL.md +226 -0
  28. package/skills/Build Infrastructure/Build Orchestration/SKILL.md +257 -0
  29. package/skills/Build Infrastructure/Dependency Compatibility Resolver/SKILL.md +259 -0
  30. package/skills/Build Infrastructure/Environment Validator/SKILL.md +311 -0
  31. package/skills/Build System/Build Cache/SKILL.md +233 -0
  32. package/skills/Build System/Build Flavor Strategy/SKILL.md +171 -0
  33. package/skills/Build System/Build Variant/SKILL.md +215 -0
  34. package/skills/Build System/Convention Plugin/SKILL.md +288 -0
  35. package/skills/Build System/Dependency Management/SKILL.md +261 -0
  36. package/skills/Build System/Gradle/SKILL.md +284 -0
  37. package/skills/Build System/Incremental Build/SKILL.md +199 -0
  38. package/skills/Build System/KAPT/SKILL.md +198 -0
  39. package/skills/Build System/KSP/SKILL.md +263 -0
  40. package/skills/Build System/Module Dependency Graph Validation/SKILL.md +223 -0
  41. package/skills/Build System/Specialized/C++/SKILL.md +308 -0
  42. package/skills/Build System/Specialized/JNI/SKILL.md +306 -0
  43. package/skills/Build System/Specialized/NDK/SKILL.md +264 -0
  44. package/skills/Build System/Version Catalog/SKILL.md +304 -0
  45. package/skills/Concurrency/Background Processing/SKILL.md +185 -0
  46. package/skills/Concurrency/Channel/SKILL.md +207 -0
  47. package/skills/Concurrency/Coroutine/SKILL.md +200 -0
  48. package/skills/Concurrency/Flow/SKILL.md +179 -0
  49. package/skills/Concurrency/Mutex Strategy/SKILL.md +185 -0
  50. package/skills/Concurrency/SharedFlow/SKILL.md +171 -0
  51. package/skills/Concurrency/StateFlow/SKILL.md +175 -0
  52. package/skills/Concurrency/Structured Concurrency/SKILL.md +197 -0
  53. package/skills/Concurrency/Synchronization Policy/SKILL.md +192 -0
  54. package/skills/Core Language/Annotation Processing/SKILL.md +224 -0
  55. package/skills/Core Language/DSL/SKILL.md +186 -0
  56. package/skills/Core Language/Extension Functions Design/SKILL.md +191 -0
  57. package/skills/Core Language/Immutability/SKILL.md +156 -0
  58. package/skills/Core Language/KMP/SKILL.md +182 -0
  59. package/skills/Core Language/Kotlin/SKILL.md +187 -0
  60. package/skills/Core Language/Reactive State Management/SKILL.md +228 -0
  61. package/skills/Core Language/Reactive Streams/SKILL.md +235 -0
  62. package/skills/Core Language/Serialization/SKILL.md +191 -0
  63. package/skills/Data Layer/Cache Strategy/SKILL.md +261 -0
  64. package/skills/Data Layer/Conflict Resolution/SKILL.md +248 -0
  65. package/skills/Data Layer/DAO/SKILL.md +225 -0
  66. package/skills/Data Layer/DTO Mapping/SKILL.md +269 -0
  67. package/skills/Data Layer/DataStore/SKILL.md +264 -0
  68. package/skills/Data Layer/Database Versioning Strategy/SKILL.md +215 -0
  69. package/skills/Data Layer/Encrypted Database/SKILL.md +212 -0
  70. package/skills/Data Layer/File Storage/SKILL.md +247 -0
  71. package/skills/Data Layer/Indexing/SKILL.md +184 -0
  72. package/skills/Data Layer/Key-Value Store Strategy/SKILL.md +185 -0
  73. package/skills/Data Layer/Merge Strategy/SKILL.md +240 -0
  74. package/skills/Data Layer/Migration/SKILL.md +243 -0
  75. package/skills/Data Layer/Paging/SKILL.md +264 -0
  76. package/skills/Data Layer/Proto DataStore/SKILL.md +250 -0
  77. package/skills/Data Layer/Room/SKILL.md +244 -0
  78. package/skills/Data Layer/SQLite/SKILL.md +255 -0
  79. package/skills/Data Layer/Sync Engine/SKILL.md +268 -0
  80. package/skills/Dependency Injection/Dagger/SKILL.md +283 -0
  81. package/skills/Dependency Injection/Hilt/SKILL.md +345 -0
  82. package/skills/Dependency Injection/Koin/SKILL.md +282 -0
  83. package/skills/Developer Experience/Detekt/SKILL.md +272 -0
  84. package/skills/Developer Experience/Lint Rule/SKILL.md +281 -0
  85. package/skills/Google Ecosystem/Analytics/SKILL.md +281 -0
  86. package/skills/Google Ecosystem/Crashlytics/SKILL.md +234 -0
  87. package/skills/Google Ecosystem/Firebase/SKILL.md +200 -0
  88. package/skills/Google Ecosystem/Firebase Messaging/SKILL.md +266 -0
  89. package/skills/Media/Audio/SKILL.md +257 -0
  90. package/skills/Media/Camera/SKILL.md +229 -0
  91. package/skills/Media/CameraX/SKILL.md +295 -0
  92. package/skills/Media/ExoPlayer/SKILL.md +258 -0
  93. package/skills/Media/Video/SKILL.md +228 -0
  94. package/skills/Meta Skills/Domain Error Model/SKILL.md +238 -0
  95. package/skills/Meta Skills/Error Handling/SKILL.md +255 -0
  96. package/skills/Meta Skills/Error Mapping/SKILL.md +232 -0
  97. package/skills/Meta Skills/Failure Strategy/SKILL.md +294 -0
  98. package/skills/Meta Skills/Migration Strategy/SKILL.md +305 -0
  99. package/skills/Meta Skills/User Friendly Errors/SKILL.md +334 -0
  100. package/skills/Navigation/Deep Navigation/SKILL.md +209 -0
  101. package/skills/Navigation/Navigation/SKILL.md +215 -0
  102. package/skills/Navigation/Nested Navigation/SKILL.md +214 -0
  103. package/skills/Networking/API Contract/SKILL.md +220 -0
  104. package/skills/Networking/Authentication/SKILL.md +210 -0
  105. package/skills/Networking/Certificate Pinning/SKILL.md +167 -0
  106. package/skills/Networking/Fallback Strategy/SKILL.md +182 -0
  107. package/skills/Networking/Ktor/SKILL.md +219 -0
  108. package/skills/Networking/Multipart Upload/SKILL.md +213 -0
  109. package/skills/Networking/OkHttp/SKILL.md +193 -0
  110. package/skills/Networking/REST/SKILL.md +178 -0
  111. package/skills/Networking/Rate Limiting/SKILL.md +170 -0
  112. package/skills/Networking/Retrofit/SKILL.md +241 -0
  113. package/skills/Networking/Retry-Backoff/SKILL.md +181 -0
  114. package/skills/Networking/Server-Sent Events (SSE)/SKILL.md +196 -0
  115. package/skills/Networking/WebSocket/SKILL.md +224 -0
  116. package/skills/Observability/Crash Reporting/SKILL.md +219 -0
  117. package/skills/Observability/Logging/SKILL.md +168 -0
  118. package/skills/Observability/Metrics/SKILL.md +227 -0
  119. package/skills/Observability/Structured Logging/SKILL.md +234 -0
  120. package/skills/Performance/ANR Prevention/SKILL.md +192 -0
  121. package/skills/Performance/Allocation Optimization/SKILL.md +179 -0
  122. package/skills/Performance/App Startup/SKILL.md +183 -0
  123. package/skills/Performance/Baseline Profile/SKILL.md +205 -0
  124. package/skills/Performance/Battery Optimization/SKILL.md +192 -0
  125. package/skills/Performance/Benchmark/SKILL.md +182 -0
  126. package/skills/Performance/Bitmap Optimization/SKILL.md +178 -0
  127. package/skills/Performance/Compose Optimization/SKILL.md +187 -0
  128. package/skills/Performance/Heap Management/SKILL.md +184 -0
  129. package/skills/Performance/Macrobenchmark/SKILL.md +214 -0
  130. package/skills/Performance/Memory Leak Prevention/SKILL.md +218 -0
  131. package/skills/Performance/Rendering Performance/SKILL.md +205 -0
  132. package/skills/Performance/Startup Optimization/SKILL.md +219 -0
  133. package/skills/Security/Biometric/SKILL.md +224 -0
  134. package/skills/Security/Certificate Transparency/SKILL.md +158 -0
  135. package/skills/Security/Cryptography/SKILL.md +244 -0
  136. package/skills/Security/Encrypted Storage/SKILL.md +273 -0
  137. package/skills/Security/Frida Detection/SKILL.md +230 -0
  138. package/skills/Security/Hook Detection/SKILL.md +197 -0
  139. package/skills/Security/Keystore/SKILL.md +272 -0
  140. package/skills/Security/Network Security Config/SKILL.md +186 -0
  141. package/skills/Security/Obfuscation/SKILL.md +226 -0
  142. package/skills/Security/Proguard/SKILL.md +202 -0
  143. package/skills/Security/R8/SKILL.md +234 -0
  144. package/skills/Security/Reverse Engineering Resistance/SKILL.md +267 -0
  145. package/skills/Security/Root Detection/SKILL.md +220 -0
  146. package/skills/Security/Secure Networking/SKILL.md +220 -0
  147. package/skills/System Integration/AlarmManager/SKILL.md +182 -0
  148. package/skills/System Integration/App Widget/SKILL.md +182 -0
  149. package/skills/System Integration/Deep Link/SKILL.md +187 -0
  150. package/skills/System Integration/Foreground Service/SKILL.md +212 -0
  151. package/skills/System Integration/Notification/SKILL.md +237 -0
  152. package/skills/System Integration/WorkManager/SKILL.md +256 -0
  153. package/skills/System Integration/clipboard/SKILL.md +155 -0
  154. package/skills/System Integration/share-intent/SKILL.md +182 -0
  155. package/skills/Testing/Compose Testing/SKILL.md +296 -0
  156. package/skills/Testing/Espresso/SKILL.md +292 -0
  157. package/skills/Testing/Fake Data/SKILL.md +245 -0
  158. package/skills/Testing/Integration Testing/SKILL.md +288 -0
  159. package/skills/Testing/Mocking/SKILL.md +229 -0
  160. package/skills/Testing/Snapshot Testing/SKILL.md +259 -0
  161. package/skills/Testing/UI Testing/SKILL.md +293 -0
  162. package/skills/Testing/Unit Testing/SKILL.md +309 -0
  163. package/skills/UI System/Bottom Sheet Patterns/SKILL.md +279 -0
  164. package/skills/UI System/Compose/SKILL.md +296 -0
  165. package/skills/UI System/Compose Animation/SKILL.md +281 -0
  166. package/skills/UI System/Compose Multiplatform/SKILL.md +261 -0
  167. package/skills/UI System/Compose Navigation/SKILL.md +255 -0
  168. package/skills/UI System/Compose Performance/SKILL.md +274 -0
  169. package/skills/UI System/Design System/SKILL.md +217 -0
  170. package/skills/UI System/Empty State Strategy/SKILL.md +208 -0
  171. package/skills/UI System/Keyboard Navigation/SKILL.md +214 -0
  172. package/skills/UI System/Loading Strategy/SKILL.md +254 -0
  173. package/skills/UI System/Material 3/SKILL.md +279 -0
  174. package/skills/UI System/RTL/SKILL.md +179 -0
  175. package/src/index.ts +182 -0
  176. package/tsconfig.json +19 -0
@@ -0,0 +1,171 @@
1
+ ---
2
+ name: sharedflow
3
+ description: >
4
+ SharedFlow for hot event streams in Android with Kotlin coroutines.
5
+ Load this skill when broadcasting events to multiple collectors,
6
+ implementing one-time UI events (navigation, toasts, snackbars),
7
+ sharing a stream between multiple observers, or replacing LiveData events.
8
+ ---
9
+
10
+ # SharedFlow
11
+
12
+ ## Overview
13
+
14
+ `SharedFlow` is a hot Flow that broadcasts values to all active collectors. Unlike `StateFlow`, it has no initial value and does not replay the last value by default. It is the correct tool for one-time events (navigation, toasts) and shared event buses.
15
+
16
+ ---
17
+
18
+ ## Core Principles
19
+
20
+ - Use `SharedFlow` for **events** — things that happen once and don't have a current value
21
+ - Use `StateFlow` for **state** — things that have a current value that persists
22
+ - Default `replay = 0` is correct for one-time events — never replay navigation events
23
+ - Use `Channel` as an alternative for guaranteed delivery when the collector may not be active
24
+ - `MutableSharedFlow` is the writable version — expose as `SharedFlow` to consumers
25
+
26
+ ---
27
+
28
+ ## Basic SharedFlow
29
+
30
+ ```kotlin
31
+ // ✅ One-time event stream
32
+ class UserViewModel : ViewModel() {
33
+
34
+ private val _events = MutableSharedFlow<UserEvent>()
35
+ val events: SharedFlow<UserEvent> = _events.asSharedFlow()
36
+
37
+ fun onDeleteSuccess() {
38
+ viewModelScope.launch {
39
+ _events.emit(UserEvent.NavigateBack)
40
+ }
41
+ }
42
+
43
+ fun onError(message: String) {
44
+ viewModelScope.launch {
45
+ _events.emit(UserEvent.ShowToast(message))
46
+ }
47
+ }
48
+ }
49
+
50
+ sealed interface UserEvent {
51
+ data object NavigateBack : UserEvent
52
+ data class ShowToast(val message: String) : UserEvent
53
+ }
54
+ ```
55
+
56
+ ---
57
+
58
+ ## Collecting SharedFlow in Compose
59
+
60
+ ```kotlin
61
+ // ✅ Collect one-time events in LaunchedEffect
62
+ @Composable
63
+ fun UserScreen(
64
+ onNavigateBack: () -> Unit,
65
+ viewModel: UserViewModel = hiltViewModel()
66
+ ) {
67
+ val context = LocalContext.current
68
+
69
+ LaunchedEffect(Unit) {
70
+ viewModel.events.collect { event ->
71
+ when (event) {
72
+ is UserEvent.NavigateBack -> onNavigateBack()
73
+ is UserEvent.ShowToast -> Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
74
+ }
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ ---
81
+
82
+ ## Replay and Buffer Configuration
83
+
84
+ ```kotlin
85
+ // ✅ replay = 0 — no replay, new collectors miss past events (correct for one-time events)
86
+ val events = MutableSharedFlow<Event>(replay = 0)
87
+
88
+ // ✅ replay = 1 — new collectors get the last event (use for "current status" broadcasts)
89
+ val connectionStatus = MutableSharedFlow<ConnectionStatus>(replay = 1)
90
+
91
+ // ✅ extraBufferCapacity — don't drop events when collectors are slow
92
+ val events = MutableSharedFlow<Event>(
93
+ replay = 0,
94
+ extraBufferCapacity = 64,
95
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
96
+ )
97
+ ```
98
+
99
+ ---
100
+
101
+ ## Shared Event Bus
102
+
103
+ ```kotlin
104
+ // ✅ App-wide event bus for cross-feature communication
105
+ @Singleton
106
+ class AppEventBus @Inject constructor() {
107
+
108
+ private val _events = MutableSharedFlow<AppEvent>(
109
+ extraBufferCapacity = 32,
110
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
111
+ )
112
+ val events: SharedFlow<AppEvent> = _events.asSharedFlow()
113
+
114
+ suspend fun emit(event: AppEvent) {
115
+ _events.emit(event)
116
+ }
117
+ }
118
+
119
+ sealed interface AppEvent {
120
+ data object UserLoggedOut : AppEvent
121
+ data class PushNotificationReceived(val data: Map<String, String>) : AppEvent
122
+ }
123
+ ```
124
+
125
+ ---
126
+
127
+ ## SharedFlow vs Channel vs StateFlow
128
+
129
+ | | SharedFlow | Channel | StateFlow |
130
+ | ------------------- | ---------------- | -------------------------- | ---------- |
131
+ | Hot/Cold | Hot | Hot | Hot |
132
+ | Initial value | No | No | Yes |
133
+ | Replay | Configurable | No | Last value |
134
+ | Multiple collectors | Yes | No (one consumer) | Yes |
135
+ | Delivery guarantee | No (can miss) | Yes (buffered) | N/A |
136
+ | Use for | Broadcast events | Guaranteed single delivery | UI state |
137
+
138
+ ---
139
+
140
+ ## tryEmit vs emit
141
+
142
+ ```kotlin
143
+ // ✅ emit — suspends if buffer is full (use in coroutines)
144
+ viewModelScope.launch {
145
+ _events.emit(UserEvent.NavigateBack)
146
+ }
147
+
148
+ // ✅ tryEmit — returns false if buffer full (use outside coroutines)
149
+ val sent = _events.tryEmit(UserEvent.NavigateBack)
150
+ if (!sent) Timber.w("Event dropped — buffer full")
151
+ ```
152
+
153
+ ---
154
+
155
+ ## Anti-Patterns
156
+
157
+ - Using `SharedFlow` with `replay = 1` for navigation events — navigates again on resubscription
158
+ - Using `StateFlow` for one-time events — replays the event on recomposition
159
+ - Not setting `extraBufferCapacity` for high-frequency events — events dropped silently
160
+ - Collecting `SharedFlow` without lifecycle awareness in Android UI
161
+ - Using an app-wide event bus for everything — creates hidden coupling between features
162
+
163
+ ---
164
+
165
+ ## Related Skills
166
+
167
+ - `stateflow` — hot state holder with current value
168
+ - `channel` — guaranteed single-consumer delivery
169
+ - `flow` — cold stream fundamentals
170
+ - `coroutine` — coroutine scopes for emitting events
171
+ - `mvvm` — event pattern in ViewModel
@@ -0,0 +1,175 @@
1
+ ---
2
+ name: stateflow
3
+ description: >
4
+ StateFlow for UI state management in Android with Kotlin coroutines.
5
+ Load this skill when holding and exposing UI state from a ViewModel,
6
+ converting cold flows to StateFlow, managing state updates safely,
7
+ or replacing LiveData with StateFlow.
8
+ ---
9
+
10
+ # StateFlow
11
+
12
+ ## Overview
13
+ `StateFlow` is a hot Flow that always holds a current value and replays it to new collectors. It is the standard replacement for `LiveData` in Kotlin-first Android development. ViewModels expose `StateFlow` for UI state, and Compose collects it with `collectAsStateWithLifecycle`.
14
+
15
+ ---
16
+
17
+ ## Core Principles
18
+
19
+ - Every ViewModel exposes **one** `StateFlow<UiState>` per screen — not multiple separate flows
20
+ - `MutableStateFlow` is private — expose as `StateFlow` to prevent external mutation
21
+ - Update state atomically with `update {}` — never read-then-write separately
22
+ - Use `stateIn` to convert a cold `Flow` from the repository into a `StateFlow`
23
+ - `StateFlow` replays the current value — do not use it for one-time events
24
+
25
+ ---
26
+
27
+ ## Basic StateFlow
28
+
29
+ ```kotlin
30
+ // ✅ Private mutable, public immutable
31
+ class UserViewModel : ViewModel() {
32
+
33
+ private val _state = MutableStateFlow<UserUiState>(UserUiState.Loading)
34
+ val state: StateFlow<UserUiState> = _state.asStateFlow()
35
+
36
+ fun loadUser(id: String) {
37
+ viewModelScope.launch {
38
+ _state.value = UserUiState.Loading
39
+ repository.getUser(id).fold(
40
+ onSuccess = { _state.value = UserUiState.Success(it) },
41
+ onFailure = { _state.value = UserUiState.Error(it.message ?: "Error") }
42
+ )
43
+ }
44
+ }
45
+ }
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Atomic State Updates
51
+
52
+ ```kotlin
53
+ // ✅ update {} — thread-safe atomic update
54
+ private val _state = MutableStateFlow(UserListState())
55
+
56
+ fun onSearchQueryChanged(query: String) {
57
+ _state.update { current ->
58
+ current.copy(searchQuery = query)
59
+ }
60
+ }
61
+
62
+ fun onUserDeleted(userId: String) {
63
+ _state.update { current ->
64
+ current.copy(users = current.users.filter { it.id != userId })
65
+ }
66
+ }
67
+
68
+ // ❌ Non-atomic — race condition possible
69
+ _state.value = _state.value.copy(searchQuery = query)
70
+ ```
71
+
72
+ ---
73
+
74
+ ## stateIn — Convert Repository Flow to StateFlow
75
+
76
+ ```kotlin
77
+ // ✅ Convert cold Flow from Room/repository to StateFlow
78
+ class UserListViewModel @Inject constructor(
79
+ private val userRepository: UserRepository
80
+ ) : ViewModel() {
81
+
82
+ val state: StateFlow<UserListUiState> = userRepository
83
+ .getUsersFlow()
84
+ .map { users -> UserListUiState(users = users) }
85
+ .catch { emit(UserListUiState(error = it.message)) }
86
+ .stateIn(
87
+ scope = viewModelScope,
88
+ started = SharingStarted.WhileSubscribed(5_000), // ✅ stop 5s after last collector
89
+ initialValue = UserListUiState(isLoading = true)
90
+ )
91
+ }
92
+ ```
93
+
94
+ ---
95
+
96
+ ## SharingStarted Options
97
+
98
+ ```kotlin
99
+ // ✅ WhileSubscribed(5_000) — recommended for UI state
100
+ // Stops upstream 5s after last collector unsubscribes
101
+ // Survives configuration changes (Activity recreated within 5s)
102
+ .stateIn(
103
+ scope = viewModelScope,
104
+ started = SharingStarted.WhileSubscribed(5_000),
105
+ initialValue = initialState
106
+ )
107
+
108
+ // ✅ Eagerly — starts immediately, never stops
109
+ // Use for state that must always be fresh
110
+ .stateIn(scope = viewModelScope, started = SharingStarted.Eagerly, initialValue = initialState)
111
+
112
+ // ✅ Lazily — starts on first collector, never stops
113
+ .stateIn(scope = viewModelScope, started = SharingStarted.Lazily, initialValue = initialState)
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Collecting in Compose
119
+
120
+ ```kotlin
121
+ // ✅ collectAsStateWithLifecycle — stops collection when lifecycle is below STARTED
122
+ @Composable
123
+ fun UserListScreen(viewModel: UserListViewModel = hiltViewModel()) {
124
+ val state by viewModel.state.collectAsStateWithLifecycle()
125
+
126
+ when (state) {
127
+ is UserListUiState.Loading -> LoadingIndicator()
128
+ is UserListUiState.Success -> UserList(state.users)
129
+ is UserListUiState.Error -> ErrorMessage(state.message)
130
+ }
131
+ }
132
+
133
+ // ❌ collectAsState — doesn't respect lifecycle
134
+ val state by viewModel.state.collectAsState()
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Derived State
140
+
141
+ ```kotlin
142
+ // ✅ Derive secondary StateFlow from primary
143
+ class UserListViewModel : ViewModel() {
144
+ private val _state = MutableStateFlow(UserListState())
145
+
146
+ val filteredUsers: StateFlow<List<User>> = _state
147
+ .map { state ->
148
+ state.users.filter { it.name.contains(state.searchQuery, ignoreCase = true) }
149
+ }
150
+ .stateIn(
151
+ scope = viewModelScope,
152
+ started = SharingStarted.WhileSubscribed(5_000),
153
+ initialValue = emptyList()
154
+ )
155
+ }
156
+ ```
157
+
158
+ ---
159
+
160
+ ## Anti-Patterns
161
+
162
+ - Using `StateFlow` for one-time events (navigation, toasts) — replays on resubscription
163
+ - Exposing `MutableStateFlow` publicly — external code can mutate state
164
+ - Non-atomic state updates with `_state.value = _state.value.copy(...)` — race conditions
165
+ - Using `SharingStarted.Eagerly` for all flows — keeps upstream alive unnecessarily
166
+ - Multiple `StateFlow` properties for one screen — use one state data class
167
+
168
+ ---
169
+
170
+ ## Related Skills
171
+ - `sharedflow` — for one-time events that must not replay
172
+ - `flow` — cold stream fundamentals and operators
173
+ - `mvvm` — ViewModel pattern using StateFlow
174
+ - `mvi` — reducer pattern with StateFlow
175
+ - `lifecycle` — lifecycle-aware collection
@@ -0,0 +1,197 @@
1
+ ---
2
+ name: structured-concurrency
3
+ description: >
4
+ Structured concurrency principles and patterns in Kotlin coroutines.
5
+ Load this skill when managing parent-child coroutine relationships,
6
+ understanding cancellation propagation, using coroutineScope vs
7
+ supervisorScope, or designing components with proper coroutine lifecycle.
8
+ ---
9
+
10
+ # Structured Concurrency
11
+
12
+ ## Overview
13
+
14
+ Structured concurrency ensures that coroutines have a defined lifetime tied to a scope. Every coroutine has a parent, and when the parent is cancelled, all children are cancelled. This prevents coroutine leaks and makes async code predictable and safe.
15
+
16
+ ---
17
+
18
+ ## Core Principles
19
+
20
+ - Every coroutine has a **parent scope** — no orphan coroutines
21
+ - Cancellation flows **downward** — parent cancelled → all children cancelled
22
+ - Failure flows **upward** — child exception cancels the parent (unless `SupervisorJob`)
23
+ - A scope does not complete until **all children complete**
24
+ - Use `coroutineScope` for all-or-nothing operations, `supervisorScope` for independent children
25
+
26
+ ---
27
+
28
+ ## Parent-Child Relationship
29
+
30
+ ```
31
+ CoroutineScope (viewModelScope)
32
+ └── Job (root)
33
+ ├── Coroutine A (launch)
34
+ │ └── Coroutine A1 (launch inside A)
35
+ └── Coroutine B (launch)
36
+
37
+ Cancel viewModelScope → A, A1, B all cancelled
38
+ Exception in B → cancels scope → A, A1 also cancelled (unless SupervisorJob)
39
+ ```
40
+
41
+ ---
42
+
43
+ ## coroutineScope vs supervisorScope
44
+
45
+ ```kotlin
46
+ // ✅ coroutineScope — all children must succeed, any failure cancels all
47
+ suspend fun loadRequiredData(): DashboardData = coroutineScope {
48
+ val user = async { userRepository.getUser() } // required
49
+ val config = async { configRepository.get() } // required
50
+
51
+ DashboardData(
52
+ user = user.await(), // if either throws, both cancelled
53
+ config = config.await()
54
+ )
55
+ }
56
+
57
+ // ✅ supervisorScope — children are independent, one failure doesn't cancel others
58
+ suspend fun loadOptionalData(): DashboardExtras = supervisorScope {
59
+ val recommendations = async { recommendationRepository.get() } // optional
60
+ val ads = async { adsRepository.get() } // optional
61
+
62
+ DashboardExtras(
63
+ recommendations = runCatching { recommendations.await() }.getOrDefault(emptyList()),
64
+ ads = runCatching { ads.await() }.getOrDefault(emptyList())
65
+ )
66
+ }
67
+ ```
68
+
69
+ ---
70
+
71
+ ## SupervisorJob in Custom Scopes
72
+
73
+ ```kotlin
74
+ // ✅ SupervisorJob — child failures don't cancel the scope
75
+ @Singleton
76
+ class BackgroundSyncManager @Inject constructor() {
77
+
78
+ // SupervisorJob: one sync failure doesn't cancel all other syncs
79
+ private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
80
+
81
+ fun syncUsers() {
82
+ scope.launch {
83
+ userRepository.sync() // failure here won't cancel syncOrders
84
+ }
85
+ }
86
+
87
+ fun syncOrders() {
88
+ scope.launch {
89
+ orderRepository.sync()
90
+ }
91
+ }
92
+
93
+ fun cancel() {
94
+ scope.cancel()
95
+ }
96
+ }
97
+
98
+ // ✅ viewModelScope already uses SupervisorJob internally
99
+ // Each launch{} in ViewModel is independent
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Cancellation Propagation
105
+
106
+ ```kotlin
107
+ // ✅ CancellationException must NOT be caught and swallowed
108
+ suspend fun doWork() {
109
+ try {
110
+ delay(1_000)
111
+ fetchData()
112
+ } catch (e: CancellationException) {
113
+ throw e // ✅ always rethrow CancellationException
114
+ } catch (e: Exception) {
115
+ handleError(e)
116
+ }
117
+ }
118
+
119
+ // ✅ runCatching re-throws CancellationException automatically
120
+ val result = runCatching { fetchData() } // safe — cancellation propagates correctly
121
+
122
+ // ❌ Swallowing CancellationException — breaks structured concurrency
123
+ try {
124
+ delay(1_000)
125
+ } catch (e: Exception) {
126
+ // catches CancellationException too — coroutine won't cancel properly
127
+ }
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Scope Completion
133
+
134
+ ```kotlin
135
+ // ✅ coroutineScope suspends until all children complete
136
+ suspend fun processAll(items: List<Item>) = coroutineScope {
137
+ items.forEach { item ->
138
+ launch { processItem(item) }
139
+ }
140
+ // resumes here only when all launches have completed
141
+ }
142
+
143
+ // ✅ joinAll — wait for specific jobs
144
+ suspend fun waitForAll() {
145
+ val jobs = listOf(
146
+ viewModelScope.launch { task1() },
147
+ viewModelScope.launch { task2() }
148
+ )
149
+ jobs.joinAll() // suspends until both complete
150
+ }
151
+ ```
152
+
153
+ ---
154
+
155
+ ## Scope in Non-Lifecycle Components
156
+
157
+ ```kotlin
158
+ // ✅ Inject and cancel scope explicitly in non-ViewModel classes
159
+ class DataSyncService @Inject constructor(
160
+ private val repository: DataRepository
161
+ ) {
162
+ private val job = SupervisorJob()
163
+ private val scope = CoroutineScope(job + Dispatchers.IO)
164
+
165
+ fun start() {
166
+ scope.launch {
167
+ while (isActive) {
168
+ repository.sync()
169
+ delay(60_000)
170
+ }
171
+ }
172
+ }
173
+
174
+ fun stop() {
175
+ job.cancel() // cancels all children, scope is still reusable with new children
176
+ }
177
+ }
178
+ ```
179
+
180
+ ---
181
+
182
+ ## Anti-Patterns
183
+
184
+ - `GlobalScope.launch` — no parent, lives forever, can't be cancelled
185
+ - Catching `CancellationException` without rethrowing — breaks cancellation
186
+ - Creating a `CoroutineScope` without `SupervisorJob` in a manager class — one failure kills all tasks
187
+ - Not calling `scope.cancel()` in `onCleared()` or equivalent — coroutine leak
188
+ - Using `launch` inside `suspend fun` without a scope — use `coroutineScope {}` instead
189
+
190
+ ---
191
+
192
+ ## Related Skills
193
+
194
+ - `coroutine` — coroutine fundamentals and dispatcher selection
195
+ - `flow` — structured cancellation with flows
196
+ - `lifecycle` — viewModelScope and lifecycleScope internals
197
+ - `background-processing` — long-running work with structured scopes