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.
- package/dist/index.js +143 -0
- package/package.json +27 -0
- package/skills/Android Ecosystem/Baseline Profile Generator/SKILL.md +277 -0
- package/skills/Android Ecosystem/Glance/SKILL.md +315 -0
- package/skills/Android Platform/Configuration/SKILL.md +201 -0
- package/skills/Android Platform/Filesystem/SKILL.md +216 -0
- package/skills/Android Platform/Lifecycle/SKILL.md +233 -0
- package/skills/Android Platform/Manifest/SKILL.md +226 -0
- package/skills/Android Platform/Process Death Recovery/SKILL.md +214 -0
- package/skills/Android Platform/Resources/SKILL.md +234 -0
- package/skills/Android Platform/SavedStateHandle/SKILL.md +217 -0
- package/skills/Android Platform/State Restoration/SKILL.md +210 -0
- package/skills/Architecture/Bounded Context/SKILL.md +207 -0
- package/skills/Architecture/Clean Architecture/SKILL.md +229 -0
- package/skills/Architecture/Domain Modeling/SKILL.md +236 -0
- package/skills/Architecture/Entity Design/SKILL.md +243 -0
- package/skills/Architecture/Feature Isolation/SKILL.md +216 -0
- package/skills/Architecture/MVI/SKILL.md +224 -0
- package/skills/Architecture/MVVM/SKILL.md +198 -0
- package/skills/Architecture/Modularization/SKILL.md +194 -0
- package/skills/Architecture/Offline First/SKILL.md +249 -0
- package/skills/Architecture/Repository Pattern/SKILL.md +216 -0
- package/skills/Architecture/Side Effect Management/SKILL.md +278 -0
- package/skills/Architecture/State Management/SKILL.md +229 -0
- package/skills/Architecture/Unidirectional Data Flow/SKILL.md +196 -0
- package/skills/Architecture/Use Case Design/SKILL.md +244 -0
- package/skills/Architecture/Value Object/SKILL.md +226 -0
- package/skills/Build Infrastructure/Build Orchestration/SKILL.md +257 -0
- package/skills/Build Infrastructure/Dependency Compatibility Resolver/SKILL.md +259 -0
- package/skills/Build Infrastructure/Environment Validator/SKILL.md +311 -0
- package/skills/Build System/Build Cache/SKILL.md +233 -0
- package/skills/Build System/Build Flavor Strategy/SKILL.md +171 -0
- package/skills/Build System/Build Variant/SKILL.md +215 -0
- package/skills/Build System/Convention Plugin/SKILL.md +288 -0
- package/skills/Build System/Dependency Management/SKILL.md +261 -0
- package/skills/Build System/Gradle/SKILL.md +284 -0
- package/skills/Build System/Incremental Build/SKILL.md +199 -0
- package/skills/Build System/KAPT/SKILL.md +198 -0
- package/skills/Build System/KSP/SKILL.md +263 -0
- package/skills/Build System/Module Dependency Graph Validation/SKILL.md +223 -0
- package/skills/Build System/Specialized/C++/SKILL.md +308 -0
- package/skills/Build System/Specialized/JNI/SKILL.md +306 -0
- package/skills/Build System/Specialized/NDK/SKILL.md +264 -0
- package/skills/Build System/Version Catalog/SKILL.md +304 -0
- package/skills/Concurrency/Background Processing/SKILL.md +185 -0
- package/skills/Concurrency/Channel/SKILL.md +207 -0
- package/skills/Concurrency/Coroutine/SKILL.md +200 -0
- package/skills/Concurrency/Flow/SKILL.md +179 -0
- package/skills/Concurrency/Mutex Strategy/SKILL.md +185 -0
- package/skills/Concurrency/SharedFlow/SKILL.md +171 -0
- package/skills/Concurrency/StateFlow/SKILL.md +175 -0
- package/skills/Concurrency/Structured Concurrency/SKILL.md +197 -0
- package/skills/Concurrency/Synchronization Policy/SKILL.md +192 -0
- package/skills/Core Language/Annotation Processing/SKILL.md +224 -0
- package/skills/Core Language/DSL/SKILL.md +186 -0
- package/skills/Core Language/Extension Functions Design/SKILL.md +191 -0
- package/skills/Core Language/Immutability/SKILL.md +156 -0
- package/skills/Core Language/KMP/SKILL.md +182 -0
- package/skills/Core Language/Kotlin/SKILL.md +187 -0
- package/skills/Core Language/Reactive State Management/SKILL.md +228 -0
- package/skills/Core Language/Reactive Streams/SKILL.md +235 -0
- package/skills/Core Language/Serialization/SKILL.md +191 -0
- package/skills/Data Layer/Cache Strategy/SKILL.md +261 -0
- package/skills/Data Layer/Conflict Resolution/SKILL.md +248 -0
- package/skills/Data Layer/DAO/SKILL.md +225 -0
- package/skills/Data Layer/DTO Mapping/SKILL.md +269 -0
- package/skills/Data Layer/DataStore/SKILL.md +264 -0
- package/skills/Data Layer/Database Versioning Strategy/SKILL.md +215 -0
- package/skills/Data Layer/Encrypted Database/SKILL.md +212 -0
- package/skills/Data Layer/File Storage/SKILL.md +247 -0
- package/skills/Data Layer/Indexing/SKILL.md +184 -0
- package/skills/Data Layer/Key-Value Store Strategy/SKILL.md +185 -0
- package/skills/Data Layer/Merge Strategy/SKILL.md +240 -0
- package/skills/Data Layer/Migration/SKILL.md +243 -0
- package/skills/Data Layer/Paging/SKILL.md +264 -0
- package/skills/Data Layer/Proto DataStore/SKILL.md +250 -0
- package/skills/Data Layer/Room/SKILL.md +244 -0
- package/skills/Data Layer/SQLite/SKILL.md +255 -0
- package/skills/Data Layer/Sync Engine/SKILL.md +268 -0
- package/skills/Dependency Injection/Dagger/SKILL.md +283 -0
- package/skills/Dependency Injection/Hilt/SKILL.md +345 -0
- package/skills/Dependency Injection/Koin/SKILL.md +282 -0
- package/skills/Developer Experience/Detekt/SKILL.md +272 -0
- package/skills/Developer Experience/Lint Rule/SKILL.md +281 -0
- package/skills/Google Ecosystem/Analytics/SKILL.md +281 -0
- package/skills/Google Ecosystem/Crashlytics/SKILL.md +234 -0
- package/skills/Google Ecosystem/Firebase/SKILL.md +200 -0
- package/skills/Google Ecosystem/Firebase Messaging/SKILL.md +266 -0
- package/skills/Media/Audio/SKILL.md +257 -0
- package/skills/Media/Camera/SKILL.md +229 -0
- package/skills/Media/CameraX/SKILL.md +295 -0
- package/skills/Media/ExoPlayer/SKILL.md +258 -0
- package/skills/Media/Video/SKILL.md +228 -0
- package/skills/Meta Skills/Domain Error Model/SKILL.md +238 -0
- package/skills/Meta Skills/Error Handling/SKILL.md +255 -0
- package/skills/Meta Skills/Error Mapping/SKILL.md +232 -0
- package/skills/Meta Skills/Failure Strategy/SKILL.md +294 -0
- package/skills/Meta Skills/Migration Strategy/SKILL.md +305 -0
- package/skills/Meta Skills/User Friendly Errors/SKILL.md +334 -0
- package/skills/Navigation/Deep Navigation/SKILL.md +209 -0
- package/skills/Navigation/Navigation/SKILL.md +215 -0
- package/skills/Navigation/Nested Navigation/SKILL.md +214 -0
- package/skills/Networking/API Contract/SKILL.md +220 -0
- package/skills/Networking/Authentication/SKILL.md +210 -0
- package/skills/Networking/Certificate Pinning/SKILL.md +167 -0
- package/skills/Networking/Fallback Strategy/SKILL.md +182 -0
- package/skills/Networking/Ktor/SKILL.md +219 -0
- package/skills/Networking/Multipart Upload/SKILL.md +213 -0
- package/skills/Networking/OkHttp/SKILL.md +193 -0
- package/skills/Networking/REST/SKILL.md +178 -0
- package/skills/Networking/Rate Limiting/SKILL.md +170 -0
- package/skills/Networking/Retrofit/SKILL.md +241 -0
- package/skills/Networking/Retry-Backoff/SKILL.md +181 -0
- package/skills/Networking/Server-Sent Events (SSE)/SKILL.md +196 -0
- package/skills/Networking/WebSocket/SKILL.md +224 -0
- package/skills/Observability/Crash Reporting/SKILL.md +219 -0
- package/skills/Observability/Logging/SKILL.md +168 -0
- package/skills/Observability/Metrics/SKILL.md +227 -0
- package/skills/Observability/Structured Logging/SKILL.md +234 -0
- package/skills/Performance/ANR Prevention/SKILL.md +192 -0
- package/skills/Performance/Allocation Optimization/SKILL.md +179 -0
- package/skills/Performance/App Startup/SKILL.md +183 -0
- package/skills/Performance/Baseline Profile/SKILL.md +205 -0
- package/skills/Performance/Battery Optimization/SKILL.md +192 -0
- package/skills/Performance/Benchmark/SKILL.md +182 -0
- package/skills/Performance/Bitmap Optimization/SKILL.md +178 -0
- package/skills/Performance/Compose Optimization/SKILL.md +187 -0
- package/skills/Performance/Heap Management/SKILL.md +184 -0
- package/skills/Performance/Macrobenchmark/SKILL.md +214 -0
- package/skills/Performance/Memory Leak Prevention/SKILL.md +218 -0
- package/skills/Performance/Rendering Performance/SKILL.md +205 -0
- package/skills/Performance/Startup Optimization/SKILL.md +219 -0
- package/skills/Security/Biometric/SKILL.md +224 -0
- package/skills/Security/Certificate Transparency/SKILL.md +158 -0
- package/skills/Security/Cryptography/SKILL.md +244 -0
- package/skills/Security/Encrypted Storage/SKILL.md +273 -0
- package/skills/Security/Frida Detection/SKILL.md +230 -0
- package/skills/Security/Hook Detection/SKILL.md +197 -0
- package/skills/Security/Keystore/SKILL.md +272 -0
- package/skills/Security/Network Security Config/SKILL.md +186 -0
- package/skills/Security/Obfuscation/SKILL.md +226 -0
- package/skills/Security/Proguard/SKILL.md +202 -0
- package/skills/Security/R8/SKILL.md +234 -0
- package/skills/Security/Reverse Engineering Resistance/SKILL.md +267 -0
- package/skills/Security/Root Detection/SKILL.md +220 -0
- package/skills/Security/Secure Networking/SKILL.md +220 -0
- package/skills/System Integration/AlarmManager/SKILL.md +182 -0
- package/skills/System Integration/App Widget/SKILL.md +182 -0
- package/skills/System Integration/Deep Link/SKILL.md +187 -0
- package/skills/System Integration/Foreground Service/SKILL.md +212 -0
- package/skills/System Integration/Notification/SKILL.md +237 -0
- package/skills/System Integration/WorkManager/SKILL.md +256 -0
- package/skills/System Integration/clipboard/SKILL.md +155 -0
- package/skills/System Integration/share-intent/SKILL.md +182 -0
- package/skills/Testing/Compose Testing/SKILL.md +296 -0
- package/skills/Testing/Espresso/SKILL.md +292 -0
- package/skills/Testing/Fake Data/SKILL.md +245 -0
- package/skills/Testing/Integration Testing/SKILL.md +288 -0
- package/skills/Testing/Mocking/SKILL.md +229 -0
- package/skills/Testing/Snapshot Testing/SKILL.md +259 -0
- package/skills/Testing/UI Testing/SKILL.md +293 -0
- package/skills/Testing/Unit Testing/SKILL.md +309 -0
- package/skills/UI System/Bottom Sheet Patterns/SKILL.md +279 -0
- package/skills/UI System/Compose/SKILL.md +296 -0
- package/skills/UI System/Compose Animation/SKILL.md +281 -0
- package/skills/UI System/Compose Multiplatform/SKILL.md +261 -0
- package/skills/UI System/Compose Navigation/SKILL.md +255 -0
- package/skills/UI System/Compose Performance/SKILL.md +274 -0
- package/skills/UI System/Design System/SKILL.md +217 -0
- package/skills/UI System/Empty State Strategy/SKILL.md +208 -0
- package/skills/UI System/Keyboard Navigation/SKILL.md +214 -0
- package/skills/UI System/Loading Strategy/SKILL.md +254 -0
- package/skills/UI System/Material 3/SKILL.md +279 -0
- package/skills/UI System/RTL/SKILL.md +179 -0
- package/src/index.ts +182 -0
- 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
|