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,296 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: compose
|
|
3
|
+
description: >
|
|
4
|
+
Jetpack Compose fundamentals, patterns, and best practices for Android UI.
|
|
5
|
+
Load this skill when building any Compose UI, designing composable functions,
|
|
6
|
+
managing state in Compose, or optimizing recomposition.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Compose
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
Jetpack Compose is Android's modern declarative UI toolkit. UI is described as functions of state — when state changes, the UI recomposes automatically. Understanding recomposition, state hoisting, and composable design is essential for correct and performant Compose code.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Core Principles
|
|
17
|
+
|
|
18
|
+
- UI is a **function of state** — never imperatively mutate UI
|
|
19
|
+
- **Hoist state** to the lowest common ancestor that needs it
|
|
20
|
+
- Keep composables **small and focused** — one responsibility per composable
|
|
21
|
+
- **Stateless composables** are easier to test, preview, and reuse
|
|
22
|
+
- Avoid side effects inside composable functions — use `LaunchedEffect`, `SideEffect`, `DisposableEffect`
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Composable Function Design
|
|
27
|
+
|
|
28
|
+
```kotlin
|
|
29
|
+
// ✅ Stateless composable — accepts state, emits events
|
|
30
|
+
@Composable
|
|
31
|
+
fun UserCard(
|
|
32
|
+
user: User,
|
|
33
|
+
onUserClick: (String) -> Unit,
|
|
34
|
+
modifier: Modifier = Modifier // always include modifier parameter
|
|
35
|
+
) {
|
|
36
|
+
Card(
|
|
37
|
+
modifier = modifier.clickable { onUserClick(user.id) }
|
|
38
|
+
) {
|
|
39
|
+
Column(modifier = Modifier.padding(16.dp)) {
|
|
40
|
+
Text(text = user.name, style = MaterialTheme.typography.titleMedium)
|
|
41
|
+
Text(text = user.email, style = MaterialTheme.typography.bodySmall)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ✅ Stateful composable — owns state, delegates to stateless
|
|
47
|
+
@Composable
|
|
48
|
+
fun UserCardScreen(viewModel: UserViewModel = hiltViewModel()) {
|
|
49
|
+
val state by viewModel.state.collectAsStateWithLifecycle()
|
|
50
|
+
UserCard(
|
|
51
|
+
user = state.user,
|
|
52
|
+
onUserClick = viewModel::onUserClicked
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## State Management in Compose
|
|
60
|
+
|
|
61
|
+
```kotlin
|
|
62
|
+
// ✅ Local UI state — remember
|
|
63
|
+
@Composable
|
|
64
|
+
fun ExpandableCard(title: String, content: String) {
|
|
65
|
+
var isExpanded by remember { mutableStateOf(false) }
|
|
66
|
+
|
|
67
|
+
Card(modifier = Modifier.clickable { isExpanded = !isExpanded }) {
|
|
68
|
+
Column {
|
|
69
|
+
Text(title)
|
|
70
|
+
if (isExpanded) Text(content)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ✅ State that survives rotation — rememberSaveable
|
|
76
|
+
@Composable
|
|
77
|
+
fun SearchBar(onSearch: (String) -> Unit) {
|
|
78
|
+
var query by rememberSaveable { mutableStateOf("") }
|
|
79
|
+
TextField(
|
|
80
|
+
value = query,
|
|
81
|
+
onValueChange = { query = it },
|
|
82
|
+
trailingIcon = {
|
|
83
|
+
IconButton(onClick = { onSearch(query) }) {
|
|
84
|
+
Icon(Icons.Default.Search, contentDescription = null)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ✅ Complex state — hoist to ViewModel
|
|
91
|
+
@Composable
|
|
92
|
+
fun ProductScreen(viewModel: ProductViewModel = hiltViewModel()) {
|
|
93
|
+
val state by viewModel.state.collectAsStateWithLifecycle()
|
|
94
|
+
ProductContent(
|
|
95
|
+
state = state,
|
|
96
|
+
onAddToCart = viewModel::onAddToCart,
|
|
97
|
+
onQuantityChange = viewModel::onQuantityChange
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## State Hoisting Pattern
|
|
105
|
+
|
|
106
|
+
```kotlin
|
|
107
|
+
// ✅ Hoist state up — pass value and onValueChange
|
|
108
|
+
@Composable
|
|
109
|
+
fun EmailField(
|
|
110
|
+
value: String,
|
|
111
|
+
onValueChange: (String) -> Unit,
|
|
112
|
+
modifier: Modifier = Modifier
|
|
113
|
+
) {
|
|
114
|
+
OutlinedTextField(
|
|
115
|
+
value = value,
|
|
116
|
+
onValueChange = onValueChange,
|
|
117
|
+
label = { Text("Email") },
|
|
118
|
+
modifier = modifier
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ✅ Owner of state composes and passes down
|
|
123
|
+
@Composable
|
|
124
|
+
fun RegisterForm() {
|
|
125
|
+
var email by rememberSaveable { mutableStateOf("") }
|
|
126
|
+
var password by rememberSaveable { mutableStateOf("") }
|
|
127
|
+
|
|
128
|
+
EmailField(value = email, onValueChange = { email = it })
|
|
129
|
+
PasswordField(value = password, onValueChange = { password = it })
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Side Effects
|
|
136
|
+
|
|
137
|
+
```kotlin
|
|
138
|
+
// ✅ LaunchedEffect — coroutine tied to composition
|
|
139
|
+
@Composable
|
|
140
|
+
fun UserScreen(userId: String, viewModel: UserViewModel = hiltViewModel()) {
|
|
141
|
+
LaunchedEffect(userId) {
|
|
142
|
+
viewModel.loadUser(userId) // re-runs when userId changes
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ✅ DisposableEffect — non-coroutine cleanup
|
|
147
|
+
@Composable
|
|
148
|
+
fun LifecycleAwareScreen(onResume: () -> Unit) {
|
|
149
|
+
val lifecycleOwner = LocalLifecycleOwner.current
|
|
150
|
+
DisposableEffect(lifecycleOwner) {
|
|
151
|
+
val observer = LifecycleEventObserver { _, event ->
|
|
152
|
+
if (event == Lifecycle.Event.ON_RESUME) onResume()
|
|
153
|
+
}
|
|
154
|
+
lifecycleOwner.lifecycle.addObserver(observer)
|
|
155
|
+
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ✅ SideEffect — sync Compose state to non-Compose code
|
|
160
|
+
@Composable
|
|
161
|
+
fun AnalyticsScreen(screenName: String) {
|
|
162
|
+
SideEffect {
|
|
163
|
+
analytics.logScreenView(screenName) // runs on every successful recomposition
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ✅ One-time events from ViewModel
|
|
168
|
+
@Composable
|
|
169
|
+
fun OrderScreen(viewModel: OrderViewModel = hiltViewModel()) {
|
|
170
|
+
val lifecycleOwner = LocalLifecycleOwner.current
|
|
171
|
+
|
|
172
|
+
LaunchedEffect(Unit) {
|
|
173
|
+
viewModel.events
|
|
174
|
+
.flowWithLifecycle(lifecycleOwner.lifecycle)
|
|
175
|
+
.collect { event ->
|
|
176
|
+
when (event) {
|
|
177
|
+
is OrderEvent.NavigateToConfirmation -> navController.navigate(...)
|
|
178
|
+
is OrderEvent.ShowError -> snackbarHostState.showSnackbar(event.message)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Modifier Best Practices
|
|
188
|
+
|
|
189
|
+
```kotlin
|
|
190
|
+
// ✅ Always accept modifier as parameter — last before lambdas
|
|
191
|
+
@Composable
|
|
192
|
+
fun MyComponent(
|
|
193
|
+
text: String,
|
|
194
|
+
modifier: Modifier = Modifier, // default to Modifier (empty)
|
|
195
|
+
onClick: () -> Unit
|
|
196
|
+
) {
|
|
197
|
+
Box(modifier = modifier.clickable { onClick() }) {
|
|
198
|
+
Text(text)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ✅ Order matters — padding before clickable = larger touch target
|
|
203
|
+
Box(modifier = Modifier
|
|
204
|
+
.padding(8.dp)
|
|
205
|
+
.clickable { } // touch area includes padding
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
// ✅ Clickable before padding = smaller touch target
|
|
209
|
+
Box(modifier = Modifier
|
|
210
|
+
.clickable { }
|
|
211
|
+
.padding(8.dp) // padding is outside the touch area
|
|
212
|
+
)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Lists
|
|
218
|
+
|
|
219
|
+
```kotlin
|
|
220
|
+
// ✅ LazyColumn for long lists — only renders visible items
|
|
221
|
+
@Composable
|
|
222
|
+
fun UserList(users: List<User>, onUserClick: (String) -> Unit) {
|
|
223
|
+
LazyColumn(
|
|
224
|
+
contentPadding = PaddingValues(16.dp),
|
|
225
|
+
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
226
|
+
) {
|
|
227
|
+
items(
|
|
228
|
+
items = users,
|
|
229
|
+
key = { user -> user.id } // ✅ always provide stable key
|
|
230
|
+
) { user ->
|
|
231
|
+
UserCard(user = user, onUserClick = onUserClick)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ✅ Column only for short/fixed lists (< ~10 items)
|
|
237
|
+
@Composable
|
|
238
|
+
fun SettingsMenu(items: List<SettingItem>) {
|
|
239
|
+
Column {
|
|
240
|
+
items.forEach { item ->
|
|
241
|
+
SettingRow(item = item)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Previews
|
|
250
|
+
|
|
251
|
+
```kotlin
|
|
252
|
+
// ✅ Preview stateless composables
|
|
253
|
+
@Preview(showBackground = true)
|
|
254
|
+
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
|
255
|
+
@Composable
|
|
256
|
+
private fun UserCardPreview() {
|
|
257
|
+
AppTheme {
|
|
258
|
+
UserCard(
|
|
259
|
+
user = User(id = "1", name = "Ali Rezaei", email = "ali@example.com"),
|
|
260
|
+
onUserClick = {}
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ✅ Preview with multiple states
|
|
266
|
+
@Preview(name = "Loading")
|
|
267
|
+
@Preview(name = "Success")
|
|
268
|
+
@Composable
|
|
269
|
+
private fun UserScreenPreview() {
|
|
270
|
+
AppTheme {
|
|
271
|
+
UserContent(state = previewState)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Anti-Patterns
|
|
279
|
+
|
|
280
|
+
- Side effects directly in composable body — use `LaunchedEffect`/`DisposableEffect`
|
|
281
|
+
- Reading `viewModel.state.value` directly — use `collectAsStateWithLifecycle()`
|
|
282
|
+
- Not providing `key` in `items {}` — causes incorrect animations and recomposition
|
|
283
|
+
- Deeply nested composables — extract to named functions
|
|
284
|
+
- Not passing `modifier` parameter — prevents callers from customizing layout
|
|
285
|
+
- Creating remember/state inside loops or conditions — violates rules of hooks
|
|
286
|
+
- Using `Column` for long scrollable lists — use `LazyColumn`
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Related Skills
|
|
291
|
+
- `compose-navigation` — navigation between screens
|
|
292
|
+
- `compose-performance` — recomposition optimization
|
|
293
|
+
- `compose-animation` — animation in Compose
|
|
294
|
+
- `material3` — Material 3 components and theming
|
|
295
|
+
- `state-management` — state patterns with ViewModel
|
|
296
|
+
- `reactive-state-management` — collecting flows in Compose
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: compose-animation
|
|
3
|
+
description: >
|
|
4
|
+
Compose animation APIs and patterns for Android UI.
|
|
5
|
+
Load this skill when adding transitions, animated visibility,
|
|
6
|
+
motion between screens, or any time-based UI change in Compose.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Compose Animation
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
Compose provides a rich animation system ranging from simple animated value changes to complex physics-based motion. Animations improve UX by providing visual continuity and feedback. Choose the simplest API that meets the need.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Core Principles
|
|
17
|
+
|
|
18
|
+
- Choose the **simplest API** for the job — don't use `Animatable` when `animateFloatAsState` works
|
|
19
|
+
- Animations should **respect user accessibility settings** — check `reduceMotion`
|
|
20
|
+
- Never block the main thread — all Compose animations are coroutine-based
|
|
21
|
+
- Animate **state transitions**, not imperative sequences
|
|
22
|
+
- Use `AnimatedVisibility` and `AnimatedContent` before reaching for custom animations
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Animation API Hierarchy
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
Simple value change → animateXxxAsState
|
|
30
|
+
Visibility show/hide → AnimatedVisibility
|
|
31
|
+
Content swap → AnimatedContent
|
|
32
|
+
Screen transitions → NavHost transitions
|
|
33
|
+
Multi-value / sequenced → updateTransition
|
|
34
|
+
Full control / physics → Animatable
|
|
35
|
+
Infinite → rememberInfiniteTransition
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## animateXxxAsState — Simple Value Animation
|
|
41
|
+
|
|
42
|
+
```kotlin
|
|
43
|
+
// ✅ Animate a single value when state changes
|
|
44
|
+
@Composable
|
|
45
|
+
fun LikeButton(isLiked: Boolean, onToggle: () -> Unit) {
|
|
46
|
+
val scale by animateFloatAsState(
|
|
47
|
+
targetValue = if (isLiked) 1.2f else 1f,
|
|
48
|
+
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy),
|
|
49
|
+
label = "like_scale" // ✅ always provide label for tooling
|
|
50
|
+
)
|
|
51
|
+
val color by animateColorAsState(
|
|
52
|
+
targetValue = if (isLiked) Color.Red else Color.Gray,
|
|
53
|
+
label = "like_color"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
Icon(
|
|
57
|
+
imageVector = Icons.Default.Favorite,
|
|
58
|
+
contentDescription = null,
|
|
59
|
+
tint = color,
|
|
60
|
+
modifier = Modifier
|
|
61
|
+
.scale(scale)
|
|
62
|
+
.clickable { onToggle() }
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ✅ Animate size
|
|
67
|
+
val size by animateDpAsState(
|
|
68
|
+
targetValue = if (isExpanded) 200.dp else 100.dp,
|
|
69
|
+
animationSpec = tween(durationMillis = 300),
|
|
70
|
+
label = "card_size"
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## AnimatedVisibility
|
|
77
|
+
|
|
78
|
+
```kotlin
|
|
79
|
+
// ✅ Show/hide with animation
|
|
80
|
+
@Composable
|
|
81
|
+
fun FilterPanel(isVisible: Boolean, content: @Composable () -> Unit) {
|
|
82
|
+
AnimatedVisibility(
|
|
83
|
+
visible = isVisible,
|
|
84
|
+
enter = slideInVertically() + fadeIn(),
|
|
85
|
+
exit = slideOutVertically() + fadeOut()
|
|
86
|
+
) {
|
|
87
|
+
content()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ✅ List item appearance
|
|
92
|
+
LazyColumn {
|
|
93
|
+
items(users, key = { it.id }) { user ->
|
|
94
|
+
AnimatedVisibility(
|
|
95
|
+
visible = true,
|
|
96
|
+
enter = fadeIn() + expandVertically()
|
|
97
|
+
) {
|
|
98
|
+
UserCard(user = user)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## AnimatedContent
|
|
107
|
+
|
|
108
|
+
```kotlin
|
|
109
|
+
// ✅ Animate between different content states
|
|
110
|
+
@Composable
|
|
111
|
+
fun CounterDisplay(count: Int) {
|
|
112
|
+
AnimatedContent(
|
|
113
|
+
targetState = count,
|
|
114
|
+
transitionSpec = {
|
|
115
|
+
if (targetState > initialState) {
|
|
116
|
+
slideInVertically { -it } + fadeIn() togetherWith
|
|
117
|
+
slideOutVertically { it } + fadeOut()
|
|
118
|
+
} else {
|
|
119
|
+
slideInVertically { it } + fadeIn() togetherWith
|
|
120
|
+
slideOutVertically { -it } + fadeOut()
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
label = "counter"
|
|
124
|
+
) { targetCount ->
|
|
125
|
+
Text(text = "$targetCount", style = MaterialTheme.typography.displayLarge)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ✅ Switch between loading/content/error states
|
|
130
|
+
@Composable
|
|
131
|
+
fun ScreenContent(state: UiState) {
|
|
132
|
+
AnimatedContent(
|
|
133
|
+
targetState = state,
|
|
134
|
+
label = "screen_content"
|
|
135
|
+
) { currentState ->
|
|
136
|
+
when (currentState) {
|
|
137
|
+
is UiState.Loading -> LoadingView()
|
|
138
|
+
is UiState.Success -> ContentView(currentState.data)
|
|
139
|
+
is UiState.Error -> ErrorView(currentState.message)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## updateTransition — Coordinated Multi-Value Animation
|
|
148
|
+
|
|
149
|
+
```kotlin
|
|
150
|
+
// ✅ Animate multiple values tied to the same state
|
|
151
|
+
@Composable
|
|
152
|
+
fun ExpandableCard(isExpanded: Boolean) {
|
|
153
|
+
val transition = updateTransition(targetState = isExpanded, label = "card_transition")
|
|
154
|
+
|
|
155
|
+
val cornerRadius by transition.animateDp(label = "corner") {
|
|
156
|
+
if (it) 0.dp else 16.dp
|
|
157
|
+
}
|
|
158
|
+
val elevation by transition.animateDp(label = "elevation") {
|
|
159
|
+
if (it) 8.dp else 2.dp
|
|
160
|
+
}
|
|
161
|
+
val backgroundColor by transition.animateColor(label = "bg") {
|
|
162
|
+
if (it) MaterialTheme.colorScheme.primaryContainer
|
|
163
|
+
else MaterialTheme.colorScheme.surface
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
Card(
|
|
167
|
+
shape = RoundedCornerShape(cornerRadius),
|
|
168
|
+
colors = CardDefaults.cardColors(containerColor = backgroundColor)
|
|
169
|
+
) { ... }
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Screen Transitions (Compose Navigation)
|
|
176
|
+
|
|
177
|
+
```kotlin
|
|
178
|
+
// ✅ Global transitions in NavHost
|
|
179
|
+
NavHost(
|
|
180
|
+
navController = navController,
|
|
181
|
+
startDestination = HomeRoute,
|
|
182
|
+
enterTransition = { slideInHorizontally { it } + fadeIn() },
|
|
183
|
+
exitTransition = { slideOutHorizontally { -it } + fadeOut() },
|
|
184
|
+
popEnterTransition = { slideInHorizontally { -it } + fadeIn() },
|
|
185
|
+
popExitTransition = { slideOutHorizontally { it } + fadeOut() }
|
|
186
|
+
) {
|
|
187
|
+
composable<HomeRoute> { HomeScreen() }
|
|
188
|
+
composable<DetailRoute> { DetailScreen() }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ✅ Per-route transitions
|
|
192
|
+
composable<DetailRoute>(
|
|
193
|
+
enterTransition = { fadeIn(tween(300)) },
|
|
194
|
+
exitTransition = { fadeOut(tween(300)) }
|
|
195
|
+
) { DetailScreen() }
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Animation Specs
|
|
201
|
+
|
|
202
|
+
```kotlin
|
|
203
|
+
// tween — linear/eased time-based
|
|
204
|
+
tween(durationMillis = 300, easing = FastOutSlowInEasing)
|
|
205
|
+
|
|
206
|
+
// spring — physics-based, natural feel
|
|
207
|
+
spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessMedium)
|
|
208
|
+
|
|
209
|
+
// keyframes — precise time-value control
|
|
210
|
+
keyframes {
|
|
211
|
+
durationMillis = 400
|
|
212
|
+
0f at 0
|
|
213
|
+
1.2f at 200
|
|
214
|
+
1f at 400
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// snap — instant, no animation
|
|
218
|
+
snap()
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Accessibility — Reduced Motion
|
|
224
|
+
|
|
225
|
+
```kotlin
|
|
226
|
+
// ✅ Respect user's reduce motion setting
|
|
227
|
+
@Composable
|
|
228
|
+
fun AccessibleAnimation(isVisible: Boolean) {
|
|
229
|
+
val reduceMotion = LocalReduceMotion.current // not yet in stable — use workaround
|
|
230
|
+
|
|
231
|
+
// Workaround until LocalReduceMotion is stable
|
|
232
|
+
val animationDuration = if (isSystemInDarkTheme()) 0 else 300
|
|
233
|
+
|
|
234
|
+
AnimatedVisibility(
|
|
235
|
+
visible = isVisible,
|
|
236
|
+
enter = fadeIn(tween(animationDuration)),
|
|
237
|
+
exit = fadeOut(tween(animationDuration))
|
|
238
|
+
) { ... }
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Infinite Animation
|
|
245
|
+
|
|
246
|
+
```kotlin
|
|
247
|
+
// ✅ Loading shimmer / pulse
|
|
248
|
+
@Composable
|
|
249
|
+
fun PulsingDot() {
|
|
250
|
+
val infiniteTransition = rememberInfiniteTransition(label = "pulse")
|
|
251
|
+
val scale by infiniteTransition.animateFloat(
|
|
252
|
+
initialValue = 0.8f,
|
|
253
|
+
targetValue = 1.2f,
|
|
254
|
+
animationSpec = infiniteRepeatable(
|
|
255
|
+
animation = tween(600),
|
|
256
|
+
repeatMode = RepeatMode.Reverse
|
|
257
|
+
),
|
|
258
|
+
label = "scale"
|
|
259
|
+
)
|
|
260
|
+
Box(modifier = Modifier.scale(scale).size(12.dp).background(Color.Blue, CircleShape))
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Anti-Patterns
|
|
267
|
+
|
|
268
|
+
- Animating on every recomposition — animation state must be stable
|
|
269
|
+
- Missing `label` parameter — breaks Animation Inspector in Android Studio
|
|
270
|
+
- Using `Thread.sleep()` or `delay()` for animation timing — use animation specs
|
|
271
|
+
- Animating too many properties simultaneously — causes jank
|
|
272
|
+
- Not testing with reduced motion — accessibility oversight
|
|
273
|
+
- Using `LaunchedEffect` + manual delay for show/hide — use `AnimatedVisibility`
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Related Skills
|
|
278
|
+
- `compose` — Compose fundamentals and state
|
|
279
|
+
- `compose-performance` — avoiding recomposition during animation
|
|
280
|
+
- `material3` — Material 3 motion system
|
|
281
|
+
- `loading-strategy` — animated loading states
|