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,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
|