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.
Files changed (176) hide show
  1. package/dist/index.js +143 -0
  2. package/package.json +27 -0
  3. package/skills/Android Ecosystem/Baseline Profile Generator/SKILL.md +277 -0
  4. package/skills/Android Ecosystem/Glance/SKILL.md +315 -0
  5. package/skills/Android Platform/Configuration/SKILL.md +201 -0
  6. package/skills/Android Platform/Filesystem/SKILL.md +216 -0
  7. package/skills/Android Platform/Lifecycle/SKILL.md +233 -0
  8. package/skills/Android Platform/Manifest/SKILL.md +226 -0
  9. package/skills/Android Platform/Process Death Recovery/SKILL.md +214 -0
  10. package/skills/Android Platform/Resources/SKILL.md +234 -0
  11. package/skills/Android Platform/SavedStateHandle/SKILL.md +217 -0
  12. package/skills/Android Platform/State Restoration/SKILL.md +210 -0
  13. package/skills/Architecture/Bounded Context/SKILL.md +207 -0
  14. package/skills/Architecture/Clean Architecture/SKILL.md +229 -0
  15. package/skills/Architecture/Domain Modeling/SKILL.md +236 -0
  16. package/skills/Architecture/Entity Design/SKILL.md +243 -0
  17. package/skills/Architecture/Feature Isolation/SKILL.md +216 -0
  18. package/skills/Architecture/MVI/SKILL.md +224 -0
  19. package/skills/Architecture/MVVM/SKILL.md +198 -0
  20. package/skills/Architecture/Modularization/SKILL.md +194 -0
  21. package/skills/Architecture/Offline First/SKILL.md +249 -0
  22. package/skills/Architecture/Repository Pattern/SKILL.md +216 -0
  23. package/skills/Architecture/Side Effect Management/SKILL.md +278 -0
  24. package/skills/Architecture/State Management/SKILL.md +229 -0
  25. package/skills/Architecture/Unidirectional Data Flow/SKILL.md +196 -0
  26. package/skills/Architecture/Use Case Design/SKILL.md +244 -0
  27. package/skills/Architecture/Value Object/SKILL.md +226 -0
  28. package/skills/Build Infrastructure/Build Orchestration/SKILL.md +257 -0
  29. package/skills/Build Infrastructure/Dependency Compatibility Resolver/SKILL.md +259 -0
  30. package/skills/Build Infrastructure/Environment Validator/SKILL.md +311 -0
  31. package/skills/Build System/Build Cache/SKILL.md +233 -0
  32. package/skills/Build System/Build Flavor Strategy/SKILL.md +171 -0
  33. package/skills/Build System/Build Variant/SKILL.md +215 -0
  34. package/skills/Build System/Convention Plugin/SKILL.md +288 -0
  35. package/skills/Build System/Dependency Management/SKILL.md +261 -0
  36. package/skills/Build System/Gradle/SKILL.md +284 -0
  37. package/skills/Build System/Incremental Build/SKILL.md +199 -0
  38. package/skills/Build System/KAPT/SKILL.md +198 -0
  39. package/skills/Build System/KSP/SKILL.md +263 -0
  40. package/skills/Build System/Module Dependency Graph Validation/SKILL.md +223 -0
  41. package/skills/Build System/Specialized/C++/SKILL.md +308 -0
  42. package/skills/Build System/Specialized/JNI/SKILL.md +306 -0
  43. package/skills/Build System/Specialized/NDK/SKILL.md +264 -0
  44. package/skills/Build System/Version Catalog/SKILL.md +304 -0
  45. package/skills/Concurrency/Background Processing/SKILL.md +185 -0
  46. package/skills/Concurrency/Channel/SKILL.md +207 -0
  47. package/skills/Concurrency/Coroutine/SKILL.md +200 -0
  48. package/skills/Concurrency/Flow/SKILL.md +179 -0
  49. package/skills/Concurrency/Mutex Strategy/SKILL.md +185 -0
  50. package/skills/Concurrency/SharedFlow/SKILL.md +171 -0
  51. package/skills/Concurrency/StateFlow/SKILL.md +175 -0
  52. package/skills/Concurrency/Structured Concurrency/SKILL.md +197 -0
  53. package/skills/Concurrency/Synchronization Policy/SKILL.md +192 -0
  54. package/skills/Core Language/Annotation Processing/SKILL.md +224 -0
  55. package/skills/Core Language/DSL/SKILL.md +186 -0
  56. package/skills/Core Language/Extension Functions Design/SKILL.md +191 -0
  57. package/skills/Core Language/Immutability/SKILL.md +156 -0
  58. package/skills/Core Language/KMP/SKILL.md +182 -0
  59. package/skills/Core Language/Kotlin/SKILL.md +187 -0
  60. package/skills/Core Language/Reactive State Management/SKILL.md +228 -0
  61. package/skills/Core Language/Reactive Streams/SKILL.md +235 -0
  62. package/skills/Core Language/Serialization/SKILL.md +191 -0
  63. package/skills/Data Layer/Cache Strategy/SKILL.md +261 -0
  64. package/skills/Data Layer/Conflict Resolution/SKILL.md +248 -0
  65. package/skills/Data Layer/DAO/SKILL.md +225 -0
  66. package/skills/Data Layer/DTO Mapping/SKILL.md +269 -0
  67. package/skills/Data Layer/DataStore/SKILL.md +264 -0
  68. package/skills/Data Layer/Database Versioning Strategy/SKILL.md +215 -0
  69. package/skills/Data Layer/Encrypted Database/SKILL.md +212 -0
  70. package/skills/Data Layer/File Storage/SKILL.md +247 -0
  71. package/skills/Data Layer/Indexing/SKILL.md +184 -0
  72. package/skills/Data Layer/Key-Value Store Strategy/SKILL.md +185 -0
  73. package/skills/Data Layer/Merge Strategy/SKILL.md +240 -0
  74. package/skills/Data Layer/Migration/SKILL.md +243 -0
  75. package/skills/Data Layer/Paging/SKILL.md +264 -0
  76. package/skills/Data Layer/Proto DataStore/SKILL.md +250 -0
  77. package/skills/Data Layer/Room/SKILL.md +244 -0
  78. package/skills/Data Layer/SQLite/SKILL.md +255 -0
  79. package/skills/Data Layer/Sync Engine/SKILL.md +268 -0
  80. package/skills/Dependency Injection/Dagger/SKILL.md +283 -0
  81. package/skills/Dependency Injection/Hilt/SKILL.md +345 -0
  82. package/skills/Dependency Injection/Koin/SKILL.md +282 -0
  83. package/skills/Developer Experience/Detekt/SKILL.md +272 -0
  84. package/skills/Developer Experience/Lint Rule/SKILL.md +281 -0
  85. package/skills/Google Ecosystem/Analytics/SKILL.md +281 -0
  86. package/skills/Google Ecosystem/Crashlytics/SKILL.md +234 -0
  87. package/skills/Google Ecosystem/Firebase/SKILL.md +200 -0
  88. package/skills/Google Ecosystem/Firebase Messaging/SKILL.md +266 -0
  89. package/skills/Media/Audio/SKILL.md +257 -0
  90. package/skills/Media/Camera/SKILL.md +229 -0
  91. package/skills/Media/CameraX/SKILL.md +295 -0
  92. package/skills/Media/ExoPlayer/SKILL.md +258 -0
  93. package/skills/Media/Video/SKILL.md +228 -0
  94. package/skills/Meta Skills/Domain Error Model/SKILL.md +238 -0
  95. package/skills/Meta Skills/Error Handling/SKILL.md +255 -0
  96. package/skills/Meta Skills/Error Mapping/SKILL.md +232 -0
  97. package/skills/Meta Skills/Failure Strategy/SKILL.md +294 -0
  98. package/skills/Meta Skills/Migration Strategy/SKILL.md +305 -0
  99. package/skills/Meta Skills/User Friendly Errors/SKILL.md +334 -0
  100. package/skills/Navigation/Deep Navigation/SKILL.md +209 -0
  101. package/skills/Navigation/Navigation/SKILL.md +215 -0
  102. package/skills/Navigation/Nested Navigation/SKILL.md +214 -0
  103. package/skills/Networking/API Contract/SKILL.md +220 -0
  104. package/skills/Networking/Authentication/SKILL.md +210 -0
  105. package/skills/Networking/Certificate Pinning/SKILL.md +167 -0
  106. package/skills/Networking/Fallback Strategy/SKILL.md +182 -0
  107. package/skills/Networking/Ktor/SKILL.md +219 -0
  108. package/skills/Networking/Multipart Upload/SKILL.md +213 -0
  109. package/skills/Networking/OkHttp/SKILL.md +193 -0
  110. package/skills/Networking/REST/SKILL.md +178 -0
  111. package/skills/Networking/Rate Limiting/SKILL.md +170 -0
  112. package/skills/Networking/Retrofit/SKILL.md +241 -0
  113. package/skills/Networking/Retry-Backoff/SKILL.md +181 -0
  114. package/skills/Networking/Server-Sent Events (SSE)/SKILL.md +196 -0
  115. package/skills/Networking/WebSocket/SKILL.md +224 -0
  116. package/skills/Observability/Crash Reporting/SKILL.md +219 -0
  117. package/skills/Observability/Logging/SKILL.md +168 -0
  118. package/skills/Observability/Metrics/SKILL.md +227 -0
  119. package/skills/Observability/Structured Logging/SKILL.md +234 -0
  120. package/skills/Performance/ANR Prevention/SKILL.md +192 -0
  121. package/skills/Performance/Allocation Optimization/SKILL.md +179 -0
  122. package/skills/Performance/App Startup/SKILL.md +183 -0
  123. package/skills/Performance/Baseline Profile/SKILL.md +205 -0
  124. package/skills/Performance/Battery Optimization/SKILL.md +192 -0
  125. package/skills/Performance/Benchmark/SKILL.md +182 -0
  126. package/skills/Performance/Bitmap Optimization/SKILL.md +178 -0
  127. package/skills/Performance/Compose Optimization/SKILL.md +187 -0
  128. package/skills/Performance/Heap Management/SKILL.md +184 -0
  129. package/skills/Performance/Macrobenchmark/SKILL.md +214 -0
  130. package/skills/Performance/Memory Leak Prevention/SKILL.md +218 -0
  131. package/skills/Performance/Rendering Performance/SKILL.md +205 -0
  132. package/skills/Performance/Startup Optimization/SKILL.md +219 -0
  133. package/skills/Security/Biometric/SKILL.md +224 -0
  134. package/skills/Security/Certificate Transparency/SKILL.md +158 -0
  135. package/skills/Security/Cryptography/SKILL.md +244 -0
  136. package/skills/Security/Encrypted Storage/SKILL.md +273 -0
  137. package/skills/Security/Frida Detection/SKILL.md +230 -0
  138. package/skills/Security/Hook Detection/SKILL.md +197 -0
  139. package/skills/Security/Keystore/SKILL.md +272 -0
  140. package/skills/Security/Network Security Config/SKILL.md +186 -0
  141. package/skills/Security/Obfuscation/SKILL.md +226 -0
  142. package/skills/Security/Proguard/SKILL.md +202 -0
  143. package/skills/Security/R8/SKILL.md +234 -0
  144. package/skills/Security/Reverse Engineering Resistance/SKILL.md +267 -0
  145. package/skills/Security/Root Detection/SKILL.md +220 -0
  146. package/skills/Security/Secure Networking/SKILL.md +220 -0
  147. package/skills/System Integration/AlarmManager/SKILL.md +182 -0
  148. package/skills/System Integration/App Widget/SKILL.md +182 -0
  149. package/skills/System Integration/Deep Link/SKILL.md +187 -0
  150. package/skills/System Integration/Foreground Service/SKILL.md +212 -0
  151. package/skills/System Integration/Notification/SKILL.md +237 -0
  152. package/skills/System Integration/WorkManager/SKILL.md +256 -0
  153. package/skills/System Integration/clipboard/SKILL.md +155 -0
  154. package/skills/System Integration/share-intent/SKILL.md +182 -0
  155. package/skills/Testing/Compose Testing/SKILL.md +296 -0
  156. package/skills/Testing/Espresso/SKILL.md +292 -0
  157. package/skills/Testing/Fake Data/SKILL.md +245 -0
  158. package/skills/Testing/Integration Testing/SKILL.md +288 -0
  159. package/skills/Testing/Mocking/SKILL.md +229 -0
  160. package/skills/Testing/Snapshot Testing/SKILL.md +259 -0
  161. package/skills/Testing/UI Testing/SKILL.md +293 -0
  162. package/skills/Testing/Unit Testing/SKILL.md +309 -0
  163. package/skills/UI System/Bottom Sheet Patterns/SKILL.md +279 -0
  164. package/skills/UI System/Compose/SKILL.md +296 -0
  165. package/skills/UI System/Compose Animation/SKILL.md +281 -0
  166. package/skills/UI System/Compose Multiplatform/SKILL.md +261 -0
  167. package/skills/UI System/Compose Navigation/SKILL.md +255 -0
  168. package/skills/UI System/Compose Performance/SKILL.md +274 -0
  169. package/skills/UI System/Design System/SKILL.md +217 -0
  170. package/skills/UI System/Empty State Strategy/SKILL.md +208 -0
  171. package/skills/UI System/Keyboard Navigation/SKILL.md +214 -0
  172. package/skills/UI System/Loading Strategy/SKILL.md +254 -0
  173. package/skills/UI System/Material 3/SKILL.md +279 -0
  174. package/skills/UI System/RTL/SKILL.md +179 -0
  175. package/src/index.ts +182 -0
  176. package/tsconfig.json +19 -0
