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,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
|