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,210 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: state-restoration
|
|
3
|
+
description: >
|
|
4
|
+
Complete state restoration strategy for Android — covering UI state,
|
|
5
|
+
scroll position, form data, and Compose rememberSaveable.
|
|
6
|
+
Load this skill when deciding how to persist and restore UI state
|
|
7
|
+
across configuration changes and process death.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# State Restoration
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
State restoration ensures the user sees a consistent UI after configuration changes (rotation) or returning after process death. Android provides multiple layers of restoration — from automatic (Compose rememberSaveable) to manual (SavedStateHandle). Choosing the right layer for each piece of state is the key design decision.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Core Principles
|
|
19
|
+
|
|
20
|
+
- Match the **persistence layer** to the **state lifetime**
|
|
21
|
+
- UI-local state → `rememberSaveable`
|
|
22
|
+
- ViewModel state that must survive process death → `SavedStateHandle`
|
|
23
|
+
- Data loaded from network/DB → don't save, reload from repository
|
|
24
|
+
- Never duplicate state across multiple layers — one source of truth per state
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## State Restoration Layers
|
|
29
|
+
|
|
30
|
+
| Layer | Survives Rotation | Survives Process Death | Use For |
|
|
31
|
+
| -------------------- | ----------------- | ---------------------- | ------------------------------------- |
|
|
32
|
+
| `remember` | ❌ | ❌ | Transient UI state (animation, focus) |
|
|
33
|
+
| `rememberSaveable` | ✅ | ✅ | Local UI state (expanded, scroll) |
|
|
34
|
+
| `SavedStateHandle` | ✅ | ✅ | ViewModel state (query, selected ID) |
|
|
35
|
+
| `Room` / `DataStore` | ✅ | ✅ | Persistent data |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Compose — rememberSaveable
|
|
40
|
+
|
|
41
|
+
```kotlin
|
|
42
|
+
// ✅ Simple value — auto-saved
|
|
43
|
+
@Composable
|
|
44
|
+
fun SearchBar() {
|
|
45
|
+
var query by rememberSaveable { mutableStateOf("") }
|
|
46
|
+
// query survives rotation and process death
|
|
47
|
+
TextField(value = query, onValueChange = { query = it })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ✅ Custom type — requires Saver
|
|
51
|
+
data class FilterState(val category: String, val sort: String)
|
|
52
|
+
|
|
53
|
+
val FilterStateSaver = run {
|
|
54
|
+
val categoryKey = "category"
|
|
55
|
+
val sortKey = "sort"
|
|
56
|
+
mapSaver(
|
|
57
|
+
save = { mapOf(categoryKey to it.category, sortKey to it.sort) },
|
|
58
|
+
restore = { FilterState(it[categoryKey] as String, it[sortKey] as String) }
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Composable
|
|
63
|
+
fun FilterPanel() {
|
|
64
|
+
var filter by rememberSaveable(stateSaver = FilterStateSaver) {
|
|
65
|
+
mutableStateOf(FilterState("all", "asc"))
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ✅ List state — scroll position
|
|
70
|
+
@Composable
|
|
71
|
+
fun UserList(users: List<User>) {
|
|
72
|
+
val listState = rememberLazyListState()
|
|
73
|
+
// listState scroll position is automatically saved by rememberLazyListState
|
|
74
|
+
LazyColumn(state = listState) {
|
|
75
|
+
items(users) { UserItem(it) }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Compose — Hoist State to ViewModel
|
|
83
|
+
|
|
84
|
+
```kotlin
|
|
85
|
+
// ✅ When state needs to be shared or outlive a single composable
|
|
86
|
+
// → hoist to ViewModel backed by SavedStateHandle
|
|
87
|
+
|
|
88
|
+
@HiltViewModel
|
|
89
|
+
class ProductViewModel @Inject constructor(
|
|
90
|
+
savedStateHandle: SavedStateHandle,
|
|
91
|
+
private val repository: ProductRepository
|
|
92
|
+
) : ViewModel() {
|
|
93
|
+
|
|
94
|
+
// ✅ Search query — saved across process death
|
|
95
|
+
val query: StateFlow<String> = savedStateHandle
|
|
96
|
+
.getStateFlow("query", "")
|
|
97
|
+
|
|
98
|
+
// ✅ Products — reloaded from DB, not saved
|
|
99
|
+
val products: StateFlow<List<Product>> = query
|
|
100
|
+
.flatMapLatest { repository.search(it) }
|
|
101
|
+
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
|
|
102
|
+
|
|
103
|
+
fun onQueryChanged(q: String) { savedStateHandle["query"] = q }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@Composable
|
|
107
|
+
fun ProductScreen(viewModel: ProductViewModel = hiltViewModel()) {
|
|
108
|
+
val query by viewModel.query.collectAsStateWithLifecycle()
|
|
109
|
+
val products by viewModel.products.collectAsStateWithLifecycle()
|
|
110
|
+
|
|
111
|
+
// UI renders from ViewModel state — no local state needed
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## View System — onSaveInstanceState
|
|
118
|
+
|
|
119
|
+
```kotlin
|
|
120
|
+
// ✅ For custom Views that need to save state
|
|
121
|
+
class CounterView(context: Context) : View(context) {
|
|
122
|
+
|
|
123
|
+
private var count = 0
|
|
124
|
+
|
|
125
|
+
override fun onSaveInstanceState(): Parcelable {
|
|
126
|
+
val superState = super.onSaveInstanceState()
|
|
127
|
+
return SavedState(superState, count)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
override fun onRestoreInstanceState(state: Parcelable?) {
|
|
131
|
+
if (state is SavedState) {
|
|
132
|
+
super.onRestoreInstanceState(state.superState)
|
|
133
|
+
count = state.count
|
|
134
|
+
} else {
|
|
135
|
+
super.onRestoreInstanceState(state)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@Parcelize
|
|
140
|
+
class SavedState(val parcelable: Parcelable?, val count: Int) : BaseSavedState(parcelable)
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Restoration Decision Tree
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
Is this state local to a single composable?
|
|
150
|
+
YES → rememberSaveable
|
|
151
|
+
NO ↓
|
|
152
|
+
|
|
153
|
+
Is this state loaded from DB or network?
|
|
154
|
+
YES → reload from repository (don't save)
|
|
155
|
+
NO ↓
|
|
156
|
+
|
|
157
|
+
Is this state user input or a selected ID?
|
|
158
|
+
YES → SavedStateHandle in ViewModel
|
|
159
|
+
NO ↓
|
|
160
|
+
|
|
161
|
+
Is this critical persistent data?
|
|
162
|
+
YES → Room or DataStore
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Testing Restoration
|
|
168
|
+
|
|
169
|
+
```kotlin
|
|
170
|
+
// ✅ Test rememberSaveable with StateRestorationTester
|
|
171
|
+
@Test
|
|
172
|
+
fun searchQuery_survivesStateRestoration() {
|
|
173
|
+
val restorationTester = StateRestorationTester(composeTestRule)
|
|
174
|
+
|
|
175
|
+
restorationTester.setContent {
|
|
176
|
+
SearchBar()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
composeTestRule.onNodeWithTag("search_field").performTextInput("kotlin")
|
|
180
|
+
restorationTester.emulateSavedInstanceStateRestore()
|
|
181
|
+
composeTestRule.onNodeWithTag("search_field").assertTextEquals("kotlin")
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ✅ Test SavedStateHandle restoration
|
|
185
|
+
@Test
|
|
186
|
+
fun viewModel_restoresQueryAfterProcessDeath() {
|
|
187
|
+
val handle = SavedStateHandle(mapOf("query" to "android"))
|
|
188
|
+
val vm = ProductViewModel(handle, fakeRepository)
|
|
189
|
+
assertThat(vm.query.value).isEqualTo("android")
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Anti-Patterns
|
|
196
|
+
|
|
197
|
+
- Using `remember` instead of `rememberSaveable` for user-visible state — lost on rotation
|
|
198
|
+
- Saving large lists or bitmaps in `rememberSaveable` or `SavedStateHandle` — size limit and slow
|
|
199
|
+
- Duplicating state: saving in both ViewModel and rememberSaveable — two sources of truth
|
|
200
|
+
- Not testing restoration — the most common cause of rotation bugs
|
|
201
|
+
- Manually saving scroll position when `rememberLazyListState` does it automatically
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Related Skills
|
|
206
|
+
|
|
207
|
+
- `savedstatehandle` — SavedStateHandle in ViewModel
|
|
208
|
+
- `process-death-recovery` — surviving system-initiated kills
|
|
209
|
+
- `compose` — rememberSaveable and state hoisting in Compose
|
|
210
|
+
- `lifecycle` — when restoration occurs in the lifecycle
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bounded-context
|
|
3
|
+
description: >
|
|
4
|
+
Bounded Context pattern for Android feature organization.
|
|
5
|
+
Load this skill when defining feature boundaries, deciding what belongs
|
|
6
|
+
in a shared domain vs a feature-specific domain, preventing model pollution
|
|
7
|
+
across features, or designing the conceptual boundaries of a module.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Bounded Context
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
A Bounded Context defines the boundary within which a domain model is consistent and valid. In Android, each feature module (or major feature area) is a Bounded Context — it owns its own models, rules, and language. The same real-world concept (e.g. "User") may exist in multiple contexts with different shapes and responsibilities.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Core Principles
|
|
19
|
+
|
|
20
|
+
- Each context owns its **own model** — never share a model across contexts if they evolve differently
|
|
21
|
+
- **Context map** makes inter-context relationships explicit
|
|
22
|
+
- Cross-context communication happens through **well-defined interfaces** — not direct model sharing
|
|
23
|
+
- The same word can mean different things in different contexts — that's intentional
|
|
24
|
+
- Shared kernel only for concepts that are **truly identical** across contexts
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Example: "User" in Multiple Contexts
|
|
29
|
+
|
|
30
|
+
```kotlin
|
|
31
|
+
// ❌ Tempting but wrong — one "User" class used everywhere
|
|
32
|
+
data class User(
|
|
33
|
+
val id: String,
|
|
34
|
+
val name: String,
|
|
35
|
+
val email: String,
|
|
36
|
+
val role: UserRole,
|
|
37
|
+
val shippingAddresses: List<Address>, // only needed in Orders context
|
|
38
|
+
val orderHistory: List<OrderSummary>, // only needed in Orders context
|
|
39
|
+
val savedCards: List<PaymentCard>, // only needed in Payment context
|
|
40
|
+
val notificationPreferences: NotificationPrefs // only needed in Notifications
|
|
41
|
+
)
|
|
42
|
+
// This model becomes a dumping ground — bloated and hard to maintain
|
|
43
|
+
|
|
44
|
+
// ✅ Correct — each context defines its own model
|
|
45
|
+
// Auth context
|
|
46
|
+
data class AuthUser(
|
|
47
|
+
val id: String,
|
|
48
|
+
val email: String,
|
|
49
|
+
val role: UserRole,
|
|
50
|
+
val sessionToken: String
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
// Orders context
|
|
54
|
+
data class OrderCustomer(
|
|
55
|
+
val id: String,
|
|
56
|
+
val displayName: String,
|
|
57
|
+
val defaultShippingAddress: Address?
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
// Profile context
|
|
61
|
+
data class UserProfile(
|
|
62
|
+
val id: String,
|
|
63
|
+
val fullName: String,
|
|
64
|
+
val avatarUrl: String?,
|
|
65
|
+
val bio: String?
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
// Notifications context
|
|
69
|
+
data class NotificationRecipient(
|
|
70
|
+
val userId: String,
|
|
71
|
+
val pushToken: String?,
|
|
72
|
+
val preferences: NotificationPreferences
|
|
73
|
+
)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Context Map
|
|
79
|
+
|
|
80
|
+
```kotlin
|
|
81
|
+
// ✅ Explicit translation between contexts
|
|
82
|
+
// When Orders needs user info, it requests it through an interface
|
|
83
|
+
|
|
84
|
+
// Orders context defines what it needs
|
|
85
|
+
interface CustomerInfoProvider {
|
|
86
|
+
suspend fun getCustomer(userId: String): Result<OrderCustomer>
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// The implementation in :app or a shared module translates
|
|
90
|
+
class CustomerInfoProviderImpl @Inject constructor(
|
|
91
|
+
private val userRepository: UserRepository // Auth/Profile context
|
|
92
|
+
) : CustomerInfoProvider {
|
|
93
|
+
|
|
94
|
+
override suspend fun getCustomer(userId: String): Result<OrderCustomer> =
|
|
95
|
+
userRepository.getUser(userId).map { user ->
|
|
96
|
+
OrderCustomer(
|
|
97
|
+
id = user.id,
|
|
98
|
+
displayName = user.name,
|
|
99
|
+
defaultShippingAddress = user.addresses.firstOrNull { it.isDefault }
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Feature Module as Bounded Context
|
|
108
|
+
|
|
109
|
+
```kotlin
|
|
110
|
+
// ✅ :feature:orders owns its domain model
|
|
111
|
+
// feature/orders/domain/model/
|
|
112
|
+
data class Order(val id: String, val customerId: String, val items: List<OrderItem>, ...)
|
|
113
|
+
data class OrderItem(val productId: String, val name: String, val quantity: Int, ...)
|
|
114
|
+
data class OrderSummary(val id: String, val status: OrderStatus, val totalAmount: Money)
|
|
115
|
+
|
|
116
|
+
// ✅ :feature:catalog owns its domain model — Product means something different here
|
|
117
|
+
// feature/catalog/domain/model/
|
|
118
|
+
data class Product(val id: String, val name: String, val description: String, val price: Money, ...)
|
|
119
|
+
data class Category(val id: String, val name: String, val products: List<ProductSummary>)
|
|
120
|
+
|
|
121
|
+
// ✅ :feature:cart owns its domain model — Product in cart context = CartItem
|
|
122
|
+
// feature/cart/domain/model/
|
|
123
|
+
data class Cart(val id: String, val items: List<CartItem>)
|
|
124
|
+
data class CartItem(val productId: String, val productName: String, val unitPrice: Money, val quantity: Int)
|
|
125
|
+
// Note: CartItem has productName denormalized — it owns what it needs
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Shared Kernel
|
|
131
|
+
|
|
132
|
+
```kotlin
|
|
133
|
+
// ✅ Truly shared concepts live in :core:domain
|
|
134
|
+
// These are stable and identical across all contexts
|
|
135
|
+
|
|
136
|
+
// :core:domain/model/shared/
|
|
137
|
+
@JvmInline value class UserId(val value: String)
|
|
138
|
+
@JvmInline value class ProductId(val value: String)
|
|
139
|
+
data class Money(val amount: Long, val currency: Currency)
|
|
140
|
+
enum class Currency { IRR, USD, EUR }
|
|
141
|
+
|
|
142
|
+
// All features import these from :core:domain — not from each other
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Anti-Corruption Layer
|
|
148
|
+
|
|
149
|
+
```kotlin
|
|
150
|
+
// ✅ Translate external API models without polluting domain
|
|
151
|
+
// When integrating a payment gateway, don't let its model bleed in
|
|
152
|
+
|
|
153
|
+
// External (Zarinpal, Stripe, etc.)
|
|
154
|
+
data class PaymentGatewayResponse(
|
|
155
|
+
val status: Int,
|
|
156
|
+
val ref_id: String?,
|
|
157
|
+
val error_code: String?
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
// Anti-corruption layer — translates to domain language
|
|
161
|
+
class PaymentGatewayAdapter @Inject constructor(
|
|
162
|
+
private val gateway: PaymentGatewayService
|
|
163
|
+
) {
|
|
164
|
+
suspend fun processPayment(request: PaymentRequest): Result<PaymentConfirmation> =
|
|
165
|
+
runCatching {
|
|
166
|
+
val response = gateway.pay(request.toGatewayRequest())
|
|
167
|
+
when (response.status) {
|
|
168
|
+
100 -> PaymentConfirmation(
|
|
169
|
+
transactionId = response.ref_id ?: error("Missing ref_id"),
|
|
170
|
+
amount = request.amount
|
|
171
|
+
)
|
|
172
|
+
else -> throw PaymentException(response.error_code ?: "Unknown error")
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## When to Split vs Share
|
|
181
|
+
|
|
182
|
+
| Signal | Action |
|
|
183
|
+
| ------------------------------------------------------ | ------------------------------------------------ |
|
|
184
|
+
| Same concept evolves differently in two features | Split into two models |
|
|
185
|
+
| Same concept has identical fields and rules everywhere | Put in shared kernel |
|
|
186
|
+
| Feature A needs data from Feature B | Define interface in A, implement in :app |
|
|
187
|
+
| Two features share a DTO from the same API endpoint | Split into feature-specific domain models anyway |
|
|
188
|
+
| Changing one feature's model breaks another feature | Context boundary is violated — split |
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Anti-Patterns
|
|
193
|
+
|
|
194
|
+
- One giant `User` class used by auth, profile, orders, notifications — split by context
|
|
195
|
+
- Feature modules importing domain models from other feature modules — route through :core or interfaces
|
|
196
|
+
- Shared kernel growing to include feature-specific concepts — keep shared kernel minimal and stable
|
|
197
|
+
- Skipping the translation layer — directly mapping API DTOs to domain models used by multiple features
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Related Skills
|
|
202
|
+
|
|
203
|
+
- `modularization` — module boundaries that enforce context boundaries
|
|
204
|
+
- `feature-isolation` — isolating feature implementation details
|
|
205
|
+
- `domain-modeling` — modeling within a bounded context
|
|
206
|
+
- `clean-architecture` — layer boundaries within a context
|
|
207
|
+
- `repository-pattern` — repository interfaces as context boundaries
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: clean-architecture
|
|
3
|
+
description: >
|
|
4
|
+
Clean Architecture layering for Android projects.
|
|
5
|
+
Load this skill when designing layer boundaries, defining dependencies between
|
|
6
|
+
layers, structuring packages, or deciding where logic belongs (UI vs Domain vs Data).
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Clean Architecture
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
Clean Architecture organizes code into three concentric layers: **Presentation**, **Domain**, and **Data**. Dependencies flow inward only — outer layers depend on inner layers, never the reverse. The Domain layer is pure Kotlin with no Android dependencies.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Layer Structure
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
app/
|
|
21
|
+
├── presentation/ # Compose UI, ViewModels, UiState, UiEvent
|
|
22
|
+
│ └── feature/
|
|
23
|
+
│ ├── FeatureScreen.kt
|
|
24
|
+
│ ├── FeatureViewModel.kt
|
|
25
|
+
│ └── FeatureUiState.kt
|
|
26
|
+
├── domain/ # Pure Kotlin — UseCases, Entities, Repository interfaces
|
|
27
|
+
│ ├── model/
|
|
28
|
+
│ │ └── User.kt
|
|
29
|
+
│ ├── repository/
|
|
30
|
+
│ │ └── UserRepository.kt # interface only
|
|
31
|
+
│ └── usecase/
|
|
32
|
+
│ └── GetUserUseCase.kt
|
|
33
|
+
└── data/ # Repository implementations, Room, Retrofit, DataStore
|
|
34
|
+
├── repository/
|
|
35
|
+
│ └── UserRepositoryImpl.kt
|
|
36
|
+
├── local/
|
|
37
|
+
│ ├── UserDao.kt
|
|
38
|
+
│ └── UserEntity.kt
|
|
39
|
+
├── remote/
|
|
40
|
+
│ ├── UserApiService.kt
|
|
41
|
+
│ └── UserDto.kt
|
|
42
|
+
└── mapper/
|
|
43
|
+
└── UserMapper.kt
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Dependency Rule
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
Presentation → Domain ← Data
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- **Domain** depends on nothing (no Android, no Room, no Retrofit)
|
|
55
|
+
- **Presentation** depends on Domain (uses UseCases and models)
|
|
56
|
+
- **Data** depends on Domain (implements Repository interfaces)
|
|
57
|
+
- **Data** never imports from **Presentation**
|
|
58
|
+
- **Presentation** never imports from **Data** directly
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Domain Layer
|
|
63
|
+
|
|
64
|
+
```kotlin
|
|
65
|
+
// ✅ Entity — pure Kotlin data class, no annotations
|
|
66
|
+
data class User(
|
|
67
|
+
val id: String,
|
|
68
|
+
val name: String,
|
|
69
|
+
val email: String,
|
|
70
|
+
val role: UserRole
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
enum class UserRole { ADMIN, MEMBER, GUEST }
|
|
74
|
+
|
|
75
|
+
// ✅ Repository interface — defined in Domain, implemented in Data
|
|
76
|
+
interface UserRepository {
|
|
77
|
+
suspend fun getUser(id: String): Result<User>
|
|
78
|
+
suspend fun getUsers(): Result<List<User>>
|
|
79
|
+
suspend fun saveUser(user: User): Result<Unit>
|
|
80
|
+
fun observeUsers(): Flow<List<User>>
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ✅ UseCase — single responsibility, pure logic
|
|
84
|
+
class GetUserUseCase @Inject constructor(
|
|
85
|
+
private val repository: UserRepository
|
|
86
|
+
) {
|
|
87
|
+
suspend operator fun invoke(id: String): Result<User> =
|
|
88
|
+
repository.getUser(id)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class GetActiveUsersUseCase @Inject constructor(
|
|
92
|
+
private val repository: UserRepository
|
|
93
|
+
) {
|
|
94
|
+
operator fun invoke(): Flow<List<User>> =
|
|
95
|
+
repository.observeUsers().map { users ->
|
|
96
|
+
users.filter { it.role != UserRole.GUEST }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Data Layer
|
|
104
|
+
|
|
105
|
+
```kotlin
|
|
106
|
+
// ✅ Entity — Room-specific, isolated in data layer
|
|
107
|
+
@Entity(tableName = "users")
|
|
108
|
+
data class UserEntity(
|
|
109
|
+
@PrimaryKey val id: String,
|
|
110
|
+
val name: String,
|
|
111
|
+
val email: String,
|
|
112
|
+
val role: String
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
// ✅ DTO — API response model
|
|
116
|
+
data class UserDto(
|
|
117
|
+
@SerialName("id") val id: String,
|
|
118
|
+
@SerialName("full_name") val name: String,
|
|
119
|
+
@SerialName("email_address") val email: String,
|
|
120
|
+
@SerialName("role") val role: String
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
// ✅ Mapper — converts between layers
|
|
124
|
+
object UserMapper {
|
|
125
|
+
fun UserDto.toDomain(): User = User(
|
|
126
|
+
id = id,
|
|
127
|
+
name = name,
|
|
128
|
+
email = email,
|
|
129
|
+
role = UserRole.valueOf(role.uppercase())
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
fun UserEntity.toDomain(): User = User(
|
|
133
|
+
id = id,
|
|
134
|
+
name = name,
|
|
135
|
+
email = email,
|
|
136
|
+
role = UserRole.valueOf(role.uppercase())
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
fun User.toEntity(): UserEntity = UserEntity(
|
|
140
|
+
id = id,
|
|
141
|
+
name = name,
|
|
142
|
+
email = email,
|
|
143
|
+
role = role.name.lowercase()
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ✅ Repository implementation — in Data, implements Domain interface
|
|
148
|
+
class UserRepositoryImpl @Inject constructor(
|
|
149
|
+
private val api: UserApiService,
|
|
150
|
+
private val dao: UserDao
|
|
151
|
+
) : UserRepository {
|
|
152
|
+
|
|
153
|
+
override suspend fun getUser(id: String): Result<User> = runCatching {
|
|
154
|
+
val entity = dao.getUserById(id)
|
|
155
|
+
entity?.toDomain() ?: run {
|
|
156
|
+
val dto = api.getUser(id)
|
|
157
|
+
val domain = dto.toDomain()
|
|
158
|
+
dao.insert(domain.toEntity())
|
|
159
|
+
domain
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
override fun observeUsers(): Flow<List<User>> =
|
|
164
|
+
dao.observeAll().map { entities -> entities.map { it.toDomain() } }
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Presentation Layer
|
|
171
|
+
|
|
172
|
+
```kotlin
|
|
173
|
+
// ✅ ViewModel uses UseCases — never Repository directly
|
|
174
|
+
@HiltViewModel
|
|
175
|
+
class UserListViewModel @Inject constructor(
|
|
176
|
+
private val getActiveUsersUseCase: GetActiveUsersUseCase
|
|
177
|
+
) : ViewModel() {
|
|
178
|
+
|
|
179
|
+
val state: StateFlow<UserListUiState> =
|
|
180
|
+
getActiveUsersUseCase()
|
|
181
|
+
.map { UserListUiState.Success(it) }
|
|
182
|
+
.catch { emit(UserListUiState.Error(it.message ?: "Error")) }
|
|
183
|
+
.stateIn(
|
|
184
|
+
scope = viewModelScope,
|
|
185
|
+
started = SharingStarted.WhileSubscribed(5_000),
|
|
186
|
+
initialValue = UserListUiState.Loading
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Hilt Binding
|
|
194
|
+
|
|
195
|
+
```kotlin
|
|
196
|
+
// ✅ Bind interface to implementation in data module
|
|
197
|
+
@Module
|
|
198
|
+
@InstallIn(SingletonComponent::class)
|
|
199
|
+
abstract class RepositoryModule {
|
|
200
|
+
|
|
201
|
+
@Binds
|
|
202
|
+
@Singleton
|
|
203
|
+
abstract fun bindUserRepository(
|
|
204
|
+
impl: UserRepositoryImpl
|
|
205
|
+
): UserRepository
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Anti-Patterns
|
|
212
|
+
|
|
213
|
+
- Importing `android.*` or `androidx.*` in Domain layer — Domain must be pure Kotlin
|
|
214
|
+
- ViewModel calling Repository directly — always go through a UseCase
|
|
215
|
+
- Presentation importing Data classes (Entity, DTO) — only Domain models cross into Presentation
|
|
216
|
+
- UseCase containing more than one responsibility — one UseCase, one operation
|
|
217
|
+
- Repository interface defined in Data layer — interfaces belong in Domain
|
|
218
|
+
- Skipping the mapper — never pass DTO or Entity into Domain or Presentation
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Related Skills
|
|
223
|
+
|
|
224
|
+
- `mvvm` — ViewModel and UI state structure
|
|
225
|
+
- `use-case-design` — designing UseCases in Domain layer
|
|
226
|
+
- `repository-pattern` — Repository interface and implementation patterns
|
|
227
|
+
- `dto-mapping` — mapping between layers
|
|
228
|
+
- `hilt` — dependency injection wiring
|
|
229
|
+
- `domain-modeling` — modeling domain entities
|