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,200 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: coroutine
|
|
3
|
+
description: >
|
|
4
|
+
Kotlin Coroutines fundamentals and patterns for Android development.
|
|
5
|
+
Load this skill when launching coroutines, choosing the right scope,
|
|
6
|
+
managing coroutine lifecycle, handling exceptions, or structuring
|
|
7
|
+
async operations with suspend functions.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Coroutine
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
Kotlin Coroutines provide a structured way to write asynchronous code sequentially. On Android, coroutines are the standard for all async operations — network calls, database queries, and background work. Every coroutine must be launched in a scope that controls its lifecycle.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Core Principles
|
|
19
|
+
|
|
20
|
+
- Always use the **narrowest scope** available — `viewModelScope`, `lifecycleScope`, not `GlobalScope`
|
|
21
|
+
- Use `Dispatchers.IO` for blocking I/O, `Dispatchers.Default` for CPU-heavy work
|
|
22
|
+
- Never use `GlobalScope` — it has no lifecycle and can't be cancelled
|
|
23
|
+
- Exceptions in coroutines must be handled explicitly — they don't propagate like regular exceptions
|
|
24
|
+
- Use `supervisorScope` when child failures should not cancel siblings
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Scope Selection
|
|
29
|
+
|
|
30
|
+
```kotlin
|
|
31
|
+
// ✅ viewModelScope — cancelled when ViewModel is cleared
|
|
32
|
+
class UserViewModel : ViewModel() {
|
|
33
|
+
fun loadUser() {
|
|
34
|
+
viewModelScope.launch {
|
|
35
|
+
// auto-cancelled when ViewModel is destroyed
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ✅ lifecycleScope — cancelled when lifecycle is destroyed
|
|
41
|
+
class UserFragment : Fragment() {
|
|
42
|
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
43
|
+
viewLifecycleOwner.lifecycleScope.launch {
|
|
44
|
+
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
45
|
+
// cancelled when below STARTED
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ✅ Custom scope — for non-lifecycle components
|
|
52
|
+
@Singleton
|
|
53
|
+
class SyncManager @Inject constructor() {
|
|
54
|
+
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
55
|
+
|
|
56
|
+
fun startSync() {
|
|
57
|
+
scope.launch { /* ... */ }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fun cancel() {
|
|
61
|
+
scope.cancel()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ❌ Never use GlobalScope
|
|
66
|
+
GlobalScope.launch { /* no lifecycle, no cancellation */ }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Dispatcher Selection
|
|
72
|
+
|
|
73
|
+
```kotlin
|
|
74
|
+
// ✅ IO — network, database, file operations
|
|
75
|
+
viewModelScope.launch(Dispatchers.IO) {
|
|
76
|
+
val users = userDao.getAll()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ✅ Default — CPU-intensive work (sorting, parsing, computation)
|
|
80
|
+
viewModelScope.launch(Dispatchers.Default) {
|
|
81
|
+
val sorted = largeList.sortedBy { it.name }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ✅ Main — UI updates (usually implicit in viewModelScope)
|
|
85
|
+
viewModelScope.launch(Dispatchers.Main) {
|
|
86
|
+
binding.textView.text = "Updated"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ✅ withContext — switch dispatcher inside a coroutine
|
|
90
|
+
suspend fun processData(): List<Item> = withContext(Dispatchers.Default) {
|
|
91
|
+
rawData.map { parse(it) }
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Exception Handling
|
|
98
|
+
|
|
99
|
+
```kotlin
|
|
100
|
+
// ✅ try/catch inside launch
|
|
101
|
+
viewModelScope.launch {
|
|
102
|
+
try {
|
|
103
|
+
val user = repository.getUser(id)
|
|
104
|
+
_state.value = UiState.Success(user)
|
|
105
|
+
} catch (e: Exception) {
|
|
106
|
+
_state.value = UiState.Error(e.message ?: "Error")
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ✅ CoroutineExceptionHandler for unhandled exceptions
|
|
111
|
+
val handler = CoroutineExceptionHandler { _, throwable ->
|
|
112
|
+
Timber.e(throwable, "Unhandled coroutine exception")
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
viewModelScope.launch(handler) {
|
|
116
|
+
riskyOperation()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ✅ runCatching — functional style
|
|
120
|
+
val result = runCatching { repository.getUser(id) }
|
|
121
|
+
result.onSuccess { _state.value = UiState.Success(it) }
|
|
122
|
+
result.onFailure { _state.value = UiState.Error(it.message ?: "Error") }
|
|
123
|
+
|
|
124
|
+
// ❌ async exception — not caught by try/catch on launch
|
|
125
|
+
val deferred = viewModelScope.async { riskyOperation() }
|
|
126
|
+
deferred.await() // exception thrown here — wrap in try/catch
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Parallel Execution
|
|
132
|
+
|
|
133
|
+
```kotlin
|
|
134
|
+
// ✅ Run multiple operations in parallel with async
|
|
135
|
+
suspend fun loadDashboard(): Dashboard {
|
|
136
|
+
return coroutineScope {
|
|
137
|
+
val users = async { userRepository.getUsers() }
|
|
138
|
+
val orders = async { orderRepository.getOrders() }
|
|
139
|
+
val stats = async { statsRepository.getStats() }
|
|
140
|
+
|
|
141
|
+
Dashboard(
|
|
142
|
+
users = users.await().getOrDefault(emptyList()),
|
|
143
|
+
orders = orders.await().getOrDefault(emptyList()),
|
|
144
|
+
stats = stats.await().getOrNull()
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ✅ supervisorScope — one failure doesn't cancel others
|
|
150
|
+
suspend fun loadOptionalData() = supervisorScope {
|
|
151
|
+
val primary = async { primaryRepository.getData() }
|
|
152
|
+
val secondary = async { secondaryRepository.getData() } // failure here won't cancel primary
|
|
153
|
+
|
|
154
|
+
PrimaryData(
|
|
155
|
+
main = primary.await(),
|
|
156
|
+
extra = runCatching { secondary.await() }.getOrNull()
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Cancellation
|
|
164
|
+
|
|
165
|
+
```kotlin
|
|
166
|
+
// ✅ Check cancellation in long loops
|
|
167
|
+
suspend fun processItems(items: List<Item>) {
|
|
168
|
+
items.forEach { item ->
|
|
169
|
+
ensureActive() // throws CancellationException if cancelled
|
|
170
|
+
processItem(item)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ✅ withTimeout
|
|
175
|
+
suspend fun fetchWithTimeout(): User = withTimeout(5_000) {
|
|
176
|
+
api.getUser(id)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ✅ withTimeoutOrNull — returns null on timeout
|
|
180
|
+
val user = withTimeoutOrNull(5_000) { api.getUser(id) }
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Anti-Patterns
|
|
186
|
+
|
|
187
|
+
- Using `GlobalScope` — no lifecycle management, can't be cancelled
|
|
188
|
+
- Launching coroutines without handling exceptions — silent failures
|
|
189
|
+
- Calling blocking functions (`Thread.sleep`, blocking I/O) without `Dispatchers.IO`
|
|
190
|
+
- Using `runBlocking` in Android code outside of tests — blocks the thread
|
|
191
|
+
- Ignoring `CancellationException` in catch blocks — breaks structured concurrency
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Related Skills
|
|
196
|
+
|
|
197
|
+
- `flow` — reactive streams with coroutines
|
|
198
|
+
- `structured-concurrency` — parent-child coroutine relationships
|
|
199
|
+
- `viewmodel` — viewModelScope usage
|
|
200
|
+
- `lifecycle` — lifecycleScope and repeatOnLifecycle
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: flow
|
|
3
|
+
description: >
|
|
4
|
+
Kotlin Flow for reactive streams in Android.
|
|
5
|
+
Load this skill when building cold streams, transforming data pipelines,
|
|
6
|
+
collecting flows with lifecycle awareness, combining multiple flows,
|
|
7
|
+
or converting callbacks to Flow.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Flow
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
Kotlin Flow is a cold, asynchronous stream built on coroutines. It emits values sequentially and is collected in a coroutine. Flow is the standard for reactive data streams in Android — from database queries to UI state. Unlike LiveData, Flow is lifecycle-agnostic and must be collected with lifecycle awareness in the UI layer.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Core Principles
|
|
19
|
+
|
|
20
|
+
- Flow is **cold** — it only runs when collected
|
|
21
|
+
- Collect flows in the UI with `collectAsStateWithLifecycle` (Compose) or `repeatOnLifecycle`
|
|
22
|
+
- Use `flowOn` to switch the dispatcher for upstream operators — not for collection
|
|
23
|
+
- Prefer `StateFlow` for UI state and `SharedFlow` for events — not plain `Flow`
|
|
24
|
+
- Use `callbackFlow` to bridge callback-based APIs to Flow
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Basic Flow
|
|
29
|
+
|
|
30
|
+
```kotlin
|
|
31
|
+
// ✅ Cold flow — runs fresh for each collector
|
|
32
|
+
fun getUsers(): Flow<List<User>> = flow {
|
|
33
|
+
while (true) {
|
|
34
|
+
emit(repository.getUsers())
|
|
35
|
+
delay(30_000) // poll every 30s
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ✅ Flow from Room — already a Flow
|
|
40
|
+
fun getUsersFromDb(): Flow<List<User>> = userDao.getAllUsers() // Room emits on every change
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Flow Operators
|
|
46
|
+
|
|
47
|
+
```kotlin
|
|
48
|
+
// ✅ Transform
|
|
49
|
+
val userNames: Flow<List<String>> = getUsersFlow()
|
|
50
|
+
.map { users -> users.map { it.name } }
|
|
51
|
+
|
|
52
|
+
// ✅ Filter
|
|
53
|
+
val activeUsers: Flow<List<User>> = getUsersFlow()
|
|
54
|
+
.map { users -> users.filter { it.isActive } }
|
|
55
|
+
|
|
56
|
+
// ✅ debounce — wait for stable value
|
|
57
|
+
val searchResults: Flow<List<User>> = searchQuery
|
|
58
|
+
.debounce(300)
|
|
59
|
+
.distinctUntilChanged()
|
|
60
|
+
.flatMapLatest { query -> searchRepository.search(query) }
|
|
61
|
+
|
|
62
|
+
// ✅ combine — merge two flows into one
|
|
63
|
+
val uiState: Flow<UiState> = combine(usersFlow, filtersFlow) { users, filters ->
|
|
64
|
+
UiState(users = users.filter { filters.matches(it) })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ✅ zip — pair emissions one-to-one
|
|
68
|
+
val paired: Flow<Pair<User, Order>> = usersFlow.zip(ordersFlow) { user, order ->
|
|
69
|
+
user to order
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ✅ flatMapLatest — cancel previous and switch to latest
|
|
73
|
+
val results: Flow<List<Result>> = queryFlow
|
|
74
|
+
.flatMapLatest { query -> searchUseCase(query) }
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Error Handling
|
|
80
|
+
|
|
81
|
+
```kotlin
|
|
82
|
+
// ✅ catch — handle errors in the stream
|
|
83
|
+
getUsersFlow()
|
|
84
|
+
.catch { error ->
|
|
85
|
+
emit(emptyList()) // emit fallback
|
|
86
|
+
Timber.e(error)
|
|
87
|
+
}
|
|
88
|
+
.collect { users -> render(users) }
|
|
89
|
+
|
|
90
|
+
// ✅ retry
|
|
91
|
+
getUsersFlow()
|
|
92
|
+
.retry(3) { cause -> cause is IOException }
|
|
93
|
+
.collect { users -> render(users) }
|
|
94
|
+
|
|
95
|
+
// ✅ onEach for side effects
|
|
96
|
+
getUsersFlow()
|
|
97
|
+
.onEach { users -> Timber.d("Received ${users.size} users") }
|
|
98
|
+
.catch { Timber.e(it) }
|
|
99
|
+
.collect { render(it) }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Lifecycle-Aware Collection
|
|
105
|
+
|
|
106
|
+
```kotlin
|
|
107
|
+
// ✅ Compose — collectAsStateWithLifecycle
|
|
108
|
+
@Composable
|
|
109
|
+
fun UserListScreen(viewModel: UserListViewModel = hiltViewModel()) {
|
|
110
|
+
val state by viewModel.state.collectAsStateWithLifecycle()
|
|
111
|
+
UserListContent(state)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ✅ Fragment — repeatOnLifecycle
|
|
115
|
+
viewLifecycleOwner.lifecycleScope.launch {
|
|
116
|
+
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
117
|
+
viewModel.state.collect { render(it) }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ❌ Never collect without lifecycle awareness in UI
|
|
122
|
+
lifecycleScope.launch {
|
|
123
|
+
viewModel.state.collect { render(it) } // doesn't cancel when backgrounded
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## callbackFlow — Bridge Callbacks to Flow
|
|
130
|
+
|
|
131
|
+
```kotlin
|
|
132
|
+
// ✅ Convert callback API to Flow
|
|
133
|
+
fun listenToLocation(): Flow<Location> = callbackFlow {
|
|
134
|
+
val callback = object : LocationCallback() {
|
|
135
|
+
override fun onLocationResult(result: LocationResult) {
|
|
136
|
+
result.lastLocation?.let { trySend(it) }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
locationManager.requestUpdates(callback)
|
|
141
|
+
|
|
142
|
+
awaitClose {
|
|
143
|
+
locationManager.removeUpdates(callback) // cleanup on cancellation
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## flowOn — Upstream Dispatcher
|
|
151
|
+
|
|
152
|
+
```kotlin
|
|
153
|
+
// ✅ flowOn changes dispatcher for everything upstream of it
|
|
154
|
+
val processedData: Flow<List<Item>> = rawDataFlow
|
|
155
|
+
.map { parseHeavy(it) } // runs on Default
|
|
156
|
+
.filter { it.isValid } // runs on Default
|
|
157
|
+
.flowOn(Dispatchers.Default) // applies to map + filter above
|
|
158
|
+
.map { it.toUiModel() } // runs on collection dispatcher
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Anti-Patterns
|
|
164
|
+
|
|
165
|
+
- Collecting flow without lifecycle awareness in Android UI — subscribes even in background
|
|
166
|
+
- Using `flow { }` for hot data that should be `StateFlow` or `SharedFlow`
|
|
167
|
+
- Calling `.collect {}` directly in a composable body — use `collectAsStateWithLifecycle`
|
|
168
|
+
- Using `flatMapMerge` for search/autocomplete — use `flatMapLatest` to cancel previous
|
|
169
|
+
- Not calling `awaitClose` in `callbackFlow` — resource leak on cancellation
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Related Skills
|
|
174
|
+
|
|
175
|
+
- `stateflow` — hot state holder for UI state
|
|
176
|
+
- `sharedflow` — hot shared stream for events
|
|
177
|
+
- `coroutine` — coroutine fundamentals
|
|
178
|
+
- `lifecycle` — lifecycle-aware collection patterns
|
|
179
|
+
- `reactive-streams` — broader reactive programming concepts
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mutex-strategy
|
|
3
|
+
description: >
|
|
4
|
+
Mutex and coroutine-based locking strategies in Android.
|
|
5
|
+
Load this skill when protecting shared mutable state across coroutines,
|
|
6
|
+
preventing concurrent access to a critical section, serializing operations,
|
|
7
|
+
or avoiding race conditions in async code.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Mutex Strategy
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
`Mutex` is a coroutine-friendly mutual exclusion lock. Unlike Java's `synchronized`, it suspends rather than blocks the thread while waiting for the lock. It is the correct tool for protecting shared mutable state that multiple coroutines may access concurrently.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Use `Mutex` instead of `synchronized` in coroutine code — `synchronized` blocks the thread
|
|
20
|
+
- Keep the critical section **as short as possible** — don't do heavy work inside `withLock`
|
|
21
|
+
- Never `withLock` on the main dispatcher for blocking operations
|
|
22
|
+
- Use `Mutex` for single-resource protection — use `Semaphore` for limiting concurrency count
|
|
23
|
+
- Prefer immutable state + atomic updates where possible — avoid Mutex if you can
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Basic Mutex Usage
|
|
28
|
+
|
|
29
|
+
```kotlin
|
|
30
|
+
// ✅ Protect shared mutable state
|
|
31
|
+
class TokenCache @Inject constructor() {
|
|
32
|
+
private val mutex = Mutex()
|
|
33
|
+
private var cachedToken: String? = null
|
|
34
|
+
private var tokenExpiry: Long = 0L
|
|
35
|
+
|
|
36
|
+
suspend fun getToken(fetchToken: suspend () -> String): String {
|
|
37
|
+
mutex.withLock {
|
|
38
|
+
val now = System.currentTimeMillis()
|
|
39
|
+
if (cachedToken != null && now < tokenExpiry) {
|
|
40
|
+
return cachedToken!!
|
|
41
|
+
}
|
|
42
|
+
val newToken = fetchToken()
|
|
43
|
+
cachedToken = newToken
|
|
44
|
+
tokenExpiry = now + 3_600_000L // 1 hour
|
|
45
|
+
return newToken
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
suspend fun invalidate() {
|
|
50
|
+
mutex.withLock {
|
|
51
|
+
cachedToken = null
|
|
52
|
+
tokenExpiry = 0L
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Token Refresh with Mutex
|
|
61
|
+
|
|
62
|
+
```kotlin
|
|
63
|
+
// ✅ Prevent multiple simultaneous token refreshes
|
|
64
|
+
class AuthRepository @Inject constructor(
|
|
65
|
+
private val api: AuthApi,
|
|
66
|
+
private val tokenStorage: TokenStorage
|
|
67
|
+
) {
|
|
68
|
+
private val refreshMutex = Mutex()
|
|
69
|
+
|
|
70
|
+
suspend fun refreshToken(): String? {
|
|
71
|
+
return refreshMutex.withLock {
|
|
72
|
+
// Check if another coroutine already refreshed while we were waiting
|
|
73
|
+
val currentToken = tokenStorage.getAccessToken()
|
|
74
|
+
if (currentToken != null && !isTokenExpired(currentToken)) {
|
|
75
|
+
return@withLock currentToken
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
runCatching {
|
|
79
|
+
val response = api.refresh(tokenStorage.getRefreshToken() ?: return@withLock null)
|
|
80
|
+
tokenStorage.saveTokens(response.accessToken, response.refreshToken)
|
|
81
|
+
response.accessToken
|
|
82
|
+
}.getOrNull()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Semaphore — Limit Concurrency
|
|
91
|
+
|
|
92
|
+
```kotlin
|
|
93
|
+
// ✅ Semaphore — allow max N concurrent operations
|
|
94
|
+
class ImageProcessor @Inject constructor() {
|
|
95
|
+
// Max 3 concurrent image processing operations
|
|
96
|
+
private val semaphore = Semaphore(3)
|
|
97
|
+
|
|
98
|
+
suspend fun processImage(imageUri: Uri): Bitmap {
|
|
99
|
+
return semaphore.withPermit {
|
|
100
|
+
heavyImageProcessing(imageUri)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ✅ Rate-limited API calls
|
|
106
|
+
class ApiThrottler(maxConcurrent: Int = 5) {
|
|
107
|
+
private val semaphore = Semaphore(maxConcurrent)
|
|
108
|
+
|
|
109
|
+
suspend fun <T> throttled(call: suspend () -> T): T {
|
|
110
|
+
return semaphore.withPermit { call() }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Mutex vs Other Synchronization
|
|
118
|
+
|
|
119
|
+
| Tool | Suspends? | Use For |
|
|
120
|
+
|------|-----------|---------|
|
|
121
|
+
| `Mutex` | ✅ Yes | Single-resource exclusive access |
|
|
122
|
+
| `Semaphore` | ✅ Yes | Limit number of concurrent operations |
|
|
123
|
+
| `synchronized` | ❌ Blocks thread | Non-coroutine Java interop only |
|
|
124
|
+
| `AtomicInteger` | N/A | Simple counter without lock |
|
|
125
|
+
| `StateFlow.update` | N/A | Atomic state updates |
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Atomic Alternatives
|
|
130
|
+
|
|
131
|
+
```kotlin
|
|
132
|
+
// ✅ AtomicInteger for simple counters — no Mutex needed
|
|
133
|
+
class RequestCounter {
|
|
134
|
+
private val count = AtomicInteger(0)
|
|
135
|
+
fun increment() = count.incrementAndGet()
|
|
136
|
+
fun get() = count.get()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ✅ StateFlow.update for UI state — thread-safe without Mutex
|
|
140
|
+
private val _state = MutableStateFlow(UiState())
|
|
141
|
+
fun updateUser(user: User) {
|
|
142
|
+
_state.update { it.copy(user = user) } // atomic — no Mutex needed
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## tryLock — Non-Blocking Attempt
|
|
149
|
+
|
|
150
|
+
```kotlin
|
|
151
|
+
// ✅ tryLock — skip if already locked
|
|
152
|
+
class PeriodicSync @Inject constructor() {
|
|
153
|
+
private val syncMutex = Mutex()
|
|
154
|
+
|
|
155
|
+
suspend fun trySyncNow() {
|
|
156
|
+
if (!syncMutex.tryLock()) {
|
|
157
|
+
// sync already in progress — skip
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
performSync()
|
|
162
|
+
} finally {
|
|
163
|
+
syncMutex.unlock()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Anti-Patterns
|
|
172
|
+
|
|
173
|
+
- Using `synchronized {}` in coroutine code — blocks the thread, not coroutine-friendly
|
|
174
|
+
- Long operations inside `withLock` — holds the lock too long, blocks other coroutines
|
|
175
|
+
- Nested `withLock` calls on the same Mutex — deadlock
|
|
176
|
+
- Using Mutex for simple counter updates — use `AtomicInteger` instead
|
|
177
|
+
- Not releasing the lock in finally — use `withLock` extension, not manual lock/unlock
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Related Skills
|
|
182
|
+
- `coroutine` — coroutine fundamentals
|
|
183
|
+
- `structured-concurrency` — parent-child relationships and cancellation
|
|
184
|
+
- `synchronization-policy` — broader thread safety strategy
|
|
185
|
+
- `authentication` — token refresh with Mutex
|