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,228 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reactive-state-management
|
|
3
|
+
description: >
|
|
4
|
+
Reactive state management patterns for Android using StateFlow and Flow.
|
|
5
|
+
Load this skill when designing how state is produced, transformed, and
|
|
6
|
+
consumed across ViewModel and UI layers in a reactive, observable way.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Reactive State Management
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
Reactive State Management means that UI state is modeled as an observable stream. The UI never pulls state — it reacts to state emissions. State is always immutable, flows in one direction, and is the single source of truth for what the UI renders.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- State is **immutable** — never mutate, always replace via `copy()`
|
|
20
|
+
- State flows **one direction** — ViewModel → UI, never reverse
|
|
21
|
+
- UI **observes** state — never reads it on demand
|
|
22
|
+
- There is **one state object** per screen — not multiple separate fields
|
|
23
|
+
- State is **complete** — the UI can render correctly from state alone
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## State Modeling
|
|
28
|
+
|
|
29
|
+
```kotlin
|
|
30
|
+
// ✅ Single sealed state per screen
|
|
31
|
+
data class UserListUiState(
|
|
32
|
+
val users: List<User> = emptyList(),
|
|
33
|
+
val isLoading: Boolean = false,
|
|
34
|
+
val error: String? = null,
|
|
35
|
+
val isEmpty: Boolean = false
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
// ✅ Use sealed class for mutually exclusive states
|
|
39
|
+
sealed class ScreenState<out T> {
|
|
40
|
+
object Loading : ScreenState<Nothing>()
|
|
41
|
+
data class Success<T>(val data: T) : ScreenState<T>()
|
|
42
|
+
data class Error(val message: String) : ScreenState<Nothing>()
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## ViewModel — Producing State
|
|
49
|
+
|
|
50
|
+
```kotlin
|
|
51
|
+
class UserListViewModel(
|
|
52
|
+
private val getUsersUseCase: GetUsersUseCase
|
|
53
|
+
) : ViewModel() {
|
|
54
|
+
|
|
55
|
+
// ✅ Private mutable — public immutable
|
|
56
|
+
private val _state = MutableStateFlow(UserListUiState())
|
|
57
|
+
val state: StateFlow<UserListUiState> = _state.asStateFlow()
|
|
58
|
+
|
|
59
|
+
// ✅ One-off events via SharedFlow
|
|
60
|
+
private val _events = MutableSharedFlow<UserListEvent>()
|
|
61
|
+
val events: SharedFlow<UserListEvent> = _events.asSharedFlow()
|
|
62
|
+
|
|
63
|
+
init {
|
|
64
|
+
loadUsers()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fun loadUsers() {
|
|
68
|
+
viewModelScope.launch {
|
|
69
|
+
_state.update { it.copy(isLoading = true, error = null) }
|
|
70
|
+
getUsersUseCase()
|
|
71
|
+
.onSuccess { users ->
|
|
72
|
+
_state.update {
|
|
73
|
+
it.copy(
|
|
74
|
+
isLoading = false,
|
|
75
|
+
users = users,
|
|
76
|
+
isEmpty = users.isEmpty()
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
.onFailure { error ->
|
|
81
|
+
_state.update {
|
|
82
|
+
it.copy(isLoading = false, error = error.message)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fun onUserClicked(userId: String) {
|
|
89
|
+
viewModelScope.launch {
|
|
90
|
+
_events.emit(UserListEvent.NavigateToDetail(userId))
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ✅ Events for one-time side effects
|
|
96
|
+
sealed class UserListEvent {
|
|
97
|
+
data class NavigateToDetail(val userId: String) : UserListEvent()
|
|
98
|
+
data class ShowSnackbar(val message: String) : UserListEvent()
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Deriving State from Flow
|
|
105
|
+
|
|
106
|
+
```kotlin
|
|
107
|
+
// ✅ stateIn — derive StateFlow from upstream Flow
|
|
108
|
+
class UserListViewModel(repository: UserRepository) : ViewModel() {
|
|
109
|
+
|
|
110
|
+
val state: StateFlow<UserListUiState> = repository
|
|
111
|
+
.observeUsers()
|
|
112
|
+
.map { users ->
|
|
113
|
+
UserListUiState(
|
|
114
|
+
users = users,
|
|
115
|
+
isEmpty = users.isEmpty()
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
.stateIn(
|
|
119
|
+
scope = viewModelScope,
|
|
120
|
+
started = SharingStarted.WhileSubscribed(5_000),
|
|
121
|
+
initialValue = UserListUiState(isLoading = true)
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Combining Multiple Sources
|
|
129
|
+
|
|
130
|
+
```kotlin
|
|
131
|
+
// ✅ combine — merge multiple streams into one state
|
|
132
|
+
class DashboardViewModel(
|
|
133
|
+
userRepository: UserRepository,
|
|
134
|
+
settingsRepository: SettingsRepository
|
|
135
|
+
) : ViewModel() {
|
|
136
|
+
|
|
137
|
+
val state: StateFlow<DashboardUiState> = combine(
|
|
138
|
+
userRepository.observeCurrentUser(),
|
|
139
|
+
settingsRepository.observeSettings()
|
|
140
|
+
) { user, settings ->
|
|
141
|
+
DashboardUiState(
|
|
142
|
+
userName = user.name,
|
|
143
|
+
isDarkMode = settings.isDarkMode
|
|
144
|
+
)
|
|
145
|
+
}.stateIn(
|
|
146
|
+
scope = viewModelScope,
|
|
147
|
+
started = SharingStarted.WhileSubscribed(5_000),
|
|
148
|
+
initialValue = DashboardUiState()
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## UI — Consuming State
|
|
156
|
+
|
|
157
|
+
```kotlin
|
|
158
|
+
// ✅ Compose — collect state
|
|
159
|
+
@Composable
|
|
160
|
+
fun UserListScreen(viewModel: UserListViewModel = hiltViewModel()) {
|
|
161
|
+
val state by viewModel.state.collectAsStateWithLifecycle()
|
|
162
|
+
|
|
163
|
+
// ✅ Collect one-time events
|
|
164
|
+
val lifecycleOwner = LocalLifecycleOwner.current
|
|
165
|
+
LaunchedEffect(Unit) {
|
|
166
|
+
viewModel.events
|
|
167
|
+
.flowWithLifecycle(lifecycleOwner.lifecycle)
|
|
168
|
+
.collect { event ->
|
|
169
|
+
when (event) {
|
|
170
|
+
is UserListEvent.NavigateToDetail -> navController.navigate(...)
|
|
171
|
+
is UserListEvent.ShowSnackbar -> snackbarHostState.showSnackbar(...)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
UserListContent(state = state, onUserClick = viewModel::onUserClicked)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ✅ Fragment — collect with repeatOnLifecycle
|
|
180
|
+
viewLifecycleOwner.lifecycleScope.launch {
|
|
181
|
+
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
182
|
+
viewModel.state.collect { state ->
|
|
183
|
+
renderState(state)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## State vs Events
|
|
192
|
+
|
|
193
|
+
| Type | Use for | Implementation |
|
|
194
|
+
| --------- | ------------------------------------------ | -------------- |
|
|
195
|
+
| **State** | Persistent UI data (list, loading, error) | `StateFlow` |
|
|
196
|
+
| **Event** | One-time actions (navigate, toast, dialog) | `SharedFlow` |
|
|
197
|
+
|
|
198
|
+
```kotlin
|
|
199
|
+
// ✅ State — persists, survives recomposition
|
|
200
|
+
val isLoading: StateFlow<Boolean>
|
|
201
|
+
|
|
202
|
+
// ✅ Event — fires once, not replayed
|
|
203
|
+
val navigationEvent: SharedFlow<NavigationEvent>
|
|
204
|
+
|
|
205
|
+
// ❌ Don't model navigation in state — it replays on rotation
|
|
206
|
+
data class UiState(val navigateTo: String?) // wrong
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Anti-Patterns
|
|
212
|
+
|
|
213
|
+
- Multiple separate `StateFlow` fields per screen — use one state object
|
|
214
|
+
- Exposing `MutableStateFlow` from ViewModel — always expose as `StateFlow`
|
|
215
|
+
- Collecting state with `lifecycleScope.launch {}` without `repeatOnLifecycle` — leaks in background
|
|
216
|
+
- Modeling one-time events as state — use `SharedFlow` for events
|
|
217
|
+
- Calling `stateFlow.value` in UI to read state imperatively — always collect reactively
|
|
218
|
+
- Triggering side effects inside `map {}` or `combine {}` — use `onEach` or handle in ViewModel
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Related Skills
|
|
223
|
+
|
|
224
|
+
- `stateflow` — StateFlow internals and patterns
|
|
225
|
+
- `sharedflow` — SharedFlow for one-time events
|
|
226
|
+
- `reactive-streams` — Flow operators and transformation
|
|
227
|
+
- `mvvm` — ViewModel and state ownership
|
|
228
|
+
- `side-effect-management` — handling side effects in reactive pipelines
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reactive-streams
|
|
3
|
+
description: >
|
|
4
|
+
Reactive Streams patterns and operators for Android development.
|
|
5
|
+
Load this skill when working with data streams, chaining async operations,
|
|
6
|
+
transforming flows, combining multiple sources, or designing reactive pipelines
|
|
7
|
+
across layers.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Reactive Streams
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
Reactive Streams is a standard for asynchronous stream processing with non-blocking backpressure. In Kotlin/Android, this is implemented via `Flow`. This skill covers how to design, transform, and consume reactive streams correctly across all layers.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Core Principles
|
|
19
|
+
|
|
20
|
+
- Data flows **downward** — from data source to UI, never upward
|
|
21
|
+
- Streams are **cold by default** (`Flow`) — only active when collected
|
|
22
|
+
- **Never collect a Flow inside another Flow** — use operators instead
|
|
23
|
+
- **Never expose `MutableStateFlow` or `MutableSharedFlow`** from public APIs
|
|
24
|
+
- Transformation happens in the **middle layers** — not in UI, not in data source
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Flow Basics
|
|
29
|
+
|
|
30
|
+
```kotlin
|
|
31
|
+
// ✅ Cold flow — executes only when collected
|
|
32
|
+
fun getUsers(): Flow<List<User>> = flow {
|
|
33
|
+
while (true) {
|
|
34
|
+
emit(repository.fetchUsers())
|
|
35
|
+
delay(30_000)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ✅ Flow from suspend function
|
|
40
|
+
fun getUserById(id: String): Flow<User> = flow {
|
|
41
|
+
emit(repository.getUser(id))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ✅ Flow from Room (already returns Flow)
|
|
45
|
+
@Query("SELECT * FROM users")
|
|
46
|
+
fun observeUsers(): Flow<List<UserEntity>>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Key Operators
|
|
52
|
+
|
|
53
|
+
### Transformation
|
|
54
|
+
|
|
55
|
+
```kotlin
|
|
56
|
+
// map — transform each emission
|
|
57
|
+
val names: Flow<List<String>> = usersFlow.map { users ->
|
|
58
|
+
users.map { it.name }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// flatMapLatest — switch to new flow on each emission (search, user selection)
|
|
62
|
+
val results: Flow<List<Result>> = queryFlow.flatMapLatest { query ->
|
|
63
|
+
repository.search(query)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// transform — emit multiple values per input
|
|
67
|
+
val events: Flow<Event> = actionsFlow.transform { action ->
|
|
68
|
+
emit(Event.Started)
|
|
69
|
+
process(action)
|
|
70
|
+
emit(Event.Completed)
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Filtering
|
|
75
|
+
|
|
76
|
+
```kotlin
|
|
77
|
+
// filter — conditional emission
|
|
78
|
+
val activeUsers = usersFlow.filter { it.isActive }
|
|
79
|
+
|
|
80
|
+
// distinctUntilChanged — suppress duplicate emissions
|
|
81
|
+
val query = searchFlow.distinctUntilChanged()
|
|
82
|
+
|
|
83
|
+
// debounce — wait for silence (search input)
|
|
84
|
+
val debouncedQuery = searchFlow
|
|
85
|
+
.debounce(300)
|
|
86
|
+
.distinctUntilChanged()
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Combination
|
|
90
|
+
|
|
91
|
+
```kotlin
|
|
92
|
+
// combine — latest from each source
|
|
93
|
+
val uiState: Flow<UiState> = combine(
|
|
94
|
+
usersFlow,
|
|
95
|
+
isLoadingFlow,
|
|
96
|
+
errorFlow
|
|
97
|
+
) { users, isLoading, error ->
|
|
98
|
+
UiState(users = users, isLoading = isLoading, error = error)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// zip — pair emissions one-to-one
|
|
102
|
+
val paired: Flow<Pair<A, B>> = flowA.zip(flowB) { a, b -> a to b }
|
|
103
|
+
|
|
104
|
+
// merge — emit from all sources as they arrive
|
|
105
|
+
val allEvents: Flow<Event> = merge(clickFlow, scrollFlow, keyboardFlow)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Error Handling
|
|
109
|
+
|
|
110
|
+
```kotlin
|
|
111
|
+
// ✅ catch — handle errors without stopping the stream
|
|
112
|
+
usersFlow
|
|
113
|
+
.catch { e -> emit(emptyList()) }
|
|
114
|
+
.collect { ... }
|
|
115
|
+
|
|
116
|
+
// ✅ retry — retry on failure
|
|
117
|
+
networkFlow
|
|
118
|
+
.retry(3) { e -> e is IOException }
|
|
119
|
+
.collect { ... }
|
|
120
|
+
|
|
121
|
+
// ✅ retryWhen — conditional retry with backoff
|
|
122
|
+
networkFlow
|
|
123
|
+
.retryWhen { cause, attempt ->
|
|
124
|
+
cause is IOException && attempt < 3
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Flow Across Layers
|
|
131
|
+
|
|
132
|
+
```kotlin
|
|
133
|
+
// ✅ Repository — expose Flow from data source
|
|
134
|
+
class UserRepository {
|
|
135
|
+
fun observeUsers(): Flow<List<User>> =
|
|
136
|
+
localDataSource.observeUsers().map { entities ->
|
|
137
|
+
entities.map { it.toDomain() }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ✅ UseCase — transform and combine
|
|
142
|
+
class GetActiveUsersUseCase(private val repository: UserRepository) {
|
|
143
|
+
operator fun invoke(): Flow<List<User>> =
|
|
144
|
+
repository.observeUsers()
|
|
145
|
+
.map { users -> users.filter { it.isActive } }
|
|
146
|
+
.distinctUntilChanged()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ✅ ViewModel — collect into StateFlow
|
|
150
|
+
class UserViewModel(useCase: GetActiveUsersUseCase) : ViewModel() {
|
|
151
|
+
val users: StateFlow<List<User>> = useCase()
|
|
152
|
+
.stateIn(
|
|
153
|
+
scope = viewModelScope,
|
|
154
|
+
started = SharingStarted.WhileSubscribed(5_000),
|
|
155
|
+
initialValue = emptyList()
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ✅ UI — collect with lifecycle awareness
|
|
160
|
+
viewLifecycleOwner.lifecycleScope.launch {
|
|
161
|
+
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
162
|
+
viewModel.users.collect { users ->
|
|
163
|
+
adapter.submitList(users)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## stateIn vs shareIn
|
|
172
|
+
|
|
173
|
+
```kotlin
|
|
174
|
+
// ✅ stateIn — convert Flow to StateFlow (single value, replays last)
|
|
175
|
+
val state: StateFlow<T> = flow.stateIn(
|
|
176
|
+
scope = viewModelScope,
|
|
177
|
+
started = SharingStarted.WhileSubscribed(5_000),
|
|
178
|
+
initialValue = initialValue
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
// ✅ shareIn — convert Flow to SharedFlow (multicast, configurable replay)
|
|
182
|
+
val shared: SharedFlow<T> = flow.shareIn(
|
|
183
|
+
scope = viewModelScope,
|
|
184
|
+
started = SharingStarted.WhileSubscribed(5_000),
|
|
185
|
+
replay = 1
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
// WhileSubscribed(5_000) — keeps upstream alive 5s after last subscriber
|
|
189
|
+
// Lazy — starts only when first subscriber appears
|
|
190
|
+
// Eagerly — starts immediately
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Backpressure
|
|
196
|
+
|
|
197
|
+
```kotlin
|
|
198
|
+
// ✅ buffer — decouple producer from consumer
|
|
199
|
+
fastProducerFlow
|
|
200
|
+
.buffer(capacity = 64)
|
|
201
|
+
.collect { slowConsumer(it) }
|
|
202
|
+
|
|
203
|
+
// ✅ conflate — drop intermediate values, keep latest
|
|
204
|
+
sensorFlow
|
|
205
|
+
.conflate()
|
|
206
|
+
.collect { render(it) }
|
|
207
|
+
|
|
208
|
+
// ✅ collectLatest — cancel previous on new emission
|
|
209
|
+
searchFlow.collectLatest { query ->
|
|
210
|
+
val results = repository.search(query)
|
|
211
|
+
showResults(results)
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Anti-Patterns
|
|
218
|
+
|
|
219
|
+
- Collecting Flow inside `viewModelScope.launch {}` directly in UI — use `repeatOnLifecycle`
|
|
220
|
+
- Using `GlobalScope` to launch flow collection — always use scoped coroutines
|
|
221
|
+
- Nested `collect` calls — use `flatMapLatest` or `combine` instead
|
|
222
|
+
- Creating a new Flow on every recomposition in Compose — hoist to ViewModel
|
|
223
|
+
- Using `flow.first()` in a loop — use operators instead
|
|
224
|
+
- Ignoring backpressure on fast producers — use `buffer`, `conflate`, or `collectLatest`
|
|
225
|
+
- Exposing `MutableSharedFlow` — always expose as `SharedFlow` or `Flow`
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Related Skills
|
|
230
|
+
|
|
231
|
+
- `coroutine` — coroutine scope and dispatcher management
|
|
232
|
+
- `stateflow` — StateFlow specific patterns
|
|
233
|
+
- `sharedflow` — SharedFlow specific patterns
|
|
234
|
+
- `state-management` — UI state with reactive streams
|
|
235
|
+
- `repository-pattern` — exposing streams from data layer
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: serialization
|
|
3
|
+
description: >
|
|
4
|
+
Kotlinx Serialization setup and usage for Android and KMP projects.
|
|
5
|
+
Load this skill when serializing/deserializing JSON or other formats,
|
|
6
|
+
configuring the Json instance, mapping API responses, or handling
|
|
7
|
+
custom serialization logic.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Serialization
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
Kotlinx Serialization is the standard serialization library for Kotlin/Android. It is compile-time safe, KMP-compatible, and integrates natively with Retrofit, Ktor, and DataStore.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Core Principles
|
|
19
|
+
|
|
20
|
+
- Use `@Serializable` on all data transfer objects (DTOs)
|
|
21
|
+
- Never use Gson or Moshi — use Kotlinx Serialization
|
|
22
|
+
- Configure a **single shared `Json` instance** per module — never create inline
|
|
23
|
+
- Domain models must **not** be `@Serializable` — only DTOs
|
|
24
|
+
- Use `@SerialName` when API field names differ from Kotlin naming conventions
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Setup
|
|
29
|
+
|
|
30
|
+
```toml
|
|
31
|
+
# libs.versions.toml
|
|
32
|
+
[versions]
|
|
33
|
+
kotlinx-serialization = "1.7.1"
|
|
34
|
+
|
|
35
|
+
[libraries]
|
|
36
|
+
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
|
|
37
|
+
|
|
38
|
+
[plugins]
|
|
39
|
+
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```kotlin
|
|
43
|
+
// build.gradle.kts
|
|
44
|
+
plugins {
|
|
45
|
+
alias(libs.plugins.kotlin.serialization)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
dependencies {
|
|
49
|
+
implementation(libs.kotlinx.serialization.json)
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Json Instance Configuration
|
|
56
|
+
|
|
57
|
+
```kotlin
|
|
58
|
+
// ✅ Single shared instance — define once, inject everywhere
|
|
59
|
+
val json = Json {
|
|
60
|
+
ignoreUnknownKeys = true // safe for API evolution
|
|
61
|
+
isLenient = false // strict parsing by default
|
|
62
|
+
encodeDefaults = false // don't serialize default values
|
|
63
|
+
prettyPrint = false // compact output in production
|
|
64
|
+
coerceInputValues = true // handle null → default for non-nullable
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## DTO Design
|
|
71
|
+
|
|
72
|
+
```kotlin
|
|
73
|
+
// ✅ DTOs are @Serializable — domain models are not
|
|
74
|
+
@Serializable
|
|
75
|
+
data class UserDto(
|
|
76
|
+
@SerialName("user_id") val id: String,
|
|
77
|
+
@SerialName("full_name") val name: String,
|
|
78
|
+
@SerialName("email_address") val email: String,
|
|
79
|
+
@SerialName("is_active") val isActive: Boolean = true,
|
|
80
|
+
@SerialName("created_at") val createdAt: Long? = null
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
// ✅ Domain model — clean, no serialization annotations
|
|
84
|
+
data class User(
|
|
85
|
+
val id: String,
|
|
86
|
+
val name: String,
|
|
87
|
+
val email: String,
|
|
88
|
+
val isActive: Boolean,
|
|
89
|
+
val createdAt: Long?
|
|
90
|
+
)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Custom Serializers
|
|
96
|
+
|
|
97
|
+
```kotlin
|
|
98
|
+
// ✅ Use custom serializer for types that can't be annotated
|
|
99
|
+
object UUIDSerializer : KSerializer<UUID> {
|
|
100
|
+
override val descriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
|
|
101
|
+
override fun serialize(encoder: Encoder, value: UUID) = encoder.encodeString(value.toString())
|
|
102
|
+
override fun deserialize(decoder: Decoder): UUID = UUID.fromString(decoder.decodeString())
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Apply on property
|
|
106
|
+
@Serializable
|
|
107
|
+
data class OrderDto(
|
|
108
|
+
@Serializable(with = UUIDSerializer::class)
|
|
109
|
+
val id: UUID
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Sealed Class Serialization
|
|
116
|
+
|
|
117
|
+
```kotlin
|
|
118
|
+
// ✅ Use @JsonClassDiscriminator for polymorphic types
|
|
119
|
+
@Serializable
|
|
120
|
+
@JsonClassDiscriminator("type")
|
|
121
|
+
sealed class EventDto {
|
|
122
|
+
@Serializable
|
|
123
|
+
@SerialName("click")
|
|
124
|
+
data class Click(val x: Int, val y: Int) : EventDto()
|
|
125
|
+
|
|
126
|
+
@Serializable
|
|
127
|
+
@SerialName("scroll")
|
|
128
|
+
data class Scroll(val delta: Int) : EventDto()
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Integration with Retrofit
|
|
135
|
+
|
|
136
|
+
```kotlin
|
|
137
|
+
// ✅ Use kotlinx-serialization converter — not Gson
|
|
138
|
+
dependencies {
|
|
139
|
+
implementation(libs.retrofit.kotlinx.serialization)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
val retrofit = Retrofit.Builder()
|
|
143
|
+
.baseUrl(baseUrl)
|
|
144
|
+
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
|
145
|
+
.build()
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Integration with Ktor
|
|
149
|
+
|
|
150
|
+
```kotlin
|
|
151
|
+
// ✅ Use ContentNegotiation plugin
|
|
152
|
+
install(ContentNegotiation) {
|
|
153
|
+
json(json) // pass the shared Json instance
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Handling Unknown/Dynamic Fields
|
|
160
|
+
|
|
161
|
+
```kotlin
|
|
162
|
+
// ✅ Use JsonObject for truly dynamic content
|
|
163
|
+
@Serializable
|
|
164
|
+
data class WebhookDto(
|
|
165
|
+
val event: String,
|
|
166
|
+
val payload: JsonObject // dynamic structure
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
// Extract typed value from JsonObject
|
|
170
|
+
val userId = webhook.payload["user_id"]?.jsonPrimitive?.content
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Anti-Patterns
|
|
176
|
+
|
|
177
|
+
- Using Gson or Moshi — not KMP-compatible, less type-safe
|
|
178
|
+
- `@Serializable` on domain models — couples domain to transport layer
|
|
179
|
+
- Creating `Json {}` inline at call site — inconsistent configuration
|
|
180
|
+
- Missing `@SerialName` when API uses snake_case — breaks deserialization
|
|
181
|
+
- Using `Any` or `Map<String, Any>` — loses type safety; use `JsonObject`
|
|
182
|
+
- Ignoring `ignoreUnknownKeys = true` — crashes on API evolution
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Related Skills
|
|
187
|
+
|
|
188
|
+
- `dto-mapping` — mapping between DTOs and domain models
|
|
189
|
+
- `retrofit` — Retrofit setup with serialization converter
|
|
190
|
+
- `ktor` — Ktor client setup with serialization plugin
|
|
191
|
+
- `kmp` — shared serialization setup in multiplatform projects
|