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,224 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mvi
|
|
3
|
+
description: >
|
|
4
|
+
Model-View-Intent pattern for Android with Jetpack Compose.
|
|
5
|
+
Load this skill when implementing strict unidirectional data flow,
|
|
6
|
+
modeling all user interactions as Intents, using a single immutable
|
|
7
|
+
state per screen, or managing side effects as Effects separate from state.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# MVI
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
MVI enforces strict unidirectional data flow: the View emits **Intents**, the ViewModel processes them and produces a new immutable **State**, and one-time **Effects** handle side effects like navigation or toasts. Unlike MVVM, all user interactions are modeled as a sealed `Intent` class — nothing is called imperatively.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Core Principles
|
|
19
|
+
|
|
20
|
+
- **One immutable State** per screen — never partial updates
|
|
21
|
+
- All user actions are **Intents** — sealed class, no direct function calls from UI
|
|
22
|
+
- **Effects** are one-time events (navigation, toast) — separate from State
|
|
23
|
+
- State transitions are **pure** — `reduce(currentState, intent) → newState`
|
|
24
|
+
- ViewModel processes intents sequentially — no race conditions on state
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Contract (State + Intent + Effect)
|
|
29
|
+
|
|
30
|
+
```kotlin
|
|
31
|
+
// ✅ Define the full screen contract in one place
|
|
32
|
+
object UserListContract {
|
|
33
|
+
|
|
34
|
+
data class State(
|
|
35
|
+
val isLoading: Boolean = false,
|
|
36
|
+
val users: List<User> = emptyList(),
|
|
37
|
+
val error: String? = null,
|
|
38
|
+
val searchQuery: String = ""
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
sealed interface Intent {
|
|
42
|
+
data object LoadUsers : Intent
|
|
43
|
+
data class SearchChanged(val query: String) : Intent
|
|
44
|
+
data class UserClicked(val userId: String) : Intent
|
|
45
|
+
data class DeleteUser(val userId: String) : Intent
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
sealed interface Effect {
|
|
49
|
+
data class NavigateToDetail(val userId: String) : Effect
|
|
50
|
+
data class ShowSnackbar(val message: String) : Effect
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## ViewModel
|
|
58
|
+
|
|
59
|
+
```kotlin
|
|
60
|
+
// ✅ MVI ViewModel — processes intents, emits state and effects
|
|
61
|
+
@HiltViewModel
|
|
62
|
+
class UserListViewModel @Inject constructor(
|
|
63
|
+
private val getUsersUseCase: GetUsersUseCase,
|
|
64
|
+
private val deleteUserUseCase: DeleteUserUseCase
|
|
65
|
+
) : ViewModel() {
|
|
66
|
+
|
|
67
|
+
private val _state = MutableStateFlow(UserListContract.State())
|
|
68
|
+
val state: StateFlow<UserListContract.State> = _state.asStateFlow()
|
|
69
|
+
|
|
70
|
+
private val _effects = Channel<UserListContract.Effect>(Channel.BUFFERED)
|
|
71
|
+
val effects: Flow<UserListContract.Effect> = _effects.receiveAsFlow()
|
|
72
|
+
|
|
73
|
+
fun handleIntent(intent: UserListContract.Intent) {
|
|
74
|
+
when (intent) {
|
|
75
|
+
is UserListContract.Intent.LoadUsers -> loadUsers()
|
|
76
|
+
is UserListContract.Intent.SearchChanged -> onSearchChanged(intent.query)
|
|
77
|
+
is UserListContract.Intent.UserClicked -> onUserClicked(intent.userId)
|
|
78
|
+
is UserListContract.Intent.DeleteUser -> deleteUser(intent.userId)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private fun loadUsers() {
|
|
83
|
+
viewModelScope.launch {
|
|
84
|
+
_state.update { it.copy(isLoading = true, error = null) }
|
|
85
|
+
getUsersUseCase().fold(
|
|
86
|
+
onSuccess = { users ->
|
|
87
|
+
_state.update { it.copy(isLoading = false, users = users) }
|
|
88
|
+
},
|
|
89
|
+
onFailure = { error ->
|
|
90
|
+
_state.update { it.copy(isLoading = false, error = error.message) }
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private fun onSearchChanged(query: String) {
|
|
97
|
+
_state.update { it.copy(searchQuery = query) }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private fun onUserClicked(userId: String) {
|
|
101
|
+
viewModelScope.launch {
|
|
102
|
+
_effects.send(UserListContract.Effect.NavigateToDetail(userId))
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private fun deleteUser(userId: String) {
|
|
107
|
+
viewModelScope.launch {
|
|
108
|
+
deleteUserUseCase(userId).fold(
|
|
109
|
+
onSuccess = {
|
|
110
|
+
loadUsers()
|
|
111
|
+
_effects.send(UserListContract.Effect.ShowSnackbar("User deleted"))
|
|
112
|
+
},
|
|
113
|
+
onFailure = {
|
|
114
|
+
_effects.send(UserListContract.Effect.ShowSnackbar("Delete failed"))
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Composable
|
|
125
|
+
|
|
126
|
+
```kotlin
|
|
127
|
+
// ✅ View sends intents — never calls ViewModel functions directly
|
|
128
|
+
@Composable
|
|
129
|
+
fun UserListScreen(
|
|
130
|
+
onNavigateToDetail: (String) -> Unit,
|
|
131
|
+
viewModel: UserListViewModel = hiltViewModel()
|
|
132
|
+
) {
|
|
133
|
+
val state by viewModel.state.collectAsStateWithLifecycle()
|
|
134
|
+
val snackbarHostState = remember { SnackbarHostState() }
|
|
135
|
+
|
|
136
|
+
// ✅ Collect one-time effects
|
|
137
|
+
LaunchedEffect(Unit) {
|
|
138
|
+
viewModel.effects.collect { effect ->
|
|
139
|
+
when (effect) {
|
|
140
|
+
is UserListContract.Effect.NavigateToDetail ->
|
|
141
|
+
onNavigateToDetail(effect.userId)
|
|
142
|
+
is UserListContract.Effect.ShowSnackbar ->
|
|
143
|
+
snackbarHostState.showSnackbar(effect.message)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ✅ Trigger initial load via intent
|
|
149
|
+
LaunchedEffect(Unit) {
|
|
150
|
+
viewModel.handleIntent(UserListContract.Intent.LoadUsers)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) { padding ->
|
|
154
|
+
UserListContent(
|
|
155
|
+
state = state,
|
|
156
|
+
onIntent = viewModel::handleIntent,
|
|
157
|
+
modifier = Modifier.padding(padding)
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ✅ Content composable receives state and onIntent — no ViewModel reference
|
|
163
|
+
@Composable
|
|
164
|
+
private fun UserListContent(
|
|
165
|
+
state: UserListContract.State,
|
|
166
|
+
onIntent: (UserListContract.Intent) -> Unit,
|
|
167
|
+
modifier: Modifier = Modifier
|
|
168
|
+
) {
|
|
169
|
+
Column(modifier = modifier) {
|
|
170
|
+
SearchBar(
|
|
171
|
+
query = state.searchQuery,
|
|
172
|
+
onQueryChange = { onIntent(UserListContract.Intent.SearchChanged(it)) }
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
when {
|
|
176
|
+
state.isLoading -> LoadingState()
|
|
177
|
+
state.error != null -> ErrorState(
|
|
178
|
+
message = state.error,
|
|
179
|
+
onRetry = { onIntent(UserListContract.Intent.LoadUsers) }
|
|
180
|
+
)
|
|
181
|
+
state.users.isEmpty() -> EmptyState()
|
|
182
|
+
else -> UserList(
|
|
183
|
+
users = state.users,
|
|
184
|
+
onUserClick = { onIntent(UserListContract.Intent.UserClicked(it)) },
|
|
185
|
+
onDeleteClick = { onIntent(UserListContract.Intent.DeleteUser(it)) }
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## MVI vs MVVM — When to Use Each
|
|
195
|
+
|
|
196
|
+
| Concern | MVVM | MVI |
|
|
197
|
+
| -------------- | ---------------------------- | -------------------------------------- |
|
|
198
|
+
| Complexity | Simple to medium screens | Complex screens with many interactions |
|
|
199
|
+
| State tracing | Multiple StateFlows possible | Single immutable State — easy to trace |
|
|
200
|
+
| Testability | Test individual functions | Test intent → state transitions |
|
|
201
|
+
| Boilerplate | Less | More (Contract class) |
|
|
202
|
+
| Predictability | Good | Excellent |
|
|
203
|
+
|
|
204
|
+
Use **MVVM** as the default. Reach for **MVI** when a screen has many concurrent interactions and state bugs are hard to trace.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Anti-Patterns
|
|
209
|
+
|
|
210
|
+
- Calling ViewModel functions directly from UI — use `handleIntent()`
|
|
211
|
+
- Mutable state fields inside the State data class — keep State fully immutable
|
|
212
|
+
- Sending navigation as State change — navigation is an Effect, not State
|
|
213
|
+
- Multiple state flows per screen — one `StateFlow<State>` only
|
|
214
|
+
- Processing intents with side effects inside `reduce()` — `reduce()` must be pure if used
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Related Skills
|
|
219
|
+
|
|
220
|
+
- `mvvm` — simpler alternative for straightforward screens
|
|
221
|
+
- `unidirectional-data-flow` — the underlying pattern MVI is built on
|
|
222
|
+
- `state-management` — state patterns and tools
|
|
223
|
+
- `side-effect-management` — handling effects correctly
|
|
224
|
+
- `compose` — collecting state in Compose UI
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mvvm
|
|
3
|
+
description: >
|
|
4
|
+
Model-View-ViewModel pattern for Android with Jetpack Compose.
|
|
5
|
+
Load this skill when structuring a screen with ViewModel, defining
|
|
6
|
+
UI state, handling user events, managing one-time side effects,
|
|
7
|
+
or wiring ViewModel to Compose UI.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# MVVM
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
MVVM separates UI (View) from business logic (ViewModel) via observable state. In Compose, the ViewModel exposes `StateFlow` for UI state and a `Channel` or `SharedFlow` for one-time events. The composable observes state and delegates all actions to the ViewModel.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Core Principles
|
|
19
|
+
|
|
20
|
+
- ViewModel exposes **one UI state** as `StateFlow` — never multiple disconnected flags
|
|
21
|
+
- One-time events (navigation, toasts) go through a `Channel` — not `StateFlow`
|
|
22
|
+
- Composables are **stateless** — they receive state and emit events only
|
|
23
|
+
- ViewModel has **no reference** to View, Context, or composable
|
|
24
|
+
- User actions are modeled as a sealed `UiEvent` class — not individual functions for simple screens
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## UI State Model
|
|
29
|
+
|
|
30
|
+
```kotlin
|
|
31
|
+
// ✅ Single sealed state per screen
|
|
32
|
+
sealed interface UserDetailUiState {
|
|
33
|
+
data object Loading : UserDetailUiState
|
|
34
|
+
data class Success(val user: User) : UserDetailUiState
|
|
35
|
+
data class Error(val message: String) : UserDetailUiState
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ✅ Or data class for screens with multiple fields
|
|
39
|
+
data class UserListUiState(
|
|
40
|
+
val isLoading: Boolean = false,
|
|
41
|
+
val users: List<User> = emptyList(),
|
|
42
|
+
val error: String? = null,
|
|
43
|
+
val searchQuery: String = ""
|
|
44
|
+
)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## One-Time Events
|
|
50
|
+
|
|
51
|
+
```kotlin
|
|
52
|
+
// ✅ Events that happen once — not persistent state
|
|
53
|
+
sealed interface UserDetailEvent {
|
|
54
|
+
data object NavigateBack : UserDetailEvent
|
|
55
|
+
data class ShowToast(val message: String) : UserDetailEvent
|
|
56
|
+
data class NavigateToEdit(val userId: String) : UserDetailEvent
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## ViewModel
|
|
63
|
+
|
|
64
|
+
```kotlin
|
|
65
|
+
// ✅ Standard ViewModel structure
|
|
66
|
+
@HiltViewModel
|
|
67
|
+
class UserDetailViewModel @Inject constructor(
|
|
68
|
+
savedStateHandle: SavedStateHandle,
|
|
69
|
+
private val getUserUseCase: GetUserUseCase,
|
|
70
|
+
private val deleteUserUseCase: DeleteUserUseCase
|
|
71
|
+
) : ViewModel() {
|
|
72
|
+
|
|
73
|
+
private val userId: String = checkNotNull(savedStateHandle["userId"])
|
|
74
|
+
|
|
75
|
+
// UI state
|
|
76
|
+
private val _state = MutableStateFlow<UserDetailUiState>(UserDetailUiState.Loading)
|
|
77
|
+
val state: StateFlow<UserDetailUiState> = _state.asStateFlow()
|
|
78
|
+
|
|
79
|
+
// One-time events
|
|
80
|
+
private val _events = Channel<UserDetailEvent>(Channel.BUFFERED)
|
|
81
|
+
val events: Flow<UserDetailEvent> = _events.receiveAsFlow()
|
|
82
|
+
|
|
83
|
+
init {
|
|
84
|
+
loadUser()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fun loadUser() {
|
|
88
|
+
viewModelScope.launch {
|
|
89
|
+
_state.value = UserDetailUiState.Loading
|
|
90
|
+
getUserUseCase(userId).fold(
|
|
91
|
+
onSuccess = { _state.value = UserDetailUiState.Success(it) },
|
|
92
|
+
onFailure = { _state.value = UserDetailUiState.Error(it.message ?: "Error") }
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
fun onDeleteClick() {
|
|
98
|
+
viewModelScope.launch {
|
|
99
|
+
deleteUserUseCase(userId).fold(
|
|
100
|
+
onSuccess = { _events.send(UserDetailEvent.NavigateBack) },
|
|
101
|
+
onFailure = { _events.send(UserDetailEvent.ShowToast("Delete failed")) }
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fun onEditClick() {
|
|
107
|
+
viewModelScope.launch {
|
|
108
|
+
_events.send(UserDetailEvent.NavigateToEdit(userId))
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Composable
|
|
117
|
+
|
|
118
|
+
```kotlin
|
|
119
|
+
// ✅ Stateless composable — observes state, delegates actions
|
|
120
|
+
@Composable
|
|
121
|
+
fun UserDetailScreen(
|
|
122
|
+
onNavigateBack: () -> Unit,
|
|
123
|
+
onNavigateToEdit: (String) -> Unit,
|
|
124
|
+
viewModel: UserDetailViewModel = hiltViewModel()
|
|
125
|
+
) {
|
|
126
|
+
val state by viewModel.state.collectAsStateWithLifecycle()
|
|
127
|
+
val context = LocalContext.current
|
|
128
|
+
|
|
129
|
+
// ✅ Collect one-time events
|
|
130
|
+
LaunchedEffect(Unit) {
|
|
131
|
+
viewModel.events.collect { event ->
|
|
132
|
+
when (event) {
|
|
133
|
+
is UserDetailEvent.NavigateBack -> onNavigateBack()
|
|
134
|
+
is UserDetailEvent.ShowToast -> Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
|
|
135
|
+
is UserDetailEvent.NavigateToEdit -> onNavigateToEdit(event.userId)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
UserDetailContent(
|
|
141
|
+
state = state,
|
|
142
|
+
onDeleteClick = viewModel::onDeleteClick,
|
|
143
|
+
onEditClick = viewModel::onEditClick,
|
|
144
|
+
onRetryClick = viewModel::loadUser
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ✅ Pure content composable — no ViewModel reference
|
|
149
|
+
@Composable
|
|
150
|
+
private fun UserDetailContent(
|
|
151
|
+
state: UserDetailUiState,
|
|
152
|
+
onDeleteClick: () -> Unit,
|
|
153
|
+
onEditClick: () -> Unit,
|
|
154
|
+
onRetryClick: () -> Unit
|
|
155
|
+
) {
|
|
156
|
+
when (state) {
|
|
157
|
+
is UserDetailUiState.Loading -> LoadingState()
|
|
158
|
+
is UserDetailUiState.Success -> UserDetailBody(state.user, onDeleteClick, onEditClick)
|
|
159
|
+
is UserDetailUiState.Error -> ErrorState(state.message, onRetryClick)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Shared ViewModel (Between Screens)
|
|
167
|
+
|
|
168
|
+
```kotlin
|
|
169
|
+
// ✅ Shared ViewModel scoped to NavBackStackEntry
|
|
170
|
+
@Composable
|
|
171
|
+
fun ParentScreen(navController: NavController) {
|
|
172
|
+
val parentEntry = remember(navController) {
|
|
173
|
+
navController.getBackStackEntry(ParentRoute)
|
|
174
|
+
}
|
|
175
|
+
val sharedViewModel: SharedViewModel = hiltViewModel(parentEntry)
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Anti-Patterns
|
|
182
|
+
|
|
183
|
+
- Multiple `StateFlow` properties for one screen — use a single state class
|
|
184
|
+
- Using `StateFlow` for navigation events — use `Channel` instead (won't replay)
|
|
185
|
+
- Passing `Context` into ViewModel — use `ApplicationContext` only when necessary via `@ApplicationContext`
|
|
186
|
+
- Calling `viewModel.someFlow.collect {}` without `collectAsStateWithLifecycle` in Compose
|
|
187
|
+
- Business logic inside composables — belongs in ViewModel or use case
|
|
188
|
+
- ViewModel referencing a specific composable or Fragment
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Related Skills
|
|
193
|
+
|
|
194
|
+
- `clean-architecture` — layer structure ViewModel sits within
|
|
195
|
+
- `state-management` — advanced state patterns
|
|
196
|
+
- `side-effect-management` — handling side effects from ViewModel
|
|
197
|
+
- `use-case-design` — what the ViewModel delegates to
|
|
198
|
+
- `savedstatehandle` — persisting ViewModel state across process death
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: modularization
|
|
3
|
+
description: >
|
|
4
|
+
App modularization strategy for Android projects.
|
|
5
|
+
Load this skill when deciding how to split a project into modules,
|
|
6
|
+
defining module types and responsibilities, managing module dependencies,
|
|
7
|
+
or planning build performance improvements through modularization.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Modularization
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
Modularization splits the app into independent Gradle modules. Each module has a single responsibility, clear API boundaries, and explicit dependencies. This improves build times (parallel compilation, build cache), enforces architecture boundaries, and enables code reuse across features.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Modules depend **inward** — feature modules never depend on other feature modules
|
|
20
|
+
- Each module exposes a **minimal public API** — use `internal` for implementation details
|
|
21
|
+
- **No circular dependencies** between modules
|
|
22
|
+
- Core/shared modules are **stable** — feature modules are volatile
|
|
23
|
+
- Module boundaries enforce **architecture layers** — a `data` module cannot import a `ui` module
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Module Types
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
:app — entry point, wires everything together
|
|
31
|
+
:core:ui — shared Compose components, theme, design system
|
|
32
|
+
:core:domain — shared domain models and interfaces
|
|
33
|
+
:core:data — shared repository implementations, database, network
|
|
34
|
+
:core:common — utilities, extensions, base classes
|
|
35
|
+
:feature:users — users feature (presentation + domain + data for this feature)
|
|
36
|
+
:feature:products — products feature
|
|
37
|
+
:feature:settings — settings feature
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Recommended Structure
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
app/
|
|
46
|
+
├── app/ # :app — Application, MainActivity, DI root
|
|
47
|
+
├── core/
|
|
48
|
+
│ ├── ui/ # :core:ui — Theme, shared components
|
|
49
|
+
│ ├── domain/ # :core:domain — Shared models, base UseCases
|
|
50
|
+
│ ├── data/ # :core:data — Network, DB, shared repos
|
|
51
|
+
│ ├── common/ # :core:common — Extensions, utils, Result
|
|
52
|
+
│ └── testing/ # :core:testing — Fakes, test utilities
|
|
53
|
+
└── feature/
|
|
54
|
+
├── users/ # :feature:users
|
|
55
|
+
│ ├── src/main/
|
|
56
|
+
│ │ ├── presentation/ # Screen, ViewModel, UiState
|
|
57
|
+
│ │ ├── domain/ # Feature-specific UseCases
|
|
58
|
+
│ │ └── data/ # Feature-specific Repository
|
|
59
|
+
│ └── build.gradle.kts
|
|
60
|
+
└── products/ # :feature:products
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Dependency Graph
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
:app
|
|
69
|
+
├── :feature:users
|
|
70
|
+
│ ├── :core:domain
|
|
71
|
+
│ ├── :core:data
|
|
72
|
+
│ └── :core:ui
|
|
73
|
+
├── :feature:products
|
|
74
|
+
│ ├── :core:domain
|
|
75
|
+
│ ├── :core:data
|
|
76
|
+
│ └── :core:ui
|
|
77
|
+
└── :core:common
|
|
78
|
+
|
|
79
|
+
# ✅ Allowed
|
|
80
|
+
:feature:users → :core:domain
|
|
81
|
+
:feature:users → :core:ui
|
|
82
|
+
:app → :feature:users
|
|
83
|
+
|
|
84
|
+
# ❌ Not allowed
|
|
85
|
+
:feature:users → :feature:products # feature-to-feature dependency
|
|
86
|
+
:core:data → :feature:users # core depending on feature
|
|
87
|
+
:core:ui → :core:data # ui depending on data
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## build.gradle.kts per Module
|
|
93
|
+
|
|
94
|
+
```kotlin
|
|
95
|
+
// ✅ Feature module build file
|
|
96
|
+
plugins {
|
|
97
|
+
alias(libs.plugins.android.library)
|
|
98
|
+
alias(libs.plugins.kotlin.android)
|
|
99
|
+
alias(libs.plugins.hilt)
|
|
100
|
+
alias(libs.plugins.ksp)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
android {
|
|
104
|
+
namespace = "com.example.feature.users"
|
|
105
|
+
// shared config via convention plugin
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
dependencies {
|
|
109
|
+
implementation(project(":core:domain"))
|
|
110
|
+
implementation(project(":core:data"))
|
|
111
|
+
implementation(project(":core:ui"))
|
|
112
|
+
implementation(project(":core:common"))
|
|
113
|
+
|
|
114
|
+
implementation(libs.hilt.android)
|
|
115
|
+
ksp(libs.hilt.compiler)
|
|
116
|
+
|
|
117
|
+
testImplementation(project(":core:testing"))
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Navigation Between Modules
|
|
124
|
+
|
|
125
|
+
```kotlin
|
|
126
|
+
// ✅ Feature modules expose navigation routes as constants
|
|
127
|
+
// :feature:users
|
|
128
|
+
object UsersNavigation {
|
|
129
|
+
const val ROUTE = "users"
|
|
130
|
+
const val USER_DETAIL_ROUTE = "users/{userId}"
|
|
131
|
+
|
|
132
|
+
fun userDetailRoute(userId: String) = "users/$userId"
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ✅ :app wires navigation — features don't know about each other
|
|
136
|
+
@Composable
|
|
137
|
+
fun AppNavHost(navController: NavHostController) {
|
|
138
|
+
NavHost(navController = navController, startDestination = UsersNavigation.ROUTE) {
|
|
139
|
+
usersNavGraph(navController)
|
|
140
|
+
productsNavGraph(navController)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ✅ Each feature exposes a NavGraph extension
|
|
145
|
+
fun NavGraphBuilder.usersNavGraph(navController: NavController) {
|
|
146
|
+
navigation(
|
|
147
|
+
startDestination = UsersNavigation.ROUTE,
|
|
148
|
+
route = "users_graph"
|
|
149
|
+
) {
|
|
150
|
+
composable(UsersNavigation.ROUTE) { UserListScreen(navController) }
|
|
151
|
+
composable(UsersNavigation.USER_DETAIL_ROUTE) { UserDetailScreen(navController) }
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Visibility Control
|
|
159
|
+
|
|
160
|
+
```kotlin
|
|
161
|
+
// ✅ Use internal to hide implementation from other modules
|
|
162
|
+
internal class UserRepositoryImpl @Inject constructor(...) : UserRepository
|
|
163
|
+
|
|
164
|
+
// ✅ Only expose what other modules need
|
|
165
|
+
class GetUserUseCase @Inject constructor(...) // public — used by presentation
|
|
166
|
+
internal class UserRemoteDataSource(...) // internal — implementation detail
|
|
167
|
+
|
|
168
|
+
// ✅ Hilt: internal classes need explicit binding
|
|
169
|
+
@Module
|
|
170
|
+
@InstallIn(SingletonComponent::class)
|
|
171
|
+
internal abstract class UserModule {
|
|
172
|
+
@Binds
|
|
173
|
+
abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Anti-Patterns
|
|
180
|
+
|
|
181
|
+
- Feature modules depending on each other — use `:app` or a shared `:core` module to mediate
|
|
182
|
+
- Putting everything in `:core:common` — keep common truly generic; feature logic belongs in features
|
|
183
|
+
- Circular dependencies — will cause Gradle build failure
|
|
184
|
+
- Public classes that should be `internal` — leaks implementation details
|
|
185
|
+
- One mega-module — defeats purpose of modularization; build times won't improve
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Related Skills
|
|
190
|
+
- `multi-module-architecture` — detailed multi-module setup and advanced patterns
|
|
191
|
+
- `hilt` — Hilt setup across modules
|
|
192
|
+
- `gradle` — Gradle configuration for multi-module projects
|
|
193
|
+
- `convention-plugin` — sharing build configuration across modules
|
|
194
|
+
- `clean-architecture` — layer boundaries that modularization enforces
|