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,214 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: keyboard-navigation
|
|
3
|
+
description: >
|
|
4
|
+
Keyboard and IME (soft keyboard) navigation in Android/Compose.
|
|
5
|
+
Load this skill when managing focus traversal between fields, handling
|
|
6
|
+
IME actions (Next, Done, Search), avoiding content hidden behind the
|
|
7
|
+
keyboard, managing keyboard visibility, or supporting hardware keyboards.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Keyboard Navigation
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
Keyboard navigation covers two concerns: soft keyboard (IME) management for touch devices, and hardware keyboard support for accessibility and productivity. Both require explicit focus management, correct IME action wiring, and ensuring content remains accessible when the keyboard is visible.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Always set `imeAction` to match the field's role (`Next` for mid-form, `Done`/`Search` for last)
|
|
20
|
+
- Never let the keyboard obscure active input fields — use `imePadding()` or `imeNestedScroll()`
|
|
21
|
+
- Use `FocusRequester` for programmatic focus — never simulate clicks
|
|
22
|
+
- Form fields must chain focus in logical order — left-to-right, top-to-bottom (LTR) or right-to-left (RTL)
|
|
23
|
+
- Request focus on first field only after the screen is fully composed
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## IME Actions
|
|
28
|
+
|
|
29
|
+
```kotlin
|
|
30
|
+
// ✅ Chain fields with Next — last field uses Done/Search/Send
|
|
31
|
+
@Composable
|
|
32
|
+
fun LoginForm(onSubmit: () -> Unit) {
|
|
33
|
+
val focusManager = LocalFocusManager.current
|
|
34
|
+
var email by remember { mutableStateOf("") }
|
|
35
|
+
var password by remember { mutableStateOf("") }
|
|
36
|
+
|
|
37
|
+
Column(verticalArrangement = Arrangement.spacedBy(AppSpacing.md)) {
|
|
38
|
+
OutlinedTextField(
|
|
39
|
+
value = email,
|
|
40
|
+
onValueChange = { email = it },
|
|
41
|
+
label = { Text("Email") },
|
|
42
|
+
keyboardOptions = KeyboardOptions(
|
|
43
|
+
keyboardType = KeyboardType.Email,
|
|
44
|
+
imeAction = ImeAction.Next // ✅ moves focus to next field
|
|
45
|
+
),
|
|
46
|
+
keyboardActions = KeyboardActions(
|
|
47
|
+
onNext = { focusManager.moveFocus(FocusDirection.Down) }
|
|
48
|
+
),
|
|
49
|
+
singleLine = true
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
OutlinedTextField(
|
|
53
|
+
value = password,
|
|
54
|
+
onValueChange = { password = it },
|
|
55
|
+
label = { Text("Password") },
|
|
56
|
+
visualTransformation = PasswordVisualTransformation(),
|
|
57
|
+
keyboardOptions = KeyboardOptions(
|
|
58
|
+
keyboardType = KeyboardType.Password,
|
|
59
|
+
imeAction = ImeAction.Done // ✅ last field — submit
|
|
60
|
+
),
|
|
61
|
+
keyboardActions = KeyboardActions(
|
|
62
|
+
onDone = {
|
|
63
|
+
focusManager.clearFocus()
|
|
64
|
+
onSubmit()
|
|
65
|
+
}
|
|
66
|
+
),
|
|
67
|
+
singleLine = true
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## FocusRequester
|
|
76
|
+
|
|
77
|
+
```kotlin
|
|
78
|
+
// ✅ Programmatic focus after screen appears
|
|
79
|
+
@Composable
|
|
80
|
+
fun SearchScreen() {
|
|
81
|
+
val focusRequester = remember { FocusRequester() }
|
|
82
|
+
|
|
83
|
+
OutlinedTextField(
|
|
84
|
+
modifier = Modifier.focusRequester(focusRequester),
|
|
85
|
+
value = query,
|
|
86
|
+
onValueChange = { query = it },
|
|
87
|
+
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
|
88
|
+
keyboardActions = KeyboardActions(onSearch = { onSearch(query) })
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
LaunchedEffect(Unit) {
|
|
92
|
+
focusRequester.requestFocus() // ✅ focus after composition
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ✅ Chaining focus manually across fields
|
|
97
|
+
@Composable
|
|
98
|
+
fun MultiFieldForm() {
|
|
99
|
+
val (firstFocus, secondFocus, thirdFocus) = remember { FocusRequester.createRefs() }
|
|
100
|
+
|
|
101
|
+
OutlinedTextField(
|
|
102
|
+
modifier = Modifier.focusRequester(firstFocus),
|
|
103
|
+
keyboardActions = KeyboardActions(onNext = { secondFocus.requestFocus() }),
|
|
104
|
+
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
|
105
|
+
...
|
|
106
|
+
)
|
|
107
|
+
OutlinedTextField(
|
|
108
|
+
modifier = Modifier.focusRequester(secondFocus),
|
|
109
|
+
keyboardActions = KeyboardActions(onNext = { thirdFocus.requestFocus() }),
|
|
110
|
+
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
|
111
|
+
...
|
|
112
|
+
)
|
|
113
|
+
OutlinedTextField(
|
|
114
|
+
modifier = Modifier.focusRequester(thirdFocus),
|
|
115
|
+
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
|
116
|
+
...
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Avoiding Content Hidden Behind Keyboard
|
|
124
|
+
|
|
125
|
+
```kotlin
|
|
126
|
+
// ✅ imePadding on scrollable containers
|
|
127
|
+
@Composable
|
|
128
|
+
fun FormScreen() {
|
|
129
|
+
Column(
|
|
130
|
+
modifier = Modifier
|
|
131
|
+
.fillMaxSize()
|
|
132
|
+
.verticalScroll(rememberScrollState())
|
|
133
|
+
.imePadding() // ✅ pushes content above keyboard
|
|
134
|
+
.padding(AppSpacing.md)
|
|
135
|
+
) {
|
|
136
|
+
// form fields
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ✅ WindowInsets approach in Scaffold
|
|
141
|
+
Scaffold(
|
|
142
|
+
modifier = Modifier.imePadding()
|
|
143
|
+
) { padding ->
|
|
144
|
+
Column(modifier = Modifier.padding(padding)) { ... }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ✅ For LazyColumn — imeNestedScroll + imePadding
|
|
148
|
+
LazyColumn(
|
|
149
|
+
modifier = Modifier
|
|
150
|
+
.fillMaxSize()
|
|
151
|
+
.imePadding()
|
|
152
|
+
.imeNestedScroll()
|
|
153
|
+
) { ... }
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Keyboard Visibility Control
|
|
159
|
+
|
|
160
|
+
```kotlin
|
|
161
|
+
// ✅ Show keyboard programmatically (Compose)
|
|
162
|
+
val keyboardController = LocalSoftwareKeyboardController.current
|
|
163
|
+
|
|
164
|
+
Button(onClick = { keyboardController?.show() }) { Text("Open Keyboard") }
|
|
165
|
+
Button(onClick = { keyboardController?.hide() }) { Text("Close Keyboard") }
|
|
166
|
+
|
|
167
|
+
// ✅ Hide keyboard on outside tap
|
|
168
|
+
@Composable
|
|
169
|
+
fun DismissKeyboardOnTap(content: @Composable () -> Unit) {
|
|
170
|
+
val focusManager = LocalFocusManager.current
|
|
171
|
+
Box(
|
|
172
|
+
modifier = Modifier
|
|
173
|
+
.fillMaxSize()
|
|
174
|
+
.pointerInput(Unit) {
|
|
175
|
+
detectTapGestures(onTap = { focusManager.clearFocus() })
|
|
176
|
+
}
|
|
177
|
+
) {
|
|
178
|
+
content()
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Keyboard Type Reference
|
|
186
|
+
|
|
187
|
+
| KeyboardType | Use For |
|
|
188
|
+
|---|---|
|
|
189
|
+
| `Text` | General text input |
|
|
190
|
+
| `Email` | Email addresses |
|
|
191
|
+
| `Number` | Numeric only |
|
|
192
|
+
| `Phone` | Phone numbers |
|
|
193
|
+
| `Password` | Password (masked) |
|
|
194
|
+
| `NumberPassword` | PIN / numeric password |
|
|
195
|
+
| `Uri` | URLs |
|
|
196
|
+
| `Decimal` | Decimal numbers |
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Anti-Patterns
|
|
201
|
+
|
|
202
|
+
- Not setting `imeAction` — keyboard shows a random action button
|
|
203
|
+
- Using `ImeAction.Done` on non-last fields — breaks tab/next navigation
|
|
204
|
+
- Not adding `imePadding()` to scrollable forms — keyboard hides the active field
|
|
205
|
+
- Calling `requestFocus()` outside `LaunchedEffect` — crashes before composition completes
|
|
206
|
+
- Using `WindowManager.hideSoftInputFromWindow()` directly — deprecated, use `SoftwareKeyboardController`
|
|
207
|
+
- Setting `keyboardType = KeyboardType.Number` for phone numbers — use `Phone` type instead
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Related Skills
|
|
212
|
+
- `compose` — Compose fundamentals and Modifier system
|
|
213
|
+
- `bottom-sheet-pattern` — keyboard interaction with bottom sheets
|
|
214
|
+
- `accessibility` — focus order and hardware keyboard support
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: loading-strategy
|
|
3
|
+
description: >
|
|
4
|
+
Strategies for showing loading states in Android/Compose applications.
|
|
5
|
+
Load this skill when implementing skeleton screens, shimmer effects,
|
|
6
|
+
pull-to-refresh, progressive loading, pagination loading indicators,
|
|
7
|
+
or deciding between spinner vs skeleton vs placeholder approaches.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Loading Strategy
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
Loading strategy defines how the UI communicates data-fetching progress to the user. The choice between spinner, skeleton, shimmer, and placeholder affects perceived performance significantly. A consistent, well-defined loading strategy prevents layout shifts and provides a smoother user experience.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Use **skeleton/shimmer** for initial content load — never a full-screen spinner
|
|
20
|
+
- Use **inline spinner** for actions (button submit, pull-to-refresh)
|
|
21
|
+
- Never block the entire screen with a loading overlay unless the action is destructive and non-cancellable
|
|
22
|
+
- Loading state must be part of the UI state model — never a separate boolean flag
|
|
23
|
+
- Skeleton dimensions must match the real content dimensions to prevent layout shift
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## UI State Model
|
|
28
|
+
|
|
29
|
+
```kotlin
|
|
30
|
+
// ✅ Loading is part of a sealed state — not a separate flag
|
|
31
|
+
sealed interface UiState<out T> {
|
|
32
|
+
data object Loading : UiState<Nothing>
|
|
33
|
+
data class Success<T>(val data: T) : UiState<T>
|
|
34
|
+
data class Error(val message: String) : UiState<Nothing>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ✅ In ViewModel
|
|
38
|
+
class UserListViewModel : ViewModel() {
|
|
39
|
+
private val _state = MutableStateFlow<UiState<List<User>>>(UiState.Loading)
|
|
40
|
+
val state: StateFlow<UiState<List<User>>> = _state.asStateFlow()
|
|
41
|
+
|
|
42
|
+
fun loadUsers() {
|
|
43
|
+
viewModelScope.launch {
|
|
44
|
+
_state.value = UiState.Loading
|
|
45
|
+
_state.value = repository.getUsers().fold(
|
|
46
|
+
onSuccess = { UiState.Success(it) },
|
|
47
|
+
onFailure = { UiState.Error(it.message ?: "Unknown error") }
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Skeleton Screen
|
|
57
|
+
|
|
58
|
+
```kotlin
|
|
59
|
+
// ✅ Skeleton matches real item dimensions
|
|
60
|
+
@Composable
|
|
61
|
+
fun UserItemSkeleton(modifier: Modifier = Modifier) {
|
|
62
|
+
Row(
|
|
63
|
+
modifier = modifier
|
|
64
|
+
.fillMaxWidth()
|
|
65
|
+
.padding(horizontal = AppSpacing.md, vertical = AppSpacing.sm),
|
|
66
|
+
horizontalArrangement = Arrangement.spacedBy(AppSpacing.md)
|
|
67
|
+
) {
|
|
68
|
+
// Avatar placeholder
|
|
69
|
+
Box(
|
|
70
|
+
modifier = Modifier
|
|
71
|
+
.size(40.dp)
|
|
72
|
+
.clip(CircleShape)
|
|
73
|
+
.shimmer()
|
|
74
|
+
)
|
|
75
|
+
Column(verticalArrangement = Arrangement.spacedBy(AppSpacing.xs)) {
|
|
76
|
+
// Name placeholder
|
|
77
|
+
Box(
|
|
78
|
+
modifier = Modifier
|
|
79
|
+
.fillMaxWidth(0.5f)
|
|
80
|
+
.height(16.dp)
|
|
81
|
+
.clip(MaterialTheme.shapes.small)
|
|
82
|
+
.shimmer()
|
|
83
|
+
)
|
|
84
|
+
// Subtitle placeholder
|
|
85
|
+
Box(
|
|
86
|
+
modifier = Modifier
|
|
87
|
+
.fillMaxWidth(0.35f)
|
|
88
|
+
.height(12.dp)
|
|
89
|
+
.clip(MaterialTheme.shapes.small)
|
|
90
|
+
.shimmer()
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ✅ Shimmer modifier using animated brush
|
|
97
|
+
@Composable
|
|
98
|
+
fun Modifier.shimmer(): Modifier {
|
|
99
|
+
val transition = rememberInfiniteTransition(label = "shimmer")
|
|
100
|
+
val translateAnim by transition.animateFloat(
|
|
101
|
+
initialValue = 0f,
|
|
102
|
+
targetValue = 1000f,
|
|
103
|
+
animationSpec = infiniteRepeatable(
|
|
104
|
+
animation = tween(durationMillis = 1200, easing = LinearEasing),
|
|
105
|
+
repeatMode = RepeatMode.Restart
|
|
106
|
+
),
|
|
107
|
+
label = "shimmer_translate"
|
|
108
|
+
)
|
|
109
|
+
val brush = Brush.linearGradient(
|
|
110
|
+
colors = listOf(
|
|
111
|
+
MaterialTheme.colorScheme.surfaceVariant,
|
|
112
|
+
MaterialTheme.colorScheme.surface,
|
|
113
|
+
MaterialTheme.colorScheme.surfaceVariant
|
|
114
|
+
),
|
|
115
|
+
start = Offset(translateAnim - 200f, 0f),
|
|
116
|
+
end = Offset(translateAnim, 0f)
|
|
117
|
+
)
|
|
118
|
+
return this.background(brush)
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## List Loading
|
|
125
|
+
|
|
126
|
+
```kotlin
|
|
127
|
+
// ✅ Show skeleton list during initial load
|
|
128
|
+
@Composable
|
|
129
|
+
fun UserListScreen(viewModel: UserListViewModel = hiltViewModel()) {
|
|
130
|
+
val state by viewModel.state.collectAsStateWithLifecycle()
|
|
131
|
+
|
|
132
|
+
when (state) {
|
|
133
|
+
is UiState.Loading -> {
|
|
134
|
+
LazyColumn {
|
|
135
|
+
items(6) { UserItemSkeleton() } // fixed count matches expected content
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
is UiState.Success -> {
|
|
139
|
+
LazyColumn {
|
|
140
|
+
items((state as UiState.Success).data) { user ->
|
|
141
|
+
UserItem(user)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
is UiState.Error -> {
|
|
146
|
+
ErrorState(message = (state as UiState.Error).message)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Pull-to-Refresh
|
|
155
|
+
|
|
156
|
+
```kotlin
|
|
157
|
+
// ✅ Pull-to-refresh with PullToRefreshBox (M3)
|
|
158
|
+
@Composable
|
|
159
|
+
fun UserListScreen(viewModel: UserListViewModel = hiltViewModel()) {
|
|
160
|
+
val state by viewModel.state.collectAsStateWithLifecycle()
|
|
161
|
+
val isRefreshing = state is UiState.Loading && viewModel.isRefreshing
|
|
162
|
+
|
|
163
|
+
PullToRefreshBox(
|
|
164
|
+
isRefreshing = isRefreshing,
|
|
165
|
+
onRefresh = viewModel::refresh
|
|
166
|
+
) {
|
|
167
|
+
LazyColumn { ... }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Button Loading State
|
|
175
|
+
|
|
176
|
+
```kotlin
|
|
177
|
+
// ✅ Inline loading indicator in action button
|
|
178
|
+
@Composable
|
|
179
|
+
fun SubmitButton(
|
|
180
|
+
isLoading: Boolean,
|
|
181
|
+
onClick: () -> Unit
|
|
182
|
+
) {
|
|
183
|
+
Button(
|
|
184
|
+
onClick = onClick,
|
|
185
|
+
enabled = !isLoading
|
|
186
|
+
) {
|
|
187
|
+
AnimatedContent(targetState = isLoading, label = "submit_btn") { loading ->
|
|
188
|
+
if (loading) {
|
|
189
|
+
CircularProgressIndicator(
|
|
190
|
+
modifier = Modifier.size(18.dp),
|
|
191
|
+
strokeWidth = 2.dp,
|
|
192
|
+
color = MaterialTheme.colorScheme.onPrimary
|
|
193
|
+
)
|
|
194
|
+
} else {
|
|
195
|
+
Text("Submit")
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Pagination Loading
|
|
205
|
+
|
|
206
|
+
```kotlin
|
|
207
|
+
// ✅ Footer loading indicator for paginated lists
|
|
208
|
+
@Composable
|
|
209
|
+
fun PaginatedList(
|
|
210
|
+
items: List<Item>,
|
|
211
|
+
isLoadingMore: Boolean,
|
|
212
|
+
onLoadMore: () -> Unit
|
|
213
|
+
) {
|
|
214
|
+
LazyColumn {
|
|
215
|
+
items(items) { ItemRow(it) }
|
|
216
|
+
|
|
217
|
+
if (isLoadingMore) {
|
|
218
|
+
item {
|
|
219
|
+
Box(
|
|
220
|
+
modifier = Modifier
|
|
221
|
+
.fillMaxWidth()
|
|
222
|
+
.padding(AppSpacing.md),
|
|
223
|
+
contentAlignment = Alignment.Center
|
|
224
|
+
) {
|
|
225
|
+
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Trigger load more when near end
|
|
232
|
+
LaunchedEffect(items.size) {
|
|
233
|
+
if (items.isNotEmpty()) onLoadMore()
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Anti-Patterns
|
|
241
|
+
|
|
242
|
+
- Full-screen `CircularProgressIndicator` for content loading — use skeleton instead
|
|
243
|
+
- Using a separate `isLoading: Boolean` flag alongside data state — use sealed state
|
|
244
|
+
- Skeleton with wrong dimensions — causes layout shift when content appears
|
|
245
|
+
- Not disabling interactive elements during loading — allows double submissions
|
|
246
|
+
- Showing spinner on every recomposition — tie to actual async state only
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Related Skills
|
|
251
|
+
- `empty-state-strategy` — what to show when loading succeeds but data is empty
|
|
252
|
+
- `state-management` — UI state modeling
|
|
253
|
+
- `compose` — animation and layout fundamentals
|
|
254
|
+
- `paging` — pagination with Paging 3 library
|