@@ -0,0 +1,274 @@
1
+ ---
2
+ name: compose-performance
3
+ description: >
4
+ Compose recomposition optimization and performance best practices.
5
+ Load this skill when diagnosing recomposition issues, optimizing list
6
+ performance, reducing unnecessary work in Compose, or profiling UI jank.
7
+ ---
8
+
9
+ # Compose Performance
10
+
11
+ ## Overview
12
+ Compose performance is primarily about controlling **recomposition** — the process of re-running composable functions when state changes. Unnecessary recomposition wastes CPU and causes UI jank. Understanding what triggers recomposition and how to minimize its scope is the key to performant Compose UIs.
13
+
14
+ ---
15
+
16
+ ## Core Principles
17
+
18
+ - Recomposition scope is the **smallest enclosing composable** that reads changed state
19
+ - Pass **only the state each composable needs** — not the entire state object
20
+ - Use **stable types** — unstable types cause unnecessary recomposition
21
+ - Move **heavy computation** out of composition — use `remember` or `derivedStateOf`
22
+ - Use `key` in lists — enables smart diffing and avoids full re-render
23
+
24
+ ---
25
+
26
+ ## Stability
27
+
28
+ ```kotlin
29
+ // ✅ Stable types — Compose can skip recomposition if inputs unchanged
30
+ // - All primitive types (Int, String, Boolean, etc.)
31
+ // - Immutable data classes with stable fields
32
+ // - @Stable or @Immutable annotated classes
33
+
34
+ @Immutable // ✅ Promise to Compose: this object never changes
35
+ data class UserUiState(
36
+ val name: String,
37
+ val email: String,
38
+ val isActive: Boolean
39
+ )
40
+
41
+ @Stable // ✅ Promise: changes are tracked via Snapshot state
42
+ class UserPreferences {
43
+ var isDarkMode by mutableStateOf(false)
44
+ var language by mutableStateOf("en")
45
+ }
46
+
47
+ // ❌ Unstable — List<T> is considered unstable by default
48
+ data class UserListState(
49
+ val users: List<User> // triggers recomposition on every emission even if data is same
50
+ )
51
+
52
+ // ✅ Use ImmutableList from kotlinx-collections-immutable
53
+ data class UserListState(
54
+ val users: ImmutableList<User> // stable — Compose can skip recomposition
55
+ )
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Passing Minimal State
61
+
62
+ ```kotlin
63
+ // ❌ Passing entire state — entire subtree recomposes on any change
64
+ @Composable
65
+ fun UserCard(state: UserListUiState) {
66
+ Text(state.selectedUser.name) // recomposes when anything in state changes
67
+ }
68
+
69
+ // ✅ Pass only what's needed
70
+ @Composable
71
+ fun UserCard(userName: String, onUserClick: () -> Unit) {
72
+ Text(userName) // only recomposes when userName changes
73
+ }
74
+ ```
75
+
76
+ ---
77
+
78
+ ## remember and derivedStateOf
79
+
80
+ ```kotlin
81
+ // ✅ remember — cache expensive computation across recompositions
82
+ @Composable
83
+ fun FormattedPrice(priceInCents: Int) {
84
+ val formatted = remember(priceInCents) {
85
+ NumberFormat.getCurrencyInstance().format(priceInCents / 100.0)
86
+ }
87
+ Text(formatted)
88
+ }
89
+
90
+ // ✅ derivedStateOf — compute derived state, recompose only when result changes
91
+ @Composable
92
+ fun UserList(users: List<User>) {
93
+ val activeUserCount by remember {
94
+ derivedStateOf { users.count { it.isActive } }
95
+ }
96
+ // recomposes only when activeUserCount changes, not on every users update
97
+ Text("Active users: $activeUserCount")
98
+ }
99
+
100
+ // ✅ derivedStateOf for scroll-based state
101
+ @Composable
102
+ fun ScrollableScreen() {
103
+ val listState = rememberLazyListState()
104
+ val showScrollToTop by remember {
105
+ derivedStateOf { listState.firstVisibleItemIndex > 5 }
106
+ }
107
+
108
+ if (showScrollToTop) {
109
+ ScrollToTopButton()
110
+ }
111
+ }
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Lambda Stability
117
+
118
+ ```kotlin
119
+ // ❌ Lambda created on each recomposition — causes child recomposition
120
+ @Composable
121
+ fun UserList(users: List<User>, viewModel: UserViewModel) {
122
+ users.forEach { user ->
123
+ UserCard(
124
+ user = user,
125
+ onClick = { viewModel.onUserClick(user.id) } // new lambda each time
126
+ )
127
+ }
128
+ }
129
+
130
+ // ✅ Stable lambda reference
131
+ @Composable
132
+ fun UserList(users: List<User>, onUserClick: (String) -> Unit) {
133
+ users.forEach { user ->
134
+ UserCard(user = user, onClick = { onUserClick(user.id) })
135
+ }
136
+ }
137
+
138
+ // ✅ Use rememberUpdatedState for callbacks that change
139
+ @Composable
140
+ fun Timer(onTick: () -> Unit) {
141
+ val currentOnTick by rememberUpdatedState(onTick)
142
+ LaunchedEffect(Unit) {
143
+ while (true) {
144
+ delay(1_000)
145
+ currentOnTick()
146
+ }
147
+ }
148
+ }
149
+ ```
150
+
151
+ ---
152
+
153
+ ## List Performance
154
+
155
+ ```kotlin
156
+ // ✅ Always provide stable key in LazyColumn/LazyRow
157
+ LazyColumn {
158
+ items(
159
+ items = users,
160
+ key = { user -> user.id } // stable, unique key
161
+ ) { user ->
162
+ UserCard(user = user)
163
+ }
164
+ }
165
+
166
+ // ✅ contentType — helps Compose reuse item compositions
167
+ LazyColumn {
168
+ items(
169
+ items = feedItems,
170
+ key = { it.id },
171
+ contentType = { item ->
172
+ when (item) {
173
+ is FeedItem.Post -> "post"
174
+ is FeedItem.Ad -> "ad"
175
+ is FeedItem.Header -> "header"
176
+ }
177
+ }
178
+ ) { item ->
179
+ FeedItemView(item)
180
+ }
181
+ }
182
+
183
+ // ✅ Use Pager for full-screen horizontal paging
184
+ HorizontalPager(count = pages.size) { page ->
185
+ PageContent(pages[page])
186
+ }
187
+ ```
188
+
189
+ ---
190
+
191
+ ## Read State at the Lowest Level
192
+
193
+ ```kotlin
194
+ // ❌ Read state high up — entire tree recomposes
195
+ @Composable
196
+ fun Screen(scrollState: ScrollState) {
197
+ val offset = scrollState.value // read here — Screen recomposes on every scroll
198
+ Column(modifier = Modifier.offset(y = offset.dp)) {
199
+ ExpensiveContent()
200
+ }
201
+ }
202
+
203
+ // ✅ Defer state read to Modifier — no recomposition, only layout pass
204
+ @Composable
205
+ fun Screen(scrollState: ScrollState) {
206
+ Column(
207
+ modifier = Modifier.graphicsLayer {
208
+ translationY = scrollState.value.toFloat() // read inside lambda — no recompose
209
+ }
210
+ ) {
211
+ ExpensiveContent()
212
+ }
213
+ }
214
+ ```
215
+
216
+ ---
217
+
218
+ ## Baseline Profiles
219
+
220
+ ```kotlin
221
+ // ✅ Generate Baseline Profile to pre-compile critical Compose paths
222
+ // See baseline-profile skill for full setup
223
+
224
+ // Macrobenchmark test
225
+ @RunWith(AndroidJUnit4::class)
226
+ class BaselineProfileGenerator {
227
+ @get:Rule
228
+ val rule = BaselineProfileRule()
229
+
230
+ @Test
231
+ fun generate() = rule.collect(packageName = "com.example.app") {
232
+ startActivityAndWait()
233
+ device.findObject(By.text("Users")).click()
234
+ device.waitForIdle()
235
+ }
236
+ }
237
+ ```
238
+
239
+ ---
240
+
241
+ ## Profiling Tools
242
+
243
+ ```
244
+ Layout Inspector → Recomposition counts per composable
245
+ Compose Tracing → detailed timeline in Perfetto
246
+ Android Studio Profiler → CPU/frame timeline
247
+ ```
248
+
249
+ ```kotlin
250
+ // ✅ Add Compose tracing for Perfetto
251
+ dependencies {
252
+ implementation(libs.androidx.tracing.compose)
253
+ }
254
+ ```
255
+
256
+ ---
257
+
258
+ ## Anti-Patterns
259
+
260
+ - Reading `scrollState.value` at the top of a composable — causes excessive recomposition
261
+ - Using `List<T>` in state — use `ImmutableList<T>` for stable collections
262
+ - Creating lambdas inline in items — causes recomposition of every item
263
+ - Not providing `key` in `items {}` — full list recompose on any change
264
+ - Computing derived values without `remember` — recalculates every recomposition
265
+ - Putting heavy logic directly in composable body — use `remember` or move to ViewModel
266
+
267
+ ---
268
+
269
+ ## Related Skills
270
+ - `compose` — Compose fundamentals
271
+ - `compose-animation` — animation without recomposition overhead
272
+ - `baseline-profile` — pre-compiling Compose hot paths
273
+ - `benchmark` — measuring Compose performance
274
+ - `rendering-performance` — frame timing and jank detection
@@ -0,0 +1,217 @@
1
+ ---
2
+ name: design-system
3
+ description: >
4
+ Building and maintaining a custom design system on top of Material 3 in
5
+ Jetpack Compose. Load this skill when defining custom design tokens,
6
+ extending the M3 theme, creating a component library, managing spacing
7
+ scales, or enforcing visual consistency across the app.
8
+ ---
9
+
10
+ # Design System
11
+
12
+ ## Overview
13
+
14
+ A design system extends Material 3 with project-specific tokens (colors, spacing, typography, elevation) and a reusable component library. It acts as the single source of truth for all visual decisions, ensuring consistency and reducing duplication across features.
15
+
16
+ ---
17
+
18
+ ## Core Principles
19
+
20
+ - Extend M3 — never replace it; build on top of `MaterialTheme`
21
+ - Define all tokens once — consume them everywhere via `CompositionLocal`
22
+ - Components in the design system must have no business logic
23
+ - Every design system component must support preview in both light/dark and LTR/RTL
24
+ - Tokens must be sealed objects or typed wrappers — never raw `Dp` or `Color` literals in UI code
25
+
26
+ ---
27
+
28
+ ## Token Structure
29
+
30
+ ```kotlin
31
+ // ✅ Spacing tokens
32
+ object AppSpacing {
33
+ val xs = 4.dp
34
+ val sm = 8.dp
35
+ val md = 16.dp
36
+ val lg = 24.dp
37
+ val xl = 32.dp
38
+ val xxl = 48.dp
39
+ }
40
+
41
+ // ✅ Elevation tokens
42
+ object AppElevation {
43
+ val none = 0.dp
44
+ val low = 2.dp
45
+ val medium = 4.dp
46
+ val high = 8.dp
47
+ }
48
+
49
+ // ✅ Icon size tokens
50
+ object AppIconSize {
51
+ val sm = 16.dp
52
+ val md = 24.dp
53
+ val lg = 32.dp
54
+ }
55
+ ```
56
+
57
+ ---
58
+
59
+ ## Extending MaterialTheme
60
+
61
+ ```kotlin
62
+ // ✅ Custom theme extension via CompositionLocal
63
+ data class AppColors(
64
+ val success: Color,
65
+ val onSuccess: Color,
66
+ val warning: Color,
67
+ val onWarning: Color,
68
+ val info: Color,
69
+ val onInfo: Color
70
+ )
71
+
72
+ val LocalAppColors = staticCompositionLocalOf {
73
+ AppColors(
74
+ success = Color(0xFF2E7D32),
75
+ onSuccess = Color.White,
76
+ warning = Color(0xFFF57F17),
77
+ onWarning = Color.Black,
78
+ info = Color(0xFF0277BD),
79
+ onInfo = Color.White
80
+ )
81
+ }
82
+
83
+ // ✅ Access extension colors
84
+ val appColors = LocalAppColors.current
85
+ Box(modifier = Modifier.background(appColors.success))
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Theme Provider
91
+
92
+ ```kotlin
93
+ // ✅ Provide all custom locals alongside MaterialTheme
94
+ @Composable
95
+ fun AppTheme(
96
+ darkTheme: Boolean = isSystemInDarkTheme(),
97
+ content: @Composable () -> Unit
98
+ ) {
99
+ val appColors = if (darkTheme) darkAppColors() else lightAppColors()
100
+
101
+ CompositionLocalProvider(
102
+ LocalAppColors provides appColors
103
+ ) {
104
+ MaterialTheme(
105
+ colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme,
106
+ typography = AppTypography,
107
+ shapes = AppShapes,
108
+ content = content
109
+ )
110
+ }
111
+ }
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Design System Component Pattern
117
+
118
+ ```kotlin
119
+ // ✅ Atomic component — no business logic, fully configurable
120
+ @Composable
121
+ fun AppButton(
122
+ text: String,
123
+ onClick: () -> Unit,
124
+ modifier: Modifier = Modifier,
125
+ enabled: Boolean = true,
126
+ loading: Boolean = false,
127
+ style: AppButtonStyle = AppButtonStyle.Filled
128
+ ) {
129
+ val colors = when (style) {
130
+ AppButtonStyle.Filled -> ButtonDefaults.buttonColors()
131
+ AppButtonStyle.Tonal -> ButtonDefaults.filledTonalButtonColors()
132
+ AppButtonStyle.Outline -> ButtonDefaults.outlinedButtonColors()
133
+ }
134
+
135
+ Button(
136
+ onClick = onClick,
137
+ modifier = modifier,
138
+ enabled = enabled && !loading,
139
+ colors = colors
140
+ ) {
141
+ if (loading) {
142
+ CircularProgressIndicator(
143
+ modifier = Modifier.size(AppIconSize.sm),
144
+ strokeWidth = 2.dp
145
+ )
146
+ Spacer(Modifier.width(AppSpacing.sm))
147
+ }
148
+ Text(text)
149
+ }
150
+ }
151
+
152
+ enum class AppButtonStyle { Filled, Tonal, Outline }
153
+ ```
154
+
155
+ ---
156
+
157
+ ## Typography Extension
158
+
159
+ ```kotlin
160
+ // ✅ Custom text styles beyond the M3 scale
161
+ object AppTextStyle {
162
+ val code = TextStyle(
163
+ fontFamily = FontFamily.Monospace,
164
+ fontSize = 13.sp,
165
+ lineHeight = 20.sp
166
+ )
167
+ val caption = TextStyle(
168
+ fontSize = 11.sp,
169
+ lineHeight = 16.sp,
170
+ color = Color.Unspecified // inherit from context
171
+ )
172
+ }
173
+ ```
174
+
175
+ ---
176
+
177
+ ## Component Catalog Preview
178
+
179
+ ```kotlin
180
+ // ✅ Every component has a catalog preview
181
+ @Preview(name = "Light", uiMode = UI_MODE_NIGHT_NO)
182
+ @Preview(name = "Dark", uiMode = UI_MODE_NIGHT_YES)
183
+ @Preview(name = "RTL", locale = "fa")
184
+ @Composable
185
+ private fun AppButtonPreview() {
186
+ AppTheme {
187
+ Column(
188
+ modifier = Modifier.padding(AppSpacing.md),
189
+ verticalArrangement = Arrangement.spacedBy(AppSpacing.sm)
190
+ ) {
191
+ AppButton("Filled", onClick = {}, style = AppButtonStyle.Filled)
192
+ AppButton("Tonal", onClick = {}, style = AppButtonStyle.Tonal)
193
+ AppButton("Loading", onClick = {}, loading = true)
194
+ AppButton("Disabled", onClick = {}, enabled = false)
195
+ }
196
+ }
197
+ }
198
+ ```
199
+
200
+ ---
201
+
202
+ ## Anti-Patterns
203
+
204
+ - Using raw `Color(0xFF...)` literals in UI composables — use tokens
205
+ - Putting business logic inside design system components
206
+ - Overriding `MaterialTheme` colors directly via `MaterialTheme.copy()` in a nested scope — use `CompositionLocalProvider` instead
207
+ - Defining spacing inline (`Modifier.padding(16.dp)`) — use `AppSpacing.md`
208
+ - Mixing M2 and M3 components in the same design system
209
+
210
+ ---
211
+
212
+ ## Related Skills
213
+
214
+ - `material3` — M3 foundation this system builds on
215
+ - `compose` — Compose fundamentals
216
+ - `rtl` — directional token and layout requirements
217
+ - `resources` — XML resources for legacy interop
@@ -0,0 +1,208 @@
1
+ ---
2
+ name: empty-state-strategy
3
+ description: >
4
+ Strategies for displaying empty states in Android/Compose applications.
5
+ Load this skill when building screens that may have no data to show,
6
+ handling empty search results, first-time user experiences, filtered
7
+ lists with no matches, or error states that look like empty states.
8
+ ---
9
+
10
+ # Empty State Strategy
11
+
12
+ ## Overview
13
+ An empty state is what the user sees when a screen has no content to display. A well-designed empty state communicates clearly why there's nothing to show and what the user can do about it. Poor empty states leave users confused about whether the app is broken or the feature simply has no data yet.
14
+
15
+ ---
16
+
17
+ ## Core Principles
18
+
19
+ - Every list screen must account for an empty state — it is never optional
20
+ - Empty state must communicate **why** it's empty and **what the user can do**
21
+ - Distinguish between: no data yet, empty search result, filtered result, and error
22
+ - Empty state is a UI state variant — handle it in the state model, not with an `if` in the composable
23
+ - Never show a blank screen — always provide context and a call to action when possible
24
+
25
+ ---
26
+
27
+ ## Empty State Types
28
+
29
+ | Type | Cause | Action |
30
+ |------|-------|--------|
31
+ | First-time | User has no data yet | Primary CTA to create |
32
+ | Empty search | Query returned no results | Clear search / change query |
33
+ | Filtered | Active filter hides all items | Clear filters |
34
+ | Error | Failed to load | Retry button |
35
+ | Offline | No connectivity | Retry when online |
36
+
37
+ ---
38
+
39
+ ## UI State Model
40
+
41
+ ```kotlin
42
+ // ✅ Empty is a distinct state, not null data
43
+ sealed interface UiState<out T> {
44
+ data object Loading : UiState<Nothing>
45
+ data class Success<T>(val data: T) : UiState<T>
46
+ data object Empty : UiState<Nothing>
47
+ data class Error(val message: String) : UiState<Nothing>
48
+ }
49
+
50
+ // ✅ ViewModel maps domain result to correct state
51
+ fun loadItems() {
52
+ viewModelScope.launch {
53
+ _state.value = UiState.Loading
54
+ repository.getItems().fold(
55
+ onSuccess = { items ->
56
+ _state.value = if (items.isEmpty()) UiState.Empty
57
+ else UiState.Success(items)
58
+ },
59
+ onFailure = { _state.value = UiState.Error(it.message ?: "Error") }
60
+ )
61
+ }
62
+ }
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Empty State Component
68
+
69
+ ```kotlin
70
+ // ✅ Reusable empty state component
71
+ @Composable
72
+ fun EmptyState(
73
+ icon: ImageVector,
74
+ title: String,
75
+ subtitle: String? = null,
76
+ actionLabel: String? = null,
77
+ onAction: (() -> Unit)? = null,
78
+ modifier: Modifier = Modifier
79
+ ) {
80
+ Column(
81
+ modifier = modifier
82
+ .fillMaxSize()
83
+ .padding(AppSpacing.xl),
84
+ horizontalAlignment = Alignment.CenterHorizontally,
85
+ verticalArrangement = Arrangement.Center
86
+ ) {
87
+ Icon(
88
+ imageVector = icon,
89
+ contentDescription = null,
90
+ modifier = Modifier.size(64.dp),
91
+ tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f)
92
+ )
93
+ Spacer(Modifier.height(AppSpacing.md))
94
+ Text(
95
+ text = title,
96
+ style = MaterialTheme.typography.titleMedium,
97
+ textAlign = TextAlign.Center,
98
+ color = MaterialTheme.colorScheme.onSurface
99
+ )
100
+ if (subtitle != null) {
101
+ Spacer(Modifier.height(AppSpacing.sm))
102
+ Text(
103
+ text = subtitle,
104
+ style = MaterialTheme.typography.bodyMedium,
105
+ textAlign = TextAlign.Center,
106
+ color = MaterialTheme.colorScheme.onSurfaceVariant
107
+ )
108
+ }
109
+ if (actionLabel != null && onAction != null) {
110
+ Spacer(Modifier.height(AppSpacing.lg))
111
+ Button(onClick = onAction) {
112
+ Text(actionLabel)
113
+ }
114
+ }
115
+ }
116
+ }
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Usage Per Type
122
+
123
+ ```kotlin
124
+ // ✅ First-time empty state
125
+ EmptyState(
126
+ icon = Icons.Outlined.Inbox,
127
+ title = "No items yet",
128
+ subtitle = "Create your first item to get started.",
129
+ actionLabel = "Create Item",
130
+ onAction = onCreateClick
131
+ )
132
+
133
+ // ✅ Empty search result
134
+ EmptyState(
135
+ icon = Icons.Outlined.SearchOff,
136
+ title = "No results for \"$query\"",
137
+ subtitle = "Try a different search term.",
138
+ actionLabel = "Clear Search",
139
+ onAction = onClearSearch
140
+ )
141
+
142
+ // ✅ Active filter hides all items
143
+ EmptyState(
144
+ icon = Icons.Outlined.FilterAltOff,
145
+ title = "No matches",
146
+ subtitle = "No items match the active filters.",
147
+ actionLabel = "Clear Filters",
148
+ onAction = onClearFilters
149
+ )
150
+
151
+ // ✅ Error state (looks like empty — distinguish clearly)
152
+ EmptyState(
153
+ icon = Icons.Outlined.CloudOff,
154
+ title = "Couldn't load items",
155
+ subtitle = "Check your connection and try again.",
156
+ actionLabel = "Retry",
157
+ onAction = onRetry
158
+ )
159
+ ```
160
+
161
+ ---
162
+
163
+ ## Screen Integration
164
+
165
+ ```kotlin
166
+ // ✅ Handle all states in one when block
167
+ @Composable
168
+ fun ItemListScreen(viewModel: ItemListViewModel = hiltViewModel()) {
169
+ val state by viewModel.state.collectAsStateWithLifecycle()
170
+
171
+ when (state) {
172
+ is UiState.Loading -> SkeletonList()
173
+ is UiState.Empty -> EmptyState(
174
+ icon = Icons.Outlined.Inbox,
175
+ title = "Nothing here yet",
176
+ subtitle = "Add your first item.",
177
+ actionLabel = "Add Item",
178
+ onAction = viewModel::onAddClick
179
+ )
180
+ is UiState.Success -> ItemList((state as UiState.Success).data)
181
+ is UiState.Error -> EmptyState(
182
+ icon = Icons.Outlined.ErrorOutline,
183
+ title = "Something went wrong",
184
+ subtitle = (state as UiState.Error).message,
185
+ actionLabel = "Retry",
186
+ onAction = viewModel::loadItems
187
+ )
188
+ }
189
+ }
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Anti-Patterns
195
+
196
+ - Showing a blank screen when data is empty — always provide context
197
+ - Using `if (list.isEmpty())` inline in a composable instead of a sealed state
198
+ - Confusing error state with empty state — user must know why content is missing
199
+ - Showing a generic "No data" message without context or action
200
+ - Hiding the empty state behind a loading state that never resolves
201
+
202
+ ---
203
+
204
+ ## Related Skills
205
+ - `loading-strategy` — what to show while data is being fetched
206
+ - `state-management` — modeling UI states as sealed classes
207
+ - `error-handling` — error propagation and user-facing error messages
208
+ - `compose` — layout and animation fundamentals