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,278 @@
1
+ ---
2
+ name: side-effect-management
3
+ description: >
4
+ Side effect handling patterns in Android/Kotlin across all layers.
5
+ Load this skill when dealing with one-time events, navigation triggers,
6
+ analytics calls, toast/snackbar messages, or any operation that causes
7
+ observable change outside the current scope.
8
+ ---
9
+
10
+ # Side Effect Management
11
+
12
+ ## Overview
13
+
14
+ A side effect is any operation that changes state or causes observable behavior outside the current function's scope — navigation, showing a toast, logging, analytics, network calls triggered by UI events. Managing side effects correctly prevents duplication, memory leaks, and lifecycle bugs.
15
+
16
+ ---
17
+
18
+ ## Core Principles
19
+
20
+ - Side effects must be **lifecycle-aware** — never fire after the UI is gone
21
+ - **One-time events** (navigation, snackbar) must never be modeled as persistent state
22
+ - Side effects in ViewModel are communicated to UI via **SharedFlow events**
23
+ - Side effects in Compose use `LaunchedEffect`, `SideEffect`, or `DisposableEffect`
24
+ - Never trigger side effects inside `map {}`, `combine {}`, or composable body directly
25
+
26
+ ---
27
+
28
+ ## Layer Responsibility
29
+
30
+ | Layer | Side Effects | How |
31
+ | ------------ | ------------------------------------------- | -------------------------------------- |
32
+ | UI (Compose) | Navigation, show dialog, request permission | `LaunchedEffect`, collect `SharedFlow` |
33
+ | ViewModel | Emit one-time events to UI | `MutableSharedFlow` |
34
+ | UseCase | None — pure logic only | — |
35
+ | Repository | Logging, cache invalidation | Contained within repository |
36
+ | Data Source | Network call, DB write | Result returned, not side-effected |
37
+
38
+ ---
39
+
40
+ ## ViewModel — One-Time Events via SharedFlow
41
+
42
+ ```kotlin
43
+ // ✅ Sealed class for typed events
44
+ sealed class UserDetailEvent {
45
+ data class NavigateToEdit(val userId: String) : UserDetailEvent()
46
+ data class ShowSnackbar(val message: String) : UserDetailEvent()
47
+ object NavigateBack : UserDetailEvent()
48
+ object ShowDeleteConfirmation : UserDetailEvent()
49
+ }
50
+
51
+ @HiltViewModel
52
+ class UserDetailViewModel @Inject constructor(
53
+ private val repository: UserRepository
54
+ ) : ViewModel() {
55
+
56
+ private val _state = MutableStateFlow(UserDetailUiState())
57
+ val state: StateFlow<UserDetailUiState> = _state.asStateFlow()
58
+
59
+ // ✅ SharedFlow for events — replay = 0, never persisted
60
+ private val _events = MutableSharedFlow<UserDetailEvent>()
61
+ val events: SharedFlow<UserDetailEvent> = _events.asSharedFlow()
62
+
63
+ fun onEditClicked() {
64
+ viewModelScope.launch {
65
+ _events.emit(UserDetailEvent.NavigateToEdit(state.value.userId))
66
+ }
67
+ }
68
+
69
+ fun onDeleteClicked() {
70
+ viewModelScope.launch {
71
+ _events.emit(UserDetailEvent.ShowDeleteConfirmation)
72
+ }
73
+ }
74
+
75
+ fun onDeleteConfirmed() {
76
+ viewModelScope.launch {
77
+ repository.deleteUser(state.value.userId)
78
+ .onSuccess {
79
+ _events.emit(UserDetailEvent.NavigateBack)
80
+ }
81
+ .onFailure { error ->
82
+ _events.emit(UserDetailEvent.ShowSnackbar(error.message ?: "Error"))
83
+ }
84
+ }
85
+ }
86
+ }
87
+ ```
88
+
89
+ ---
90
+
91
+ ## UI — Collecting Events
92
+
93
+ ```kotlin
94
+ @Composable
95
+ fun UserDetailScreen(
96
+ navController: NavController,
97
+ viewModel: UserDetailViewModel = hiltViewModel()
98
+ ) {
99
+ val state by viewModel.state.collectAsStateWithLifecycle()
100
+ val snackbarHostState = remember { SnackbarHostState() }
101
+ val lifecycleOwner = LocalLifecycleOwner.current
102
+
103
+ // ✅ Collect one-time events with lifecycle awareness
104
+ LaunchedEffect(Unit) {
105
+ viewModel.events
106
+ .flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
107
+ .collect { event ->
108
+ when (event) {
109
+ is UserDetailEvent.NavigateToEdit ->
110
+ navController.navigate(EditUserRoute(event.userId))
111
+ is UserDetailEvent.ShowSnackbar ->
112
+ snackbarHostState.showSnackbar(event.message)
113
+ is UserDetailEvent.NavigateBack ->
114
+ navController.navigateUp()
115
+ is UserDetailEvent.ShowDeleteConfirmation ->
116
+ // show dialog via local state
117
+ showDeleteDialog = true
118
+ }
119
+ }
120
+ }
121
+
122
+ Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) { padding ->
123
+ UserDetailContent(
124
+ state = state,
125
+ onEditClick = viewModel::onEditClicked,
126
+ onDeleteClick = viewModel::onDeleteClicked,
127
+ modifier = Modifier.padding(padding)
128
+ )
129
+ }
130
+ }
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Compose Side Effects
136
+
137
+ ```kotlin
138
+ // ✅ LaunchedEffect — coroutine side effect, runs when key changes
139
+ @Composable
140
+ fun AutoScrollList(scrollToIndex: Int) {
141
+ val listState = rememberLazyListState()
142
+
143
+ LaunchedEffect(scrollToIndex) {
144
+ listState.animateScrollToItem(scrollToIndex)
145
+ }
146
+ }
147
+
148
+ // ✅ SideEffect — sync Compose state to external system on every recomposition
149
+ @Composable
150
+ fun AnalyticsTracker(screenName: String) {
151
+ SideEffect {
152
+ analytics.setCurrentScreen(screenName)
153
+ }
154
+ }
155
+
156
+ // ✅ DisposableEffect — register/unregister listener
157
+ @Composable
158
+ fun NetworkMonitor(onNetworkChange: (Boolean) -> Unit) {
159
+ val context = LocalContext.current
160
+ DisposableEffect(Unit) {
161
+ val callback = object : ConnectivityManager.NetworkCallback() {
162
+ override fun onAvailable(network: Network) = onNetworkChange(true)
163
+ override fun onLost(network: Network) = onNetworkChange(false)
164
+ }
165
+ val cm = context.getSystemService(ConnectivityManager::class.java)
166
+ cm.registerDefaultNetworkCallback(callback)
167
+ onDispose { cm.unregisterNetworkCallback(callback) }
168
+ }
169
+ }
170
+
171
+ // ✅ rememberCoroutineScope — imperative trigger from event handler
172
+ @Composable
173
+ fun SaveButton(viewModel: FormViewModel) {
174
+ val scope = rememberCoroutineScope()
175
+ val snackbarHostState = remember { SnackbarHostState() }
176
+
177
+ Button(onClick = {
178
+ scope.launch {
179
+ snackbarHostState.showSnackbar("Saved!")
180
+ }
181
+ }) {
182
+ Text("Save")
183
+ }
184
+ }
185
+ ```
186
+
187
+ ---
188
+
189
+ ## Analytics as Side Effect
190
+
191
+ ```kotlin
192
+ // ✅ Analytics in ViewModel — after state change, before emitting event
193
+ fun onPurchaseCompleted(orderId: String) {
194
+ viewModelScope.launch {
195
+ val result = repository.completeOrder(orderId)
196
+ result.onSuccess {
197
+ analytics.track("purchase_completed", mapOf("order_id" to orderId))
198
+ _events.emit(OrderEvent.NavigateToConfirmation(orderId))
199
+ }
200
+ }
201
+ }
202
+
203
+ // ❌ Analytics inside UseCase — use cases must be pure
204
+ class CompleteOrderUseCase {
205
+ operator fun invoke(orderId: String) {
206
+ // ❌ Don't call analytics here — side effect in wrong layer
207
+ analytics.track("purchase_completed")
208
+ }
209
+ }
210
+ ```
211
+
212
+ ---
213
+
214
+ ## Side Effects in Flow Pipelines
215
+
216
+ ```kotlin
217
+ // ✅ onEach — non-transforming side effect in flow
218
+ fun observeOrders(): Flow<List<Order>> =
219
+ repository.observeOrders()
220
+ .onEach { orders ->
221
+ if (orders.any { it.isUrgent }) {
222
+ notificationManager.showUrgentOrderAlert()
223
+ }
224
+ }
225
+
226
+ // ✅ onStart / onCompletion — lifecycle side effects
227
+ fun fetchUsers(): Flow<List<User>> =
228
+ repository.fetchUsers()
229
+ .onStart { analytics.track("users_fetch_started") }
230
+ .onCompletion { error ->
231
+ if (error == null) analytics.track("users_fetch_success")
232
+ }
233
+ .catch { error ->
234
+ logger.error("Failed to fetch users", error)
235
+ emit(emptyList())
236
+ }
237
+ ```
238
+
239
+ ---
240
+
241
+ ## Preventing Duplicate Events
242
+
243
+ ```kotlin
244
+ // ✅ SharedFlow with replay=0 — events are NOT replayed on new collectors
245
+ private val _events = MutableSharedFlow<Event>(
246
+ replay = 0,
247
+ extraBufferCapacity = 1,
248
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
249
+ )
250
+
251
+ // ✅ Channel for guaranteed delivery (but only one collector)
252
+ private val _events = Channel<Event>(Channel.BUFFERED)
253
+ val events = _events.receiveAsFlow()
254
+
255
+ // ❌ StateFlow for events — replays last event on rotation
256
+ private val _navigateTo = MutableStateFlow<Route?>(null) // wrong — replays on rotation
257
+ ```
258
+
259
+ ---
260
+
261
+ ## Anti-Patterns
262
+
263
+ - Modeling navigation/snackbar as state (`UiState.navigateTo`) — replays on rotation
264
+ - Calling `analytics.track()` directly inside a composable body
265
+ - Triggering side effects inside `map {}` / `combine {}` operators
266
+ - Using `GlobalScope` for event emission — not lifecycle-aware
267
+ - Forgetting `flowWithLifecycle` when collecting events — fires in background
268
+ - Using `replay = 1` on event SharedFlow — event fires again on re-subscription
269
+
270
+ ---
271
+
272
+ ## Related Skills
273
+
274
+ - `reactive-state-management` — state vs events distinction
275
+ - `compose` — LaunchedEffect, SideEffect, DisposableEffect
276
+ - `coroutine` — scope and dispatcher for side effects
277
+ - `mvvm` — ViewModel event emission patterns
278
+ - `use-case-design` — keeping use cases free of side effects
@@ -0,0 +1,229 @@
1
+ ---
2
+ name: state-management
3
+ description: >
4
+ State management patterns for Android with Compose and ViewModel.
5
+ Load this skill when designing UI state models, choosing between
6
+ StateFlow vs SharedFlow vs Channel, managing loading/error/success states,
7
+ handling state persistence, or combining multiple state sources.
8
+ ---
9
+
10
+ # State Management
11
+
12
+ ## Overview
13
+
14
+ State management defines where state lives, how it flows to the UI, and how it changes. In Android with Compose, state lives in the ViewModel as `StateFlow`, flows to composables via `collectAsStateWithLifecycle()`, and changes only through explicit events or function calls. One-time events (navigation, toasts) use `Channel` — not `StateFlow`.
15
+
16
+ ---
17
+
18
+ ## Core Principles
19
+
20
+ - **Single source of truth** — one `StateFlow<UiState>` per screen
21
+ - **Immutable state** — always produce a new state via `copy()`, never mutate
22
+ - **StateFlow for persistent state** — last value replayed to new collectors
23
+ - **Channel for one-time events** — not replayed, not missed when buffered
24
+ - **`collectAsStateWithLifecycle()`** — lifecycle-aware collection in Compose
25
+
26
+ ---
27
+
28
+ ## State Shape Patterns
29
+
30
+ ```kotlin
31
+ // ✅ Pattern 1: Sealed class — for screens with distinct modes
32
+ sealed interface ProductDetailUiState {
33
+ data object Loading : ProductDetailUiState
34
+ data class Success(val product: Product, val isFavorite: Boolean) : ProductDetailUiState
35
+ data class Error(val message: String) : ProductDetailUiState
36
+ }
37
+
38
+ // ✅ Pattern 2: Data class — for screens with parallel state fields
39
+ data class ProductListUiState(
40
+ val isLoading: Boolean = false,
41
+ val products: List<Product> = emptyList(),
42
+ val error: String? = null,
43
+ val searchQuery: String = "",
44
+ val selectedFilter: Filter = Filter.All,
45
+ val totalCount: Int = 0
46
+ )
47
+
48
+ // When to use which:
49
+ // Sealed class → screen has mutually exclusive modes (loading OR content OR error)
50
+ // Data class → screen has multiple independent fields shown simultaneously
51
+ ```
52
+
53
+ ---
54
+
55
+ ## StateFlow Patterns
56
+
57
+ ```kotlin
58
+ // ✅ Simple StateFlow
59
+ private val _state = MutableStateFlow(ProductListUiState())
60
+ val state: StateFlow<ProductListUiState> = _state.asStateFlow()
61
+
62
+ // Update with copy()
63
+ _state.update { it.copy(isLoading = true) }
64
+
65
+ // ✅ StateFlow from Flow (derived state)
66
+ val state: StateFlow<ProductListUiState> =
67
+ repository.observeProducts()
68
+ .map { products -> ProductListUiState(products = products) }
69
+ .catch { e -> emit(ProductListUiState(error = e.message)) }
70
+ .stateIn(
71
+ scope = viewModelScope,
72
+ started = SharingStarted.WhileSubscribed(5_000), // cancel 5s after UI gone
73
+ initialValue = ProductListUiState(isLoading = true)
74
+ )
75
+
76
+ // ✅ Combining multiple sources
77
+ val state: StateFlow<DashboardUiState> = combine(
78
+ userRepository.observeCurrentUser(),
79
+ orderRepository.observeRecentOrders(),
80
+ notificationRepository.observeUnreadCount()
81
+ ) { user, orders, unreadCount ->
82
+ DashboardUiState(
83
+ userName = user.name,
84
+ recentOrders = orders,
85
+ unreadNotifications = unreadCount
86
+ )
87
+ }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), DashboardUiState())
88
+ ```
89
+
90
+ ---
91
+
92
+ ## One-Time Events: Channel
93
+
94
+ ```kotlin
95
+ // ✅ Use Channel for events that happen exactly once
96
+ sealed interface ProductEvent {
97
+ data object NavigateToCart : ProductEvent
98
+ data class ShowSnackbar(val message: String) : ProductEvent
99
+ data class NavigateToDetail(val productId: String) : ProductEvent
100
+ }
101
+
102
+ private val _events = Channel<ProductEvent>(Channel.BUFFERED)
103
+ val events: Flow<ProductEvent> = _events.receiveAsFlow()
104
+
105
+ fun onAddToCart(product: Product) {
106
+ viewModelScope.launch {
107
+ addToCartUseCase(product).fold(
108
+ onSuccess = { _events.send(ProductEvent.ShowSnackbar("Added to cart")) },
109
+ onFailure = { _events.send(ProductEvent.ShowSnackbar("Failed to add")) }
110
+ )
111
+ }
112
+ }
113
+
114
+ // ✅ Collect events in Compose
115
+ LaunchedEffect(Unit) {
116
+ viewModel.events.collect { event ->
117
+ when (event) {
118
+ is ProductEvent.NavigateToCart -> navController.navigate(CartRoute)
119
+ is ProductEvent.ShowSnackbar -> snackbarHost.showSnackbar(event.message)
120
+ is ProductEvent.NavigateToDetail -> navController.navigate(DetailRoute(event.productId))
121
+ }
122
+ }
123
+ }
124
+ ```
125
+
126
+ ---
127
+
128
+ ## StateFlow vs SharedFlow vs Channel
129
+
130
+ | | StateFlow | SharedFlow | Channel |
131
+ | ------------------- | ---------- | ------------ | ------------------ |
132
+ | Replays last value | ✅ Always | Configurable | ❌ Never |
133
+ | Has initial value | ✅ Required | ❌ No | ❌ No |
134
+ | Multiple collectors | ✅ | ✅ | ❌ Single consumer |
135
+ | Use for | UI state | Event bus | One-time UI events |
136
+
137
+ ```kotlin
138
+ // ✅ StateFlow — UI state (always has a value, replays to new subscribers)
139
+ val uiState: StateFlow<MyState>
140
+
141
+ // ✅ Channel — one-time events (navigation, toast) — won't miss events when buffered
142
+ val events: Flow<MyEvent> = _channel.receiveAsFlow()
143
+
144
+ // ✅ SharedFlow — multi-cast events to multiple collectors (rare in UI layer)
145
+ val sharedEvents = MutableSharedFlow<Event>(extraBufferCapacity = 1)
146
+ ```
147
+
148
+ ---
149
+
150
+ ## Loading / Error / Success
151
+
152
+ ```kotlin
153
+ // ✅ Async operation pattern
154
+ fun loadProduct(id: String) {
155
+ viewModelScope.launch {
156
+ _state.update { it.copy(isLoading = true, error = null) }
157
+ getProductUseCase(id).fold(
158
+ onSuccess = { product ->
159
+ _state.update { it.copy(isLoading = false, product = product) }
160
+ },
161
+ onFailure = { error ->
162
+ _state.update { it.copy(isLoading = false, error = error.message ?: "Unknown error") }
163
+ }
164
+ )
165
+ }
166
+ }
167
+
168
+ // ✅ Collecting in Compose
169
+ @Composable
170
+ fun ProductScreen(viewModel: ProductViewModel = hiltViewModel()) {
171
+ val state by viewModel.state.collectAsStateWithLifecycle()
172
+
173
+ when {
174
+ state.isLoading -> LoadingIndicator()
175
+ state.error != null -> ErrorView(state.error, onRetry = viewModel::retry)
176
+ state.product != null -> ProductDetail(state.product)
177
+ }
178
+ }
179
+ ```
180
+
181
+ ---
182
+
183
+ ## Persisted State
184
+
185
+ ```kotlin
186
+ // ✅ Persist UI state across process death via SavedStateHandle
187
+ @HiltViewModel
188
+ class SearchViewModel @Inject constructor(
189
+ savedStateHandle: SavedStateHandle,
190
+ private val searchUseCase: SearchUseCase
191
+ ) : ViewModel() {
192
+
193
+ // ✅ Query persists across process death
194
+ var searchQuery by savedStateHandle.saveable { mutableStateOf("") }
195
+ private set
196
+
197
+ // ✅ StateFlow backed by SavedStateHandle
198
+ private val _query = savedStateHandle.getStateFlow("query", "")
199
+ val results: StateFlow<List<Result>> = _query
200
+ .debounce(300)
201
+ .flatMapLatest { searchUseCase(it) }
202
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
203
+
204
+ fun onQueryChanged(query: String) {
205
+ savedStateHandle["query"] = query
206
+ }
207
+ }
208
+ ```
209
+
210
+ ---
211
+
212
+ ## Anti-Patterns
213
+
214
+ - Exposing `MutableStateFlow` publicly — always expose as `StateFlow`
215
+ - Using `StateFlow` for navigation events — they replay on resubscription (double navigation)
216
+ - Multiple `StateFlow` for one screen — combine into one state class
217
+ - Mutating state directly (`_state.value.list.add(...)`) — use `copy()` and `update()`
218
+ - `collectAsState()` instead of `collectAsStateWithLifecycle()` — leaks collection in background
219
+
220
+ ---
221
+
222
+ ## Related Skills
223
+
224
+ - `mvvm` — ViewModel pattern using state management
225
+ - `mvi` — MVI pattern with strict state management
226
+ - `unidirectional-data-flow` — the underlying principle
227
+ - `reactive-state-management` — Flow operators for state derivation
228
+ - `savedstatehandle` — persisting state across process death
229
+ - `side-effect-management` — managing effects alongside state
@@ -0,0 +1,196 @@
1
+ ---
2
+ name: unidirectional-data-flow
3
+ description: >
4
+ Unidirectional Data Flow (UDF) pattern for Android.
5
+ Load this skill when designing how state flows from ViewModel to UI,
6
+ how events flow from UI to ViewModel, understanding the UDF cycle,
7
+ or enforcing a single source of truth for UI state.
8
+ ---
9
+
10
+ # Unidirectional Data Flow (UDF)
11
+
12
+ ## Overview
13
+
14
+ UDF is the underlying principle behind both MVVM and MVI. State flows **down** from ViewModel to UI; events flow **up** from UI to ViewModel. The UI never mutates state directly — it only emits events. This creates a predictable, traceable, and testable state cycle.
15
+
16
+ ---
17
+
18
+ ## The UDF Cycle
19
+
20
+ ```
21
+ ┌──────────────────────────────┐
22
+ │ ViewModel │
23
+ │ │
24
+ │ event → logic → new state │
25
+ └──────┬───────────────────────┘
26
+ │ State (StateFlow)
27
+
28
+ ┌──────────────────────────────┐
29
+ │ UI │
30
+ │ │
31
+ │ render(state) │
32
+ │ user interaction → event ──►│
33
+ └──────────────────────────────┘
34
+ ```
35
+
36
+ 1. **State flows down** — ViewModel emits `StateFlow`, UI observes and renders
37
+ 2. **Events flow up** — UI emits events (clicks, input), ViewModel processes them
38
+ 3. **ViewModel owns state** — UI never mutates state; it requests changes via events
39
+ 4. **Single source of truth** — one `StateFlow` per screen, not scattered variables
40
+
41
+ ---
42
+
43
+ ## Core Principles
44
+
45
+ - State is **immutable** — always produce a new state, never mutate in place
46
+ - UI is a **pure function of state** — same state always produces same UI
47
+ - Events are **the only way** to trigger state changes
48
+ - ViewModel is the **single source of truth** for UI state
49
+ - No two-way data binding that hides event flow
50
+
51
+ ---
52
+
53
+ ## Implementation
54
+
55
+ ```kotlin
56
+ // ✅ State flows down via StateFlow
57
+ data class SearchUiState(
58
+ val query: String = "",
59
+ val results: List<Product> = emptyList(),
60
+ val isLoading: Boolean = false,
61
+ val error: String? = null
62
+ )
63
+
64
+ // ✅ Events flow up — all triggers modeled explicitly
65
+ sealed interface SearchEvent {
66
+ data class QueryChanged(val query: String) : SearchEvent
67
+ data object SearchSubmitted : SearchEvent
68
+ data object ClearSearch : SearchEvent
69
+ data class FilterSelected(val filter: Filter) : SearchEvent
70
+ }
71
+
72
+ @HiltViewModel
73
+ class SearchViewModel @Inject constructor(
74
+ private val searchProductsUseCase: SearchProductsUseCase
75
+ ) : ViewModel() {
76
+
77
+ private val _state = MutableStateFlow(SearchUiState())
78
+ val state: StateFlow<SearchUiState> = _state.asStateFlow()
79
+
80
+ fun onEvent(event: SearchEvent) {
81
+ when (event) {
82
+ is SearchEvent.QueryChanged -> _state.update { it.copy(query = event.query) }
83
+ is SearchEvent.SearchSubmitted -> search(_state.value.query)
84
+ is SearchEvent.ClearSearch -> _state.update { SearchUiState() }
85
+ is SearchEvent.FilterSelected -> applyFilter(event.filter)
86
+ }
87
+ }
88
+
89
+ private fun search(query: String) {
90
+ viewModelScope.launch {
91
+ _state.update { it.copy(isLoading = true, error = null) }
92
+ searchProductsUseCase(query).fold(
93
+ onSuccess = { results ->
94
+ _state.update { it.copy(isLoading = false, results = results) }
95
+ },
96
+ onFailure = { error ->
97
+ _state.update { it.copy(isLoading = false, error = error.message) }
98
+ }
99
+ )
100
+ }
101
+ }
102
+ }
103
+
104
+ // ✅ UI observes state and emits events — never modifies state
105
+ @Composable
106
+ fun SearchScreen(viewModel: SearchViewModel = hiltViewModel()) {
107
+ val state by viewModel.state.collectAsStateWithLifecycle()
108
+
109
+ SearchContent(
110
+ state = state,
111
+ onEvent = viewModel::onEvent
112
+ )
113
+ }
114
+
115
+ @Composable
116
+ private fun SearchContent(
117
+ state: SearchUiState,
118
+ onEvent: (SearchEvent) -> Unit
119
+ ) {
120
+ Column {
121
+ OutlinedTextField(
122
+ value = state.query,
123
+ onValueChange = { onEvent(SearchEvent.QueryChanged(it)) }, // ✅ event up
124
+ trailingIcon = {
125
+ IconButton(onClick = { onEvent(SearchEvent.SearchSubmitted) }) {
126
+ Icon(Icons.Default.Search, contentDescription = null)
127
+ }
128
+ }
129
+ )
130
+
131
+ when {
132
+ state.isLoading -> CircularProgressIndicator()
133
+ state.error != null -> ErrorMessage(state.error)
134
+ else -> ResultList(state.results)
135
+ }
136
+ }
137
+ }
138
+ ```
139
+
140
+ ---
141
+
142
+ ## State Updates
143
+
144
+ ```kotlin
145
+ // ✅ Immutable update via copy()
146
+ _state.update { currentState ->
147
+ currentState.copy(
148
+ isLoading = false,
149
+ results = newResults
150
+ )
151
+ }
152
+
153
+ // ✅ update{} is atomic — safe for concurrent updates
154
+ // update() uses compareAndSet internally — no race conditions
155
+
156
+ // ❌ Direct mutation — never do this
157
+ _state.value.results.add(item) // breaks UDF — state is mutated, not replaced
158
+ ```
159
+
160
+ ---
161
+
162
+ ## Derived State
163
+
164
+ ```kotlin
165
+ // ✅ Derive additional state from primary state
166
+ val filteredUsers: StateFlow<List<User>> = combine(
167
+ _users,
168
+ _searchQuery
169
+ ) { users, query ->
170
+ if (query.isBlank()) users
171
+ else users.filter { it.name.contains(query, ignoreCase = true) }
172
+ }.stateIn(
173
+ scope = viewModelScope,
174
+ started = SharingStarted.WhileSubscribed(5_000),
175
+ initialValue = emptyList()
176
+ )
177
+ ```
178
+
179
+ ---
180
+
181
+ ## Anti-Patterns
182
+
183
+ - UI mutating ViewModel state directly — UI only emits events
184
+ - Two-way data binding (`var` exposed directly) — breaks traceability
185
+ - Multiple independent StateFlows for one screen — hard to reason about combined state
186
+ - Calling ViewModel functions with side-effect names from UI ("submit", "navigate") — model as events instead
187
+ - Mutable collections inside state — use `List`, not `MutableList`
188
+
189
+ ---
190
+
191
+ ## Related Skills
192
+
193
+ - `mvvm` — MVVM applies UDF with ViewModel + StateFlow
194
+ - `mvi` — MVI is a strict formalization of UDF with Intent/State/Effect
195
+ - `state-management` — advanced state patterns
196
+ - `reactive-state-management` — Flow operators for deriving state