ui-ux-consultant-cli 1.0.0-beta.1
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/assets/ui-ux-consultant/SKILL.md +844 -0
- package/assets/ui-ux-consultant/references/accessibility.md +175 -0
- package/assets/ui-ux-consultant/references/alt-libraries.md +90 -0
- package/assets/ui-ux-consultant/references/animations.md +448 -0
- package/assets/ui-ux-consultant/references/catalog/colors.md +91 -0
- package/assets/ui-ux-consultant/references/catalog/fonts.md +363 -0
- package/assets/ui-ux-consultant/references/catalog/products.md +340 -0
- package/assets/ui-ux-consultant/references/catalog/styles.md +165 -0
- package/assets/ui-ux-consultant/references/components.md +1116 -0
- package/assets/ui-ux-consultant/references/patterns.md +600 -0
- package/assets/ui-ux-consultant/references/performance.md +198 -0
- package/assets/ui-ux-consultant/references/stacks/astro.md +382 -0
- package/assets/ui-ux-consultant/references/stacks/flutter.md +308 -0
- package/assets/ui-ux-consultant/references/stacks/html-tailwind.md +415 -0
- package/assets/ui-ux-consultant/references/stacks/jetpack-compose.md +333 -0
- package/assets/ui-ux-consultant/references/stacks/laravel.md +521 -0
- package/assets/ui-ux-consultant/references/stacks/nextjs.md +275 -0
- package/assets/ui-ux-consultant/references/stacks/nuxt-ui.md +384 -0
- package/assets/ui-ux-consultant/references/stacks/nuxtjs.md +264 -0
- package/assets/ui-ux-consultant/references/stacks/react-native.md +346 -0
- package/assets/ui-ux-consultant/references/stacks/react.md +268 -0
- package/assets/ui-ux-consultant/references/stacks/shadcn.md +485 -0
- package/assets/ui-ux-consultant/references/stacks/svelte.md +429 -0
- package/assets/ui-ux-consultant/references/stacks/swiftui.md +336 -0
- package/assets/ui-ux-consultant/references/stacks/threejs.md +366 -0
- package/assets/ui-ux-consultant/references/stacks/vue.md +272 -0
- package/assets/ui-ux-consultant/references/theming.md +701 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +130 -0
- package/package.json +51 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# Jetpack Compose Reference
|
|
2
|
+
|
|
3
|
+
## When to Read
|
|
4
|
+
Read this file when building Android apps with Jetpack Compose (Kotlin) — composables, state, ViewModel, LazyColumn, navigation, Material 3 theming, or accessibility.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Recommended Libraries
|
|
9
|
+
|
|
10
|
+
| Library | Purpose |
|
|
11
|
+
|---|---|
|
|
12
|
+
| Coil | Image loading (`AsyncImage`) |
|
|
13
|
+
| Hilt | Dependency injection |
|
|
14
|
+
| Room | Local database with Flow |
|
|
15
|
+
| Retrofit + OkHttp | HTTP networking |
|
|
16
|
+
| Accompanist | Compose utilities (pager, permissions) |
|
|
17
|
+
| Voyager | Alternative navigation library |
|
|
18
|
+
| DataStore | Preferences persistence (replaces SharedPreferences) |
|
|
19
|
+
| Kotlin Serialization | JSON parsing |
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Style Recommendations
|
|
24
|
+
|
|
25
|
+
- Material 3 everywhere — not M2; `MaterialTheme` from `androidx.compose.material3`
|
|
26
|
+
- Dynamic color (`dynamicColorScheme`) on Android 12+ — fallback for older devices
|
|
27
|
+
- 8dp spacing scale: 4, 8, 12, 16, 24, 32, 48
|
|
28
|
+
- `MaterialTheme.typography` tokens — not hardcoded `sp` values
|
|
29
|
+
- `MaterialTheme.colorScheme` tokens — not raw `Color(0xFF...)` in composables
|
|
30
|
+
- Shape: `MaterialTheme.shapes.medium` for cards, `.small` for chips
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Core Paradigm
|
|
35
|
+
|
|
36
|
+
UI = composable functions. State hoisting: lift state up to the lowest common ancestor that needs it. `remember` for local ephemeral state; `ViewModel` + `StateFlow` for lifecycle-aware state.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Top UX Patterns (with Code)
|
|
41
|
+
|
|
42
|
+
### ViewModel with StateFlow
|
|
43
|
+
```kotlin
|
|
44
|
+
class UserViewModel : ViewModel() {
|
|
45
|
+
private val _users = MutableStateFlow<List<User>>(emptyList())
|
|
46
|
+
val users: StateFlow<List<User>> = _users.asStateFlow()
|
|
47
|
+
|
|
48
|
+
private val _isLoading = MutableStateFlow(false)
|
|
49
|
+
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
|
|
50
|
+
|
|
51
|
+
fun loadUsers() = viewModelScope.launch {
|
|
52
|
+
_isLoading.value = true
|
|
53
|
+
try {
|
|
54
|
+
_users.value = userRepository.getAll()
|
|
55
|
+
} finally {
|
|
56
|
+
_isLoading.value = false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// In composable:
|
|
62
|
+
@Composable
|
|
63
|
+
fun UserListScreen(viewModel: UserViewModel = hiltViewModel()) {
|
|
64
|
+
val users by viewModel.users.collectAsStateWithLifecycle()
|
|
65
|
+
val isLoading by viewModel.isLoading.collectAsStateWithLifecycle()
|
|
66
|
+
|
|
67
|
+
LaunchedEffect(Unit) { viewModel.loadUsers() }
|
|
68
|
+
|
|
69
|
+
if (isLoading) {
|
|
70
|
+
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
|
71
|
+
CircularProgressIndicator()
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
UserList(users = users)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Scaffold + TopAppBar + FAB
|
|
80
|
+
```kotlin
|
|
81
|
+
Scaffold(
|
|
82
|
+
topBar = {
|
|
83
|
+
TopAppBar(
|
|
84
|
+
title = { Text("Dashboard") },
|
|
85
|
+
navigationIcon = {
|
|
86
|
+
IconButton(onClick = { navController.navigateUp() }) {
|
|
87
|
+
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
actions = {
|
|
91
|
+
IconButton(onClick = { /* open settings */ }) {
|
|
92
|
+
Icon(Icons.Default.Settings, contentDescription = "Settings")
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
},
|
|
97
|
+
floatingActionButton = {
|
|
98
|
+
FloatingActionButton(onClick = onAdd) {
|
|
99
|
+
Icon(Icons.Default.Add, contentDescription = "Add item")
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
) { paddingValues ->
|
|
103
|
+
LazyColumn(
|
|
104
|
+
modifier = Modifier.padding(paddingValues),
|
|
105
|
+
contentPadding = PaddingValues(16.dp),
|
|
106
|
+
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
107
|
+
) {
|
|
108
|
+
items(users, key = { it.id }) { user ->
|
|
109
|
+
UserCard(user = user)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### LazyColumn (always for long lists)
|
|
116
|
+
```kotlin
|
|
117
|
+
LazyColumn(
|
|
118
|
+
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
|
119
|
+
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
120
|
+
) {
|
|
121
|
+
items(items = users, key = { it.id }) { user ->
|
|
122
|
+
UserCard(user = user)
|
|
123
|
+
}
|
|
124
|
+
item {
|
|
125
|
+
Spacer(Modifier.height(80.dp)) // clearance for FAB
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Never use Column for long lists — renders all items eagerly
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Material 3 theming with dynamic color
|
|
132
|
+
```kotlin
|
|
133
|
+
@Composable
|
|
134
|
+
fun AppTheme(
|
|
135
|
+
darkTheme: Boolean = isSystemInDarkTheme(),
|
|
136
|
+
content: @Composable () -> Unit
|
|
137
|
+
) {
|
|
138
|
+
val colorScheme = when {
|
|
139
|
+
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
|
140
|
+
val context = LocalContext.current
|
|
141
|
+
if (darkTheme) dynamicDarkColorScheme(context)
|
|
142
|
+
else dynamicLightColorScheme(context)
|
|
143
|
+
}
|
|
144
|
+
darkTheme -> darkColorScheme(primary = Color(0xFF2563EB))
|
|
145
|
+
else -> lightColorScheme(primary = Color(0xFF2563EB))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
MaterialTheme(
|
|
149
|
+
colorScheme = colorScheme,
|
|
150
|
+
typography = Typography(),
|
|
151
|
+
content = content
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Navigation (Compose Navigation)
|
|
157
|
+
```kotlin
|
|
158
|
+
val navController = rememberNavController()
|
|
159
|
+
|
|
160
|
+
NavHost(navController = navController, startDestination = "home") {
|
|
161
|
+
composable("home") {
|
|
162
|
+
HomeScreen(
|
|
163
|
+
onNavigateToDetail = { id -> navController.navigate("detail/$id") }
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
composable(
|
|
167
|
+
route = "detail/{id}",
|
|
168
|
+
arguments = listOf(navArgument("id") { type = NavType.StringType })
|
|
169
|
+
) { backStackEntry ->
|
|
170
|
+
val id = backStackEntry.arguments?.getString("id") ?: return@composable
|
|
171
|
+
DetailScreen(id = id, onBack = { navController.navigateUp() })
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### State hoisting pattern
|
|
177
|
+
```kotlin
|
|
178
|
+
// Stateful (screen-level)
|
|
179
|
+
@Composable
|
|
180
|
+
fun SearchScreen(viewModel: SearchViewModel = hiltViewModel()) {
|
|
181
|
+
val query by viewModel.query.collectAsStateWithLifecycle()
|
|
182
|
+
SearchBar(
|
|
183
|
+
query = query,
|
|
184
|
+
onQueryChange = viewModel::onQueryChange,
|
|
185
|
+
onSearch = viewModel::search
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Stateless (reusable)
|
|
190
|
+
@Composable
|
|
191
|
+
fun SearchBar(
|
|
192
|
+
query: String,
|
|
193
|
+
onQueryChange: (String) -> Unit,
|
|
194
|
+
onSearch: () -> Unit,
|
|
195
|
+
modifier: Modifier = Modifier
|
|
196
|
+
) {
|
|
197
|
+
OutlinedTextField(
|
|
198
|
+
value = query,
|
|
199
|
+
onValueChange = onQueryChange,
|
|
200
|
+
modifier = modifier.fillMaxWidth(),
|
|
201
|
+
placeholder = { Text("Search…") },
|
|
202
|
+
trailingIcon = {
|
|
203
|
+
IconButton(onClick = onSearch) {
|
|
204
|
+
Icon(Icons.Default.Search, contentDescription = "Search")
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
|
208
|
+
keyboardActions = KeyboardActions(onSearch = { onSearch() })
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Accessible icon
|
|
214
|
+
```kotlin
|
|
215
|
+
Icon(
|
|
216
|
+
imageVector = Icons.Default.Delete,
|
|
217
|
+
contentDescription = "Delete item", // never null on interactive icons
|
|
218
|
+
modifier = Modifier
|
|
219
|
+
.clickable(
|
|
220
|
+
role = Role.Button,
|
|
221
|
+
onClickLabel = "Delete this item"
|
|
222
|
+
) { onDelete() }
|
|
223
|
+
)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### derivedStateOf for expensive computation
|
|
227
|
+
```kotlin
|
|
228
|
+
val filteredItems by remember {
|
|
229
|
+
derivedStateOf {
|
|
230
|
+
// Only recomputes when searchQuery or items changes
|
|
231
|
+
items.filter { it.name.contains(searchQuery, ignoreCase = true) }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Pull-to-refresh
|
|
237
|
+
```kotlin
|
|
238
|
+
val pullRefreshState = rememberPullRefreshState(
|
|
239
|
+
refreshing = isRefreshing,
|
|
240
|
+
onRefresh = { viewModel.refresh() }
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
Box(Modifier.pullRefresh(pullRefreshState)) {
|
|
244
|
+
LazyColumn { /* items */ }
|
|
245
|
+
PullRefreshIndicator(
|
|
246
|
+
refreshing = isRefreshing,
|
|
247
|
+
state = pullRefreshState,
|
|
248
|
+
modifier = Modifier.align(Alignment.TopCenter)
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Best Practices by Category
|
|
256
|
+
|
|
257
|
+
### Composables
|
|
258
|
+
- Keep composables small and single-purpose — extract at ~30 lines
|
|
259
|
+
- Stateless composables take data + callbacks — easier to test and preview
|
|
260
|
+
- `modifier: Modifier = Modifier` as last parameter on every composable
|
|
261
|
+
- `@Preview` annotations with realistic data — not empty states only
|
|
262
|
+
- `@Stable` or `@Immutable` on data classes passed to composables for skipping recomposition
|
|
263
|
+
|
|
264
|
+
### State
|
|
265
|
+
- `remember { mutableStateOf() }` only for local transient state (toggle, input draft)
|
|
266
|
+
- ViewModel + `StateFlow` for anything that survives config changes
|
|
267
|
+
- `collectAsStateWithLifecycle()` not `collectAsState()` — stops collection in background
|
|
268
|
+
- `rememberSaveable` for state that must survive process death (text field drafts)
|
|
269
|
+
- Single source of truth: don't duplicate state between ViewModel and composable
|
|
270
|
+
|
|
271
|
+
### Lists
|
|
272
|
+
- `LazyColumn` / `LazyRow` for any list that might scroll
|
|
273
|
+
- `key = { item.id }` in `items()` — stable keys prevent unnecessary recomposition on reorder
|
|
274
|
+
- `contentPadding` on lazy lists to avoid clipping at edges
|
|
275
|
+
- `Arrangement.spacedBy(dp)` instead of per-item `Spacer`
|
|
276
|
+
- `StickyHeader` inside `LazyColumn` for grouped lists
|
|
277
|
+
|
|
278
|
+
### Navigation
|
|
279
|
+
- One `NavHost` at the root of the app
|
|
280
|
+
- Pass only IDs between destinations — fetch data in the destination's ViewModel
|
|
281
|
+
- `popUpTo` + `saveState` for bottom nav tabs (preserve back stacks)
|
|
282
|
+
- `BackHandler` for custom back behavior in leaf screens
|
|
283
|
+
|
|
284
|
+
### Material 3 Components
|
|
285
|
+
- `Card` not `Surface` + manual shape/elevation — `Card` handles semantics
|
|
286
|
+
- `FilledButton` for primary action, `OutlinedButton` secondary, `TextButton` tertiary
|
|
287
|
+
- `SnackbarHostState` + `Scaffold`'s `snackbarHost` for feedback messages
|
|
288
|
+
- `AlertDialog` for confirmations — not custom dialogs
|
|
289
|
+
- `ModalBottomSheet` for secondary flows (not navigation)
|
|
290
|
+
|
|
291
|
+
### Accessibility
|
|
292
|
+
- `contentDescription` on every `Icon` that's interactive — `null` only for decorative icons
|
|
293
|
+
- `Modifier.semantics { role = Role.Button }` when using `clickable` on non-Button
|
|
294
|
+
- `Modifier.clearAndSetSemantics { }` to replace auto-generated semantics
|
|
295
|
+
- Min touch target: 48dp × 48dp — use `Modifier.minimumInteractiveComponentSize()`
|
|
296
|
+
- Test with TalkBack on real device
|
|
297
|
+
|
|
298
|
+
### Animation
|
|
299
|
+
- `AnimatedVisibility` for enter/exit transitions
|
|
300
|
+
- `animateContentSize()` for layout size changes
|
|
301
|
+
- `updateTransition` for state-machine animations
|
|
302
|
+
- `Crossfade` for content swapping
|
|
303
|
+
- `spring()` for natural motion; `tween()` for controlled duration
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Common Anti-Patterns
|
|
308
|
+
|
|
309
|
+
1. `Column` with long lists — renders all items eagerly; freezes UI; use `LazyColumn`
|
|
310
|
+
2. No `key` in `items()` — poor diff performance on reorder/insert; React-like index issues
|
|
311
|
+
3. Heavy computation in composable body — runs on every recomposition; use `remember { derivedStateOf { } }`
|
|
312
|
+
4. `mutableStateListOf` in ViewModel — doesn't survive config change; use `StateFlow<List<T>>`
|
|
313
|
+
5. Missing `contentDescription` on icons — TalkBack announces "unlabeled button"
|
|
314
|
+
6. `collectAsState()` without lifecycle awareness — continues collection in background, wastes battery
|
|
315
|
+
7. State inside `LazyListScope` lambdas — `remember` doesn't work inside `items {}` blocks
|
|
316
|
+
8. `navController` passed deep into composables — pass lambdas instead; keeps composables testable
|
|
317
|
+
9. Hardcoded colors/dimensions — breaks theming; use `MaterialTheme.colorScheme` / `MaterialTheme.spacing`
|
|
318
|
+
10. Missing `modifier` parameter on reusable composables — callers can't adjust layout
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## Performance Checklist
|
|
323
|
+
|
|
324
|
+
- [ ] `LazyColumn`/`LazyRow` for lists (not `Column`/`Row`)
|
|
325
|
+
- [ ] `key = { item.id }` in all `items()` calls
|
|
326
|
+
- [ ] `collectAsStateWithLifecycle()` not `collectAsState()` (lifecycle-aware)
|
|
327
|
+
- [ ] `remember { derivedStateOf { } }` for expensive computations
|
|
328
|
+
- [ ] `@Stable` or `@Immutable` on data classes passed to composables
|
|
329
|
+
- [ ] `Modifier.fillMaxWidth()` before `weight()` in nested layouts
|
|
330
|
+
- [ ] `rememberSaveable` for user input that must survive process death
|
|
331
|
+
- [ ] `minimumInteractiveComponentSize()` on all clickable elements (48dp min)
|
|
332
|
+
- [ ] Profile with Layout Inspector + Composition trace in Android Studio
|
|
333
|
+
- [ ] Baseline Profiles configured for startup performance
|