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: process-death-recovery
|
|
3
|
+
description: >
|
|
4
|
+
Process death detection, prevention, and recovery strategies for Android.
|
|
5
|
+
Load this skill when designing state that must survive system-initiated
|
|
6
|
+
process kills, implementing session recovery, or testing background kill scenarios.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Process Death Recovery
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
Android can kill an app's process at any time when it's in the background — to free memory. When the user returns, Android recreates the Activity and restores the back stack, but all in-memory state (including ViewModel) is gone. A well-designed app recovers seamlessly without data loss.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Assume process death can happen **at any time** — design accordingly
|
|
20
|
+
- ViewModel does **not** survive process death — only configuration change
|
|
21
|
+
- SavedStateHandle survives process death — use it for critical UI state
|
|
22
|
+
- Persistent storage (Room, DataStore) always survives — use for important data
|
|
23
|
+
- Never show a blank or broken screen after process death recovery
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## What Survives What
|
|
28
|
+
|
|
29
|
+
| Storage | Config Change | Process Death | App Kill (swipe) |
|
|
30
|
+
| ----------------- | ------------- | ------------- | ---------------- |
|
|
31
|
+
| ViewModel | ✅ | ❌ | ❌ |
|
|
32
|
+
| SavedStateHandle | ✅ | ✅ | ❌ |
|
|
33
|
+
| Room / DataStore | ✅ | ✅ | ✅ |
|
|
34
|
+
| SharedPreferences | ✅ | ✅ | ✅ |
|
|
35
|
+
| In-memory cache | ✅ | ❌ | ❌ |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Recovery Strategy by State Type
|
|
40
|
+
|
|
41
|
+
```kotlin
|
|
42
|
+
// ✅ User input — save in SavedStateHandle
|
|
43
|
+
@HiltViewModel
|
|
44
|
+
class FormViewModel @Inject constructor(
|
|
45
|
+
savedStateHandle: SavedStateHandle
|
|
46
|
+
) : ViewModel() {
|
|
47
|
+
val name = savedStateHandle.getStateFlow("name", "")
|
|
48
|
+
val email = savedStateHandle.getStateFlow("email", "")
|
|
49
|
+
|
|
50
|
+
fun onNameChanged(value: String) { savedStateHandle["name"] = value }
|
|
51
|
+
fun onEmailChanged(value: String) { savedStateHandle["email"] = value }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ✅ Loaded data — reload from Room/repository on init
|
|
55
|
+
@HiltViewModel
|
|
56
|
+
class UserListViewModel @Inject constructor(
|
|
57
|
+
private val repository: UserRepository
|
|
58
|
+
) : ViewModel() {
|
|
59
|
+
val users: StateFlow<List<User>> = repository
|
|
60
|
+
.observeUsers() // Room Flow — always fresh after process death
|
|
61
|
+
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ✅ Current screen/selection — save ID in SavedStateHandle, reload data
|
|
65
|
+
@HiltViewModel
|
|
66
|
+
class UserDetailViewModel @Inject constructor(
|
|
67
|
+
savedStateHandle: SavedStateHandle,
|
|
68
|
+
private val repository: UserRepository
|
|
69
|
+
) : ViewModel() {
|
|
70
|
+
private val userId: String = checkNotNull(savedStateHandle["userId"])
|
|
71
|
+
|
|
72
|
+
val user: StateFlow<User?> = repository
|
|
73
|
+
.observeUser(userId) // reload data using saved ID
|
|
74
|
+
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Detecting Process Death vs Fresh Start
|
|
81
|
+
|
|
82
|
+
```kotlin
|
|
83
|
+
// ✅ Detect if Activity was restored after process death
|
|
84
|
+
class MainActivity : ComponentActivity() {
|
|
85
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
86
|
+
super.onCreate(savedInstanceState)
|
|
87
|
+
|
|
88
|
+
if (savedInstanceState != null) {
|
|
89
|
+
// Restored — either config change or process death recovery
|
|
90
|
+
// ViewModel + SavedStateHandle already restored
|
|
91
|
+
} else {
|
|
92
|
+
// Fresh start
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ✅ In ViewModel — check if state needs initialization
|
|
98
|
+
@HiltViewModel
|
|
99
|
+
class OnboardingViewModel @Inject constructor(
|
|
100
|
+
savedStateHandle: SavedStateHandle,
|
|
101
|
+
private val repository: OnboardingRepository
|
|
102
|
+
) : ViewModel() {
|
|
103
|
+
|
|
104
|
+
private val currentStep = savedStateHandle.getStateFlow("step", 0)
|
|
105
|
+
|
|
106
|
+
init {
|
|
107
|
+
// Don't reset step — it may have been restored from SavedStateHandle
|
|
108
|
+
if (currentStep.value == 0) {
|
|
109
|
+
viewModelScope.launch { loadInitialState() }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Incomplete Operations Recovery
|
|
118
|
+
|
|
119
|
+
```kotlin
|
|
120
|
+
// ✅ For critical operations (payment, upload) — use WorkManager
|
|
121
|
+
// WorkManager survives process death and system reboots
|
|
122
|
+
class UploadWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
|
123
|
+
override suspend fun doWork(): Result {
|
|
124
|
+
val fileUri = inputData.getString("file_uri") ?: return Result.failure()
|
|
125
|
+
return try {
|
|
126
|
+
uploadFile(fileUri)
|
|
127
|
+
Result.success()
|
|
128
|
+
} catch (e: Exception) {
|
|
129
|
+
Result.retry()
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Enqueue — survives process death
|
|
135
|
+
WorkManager.getInstance(context)
|
|
136
|
+
.enqueueUniqueWork(
|
|
137
|
+
"upload_${fileId}",
|
|
138
|
+
ExistingWorkPolicy.KEEP,
|
|
139
|
+
OneTimeWorkRequestBuilder<UploadWorker>()
|
|
140
|
+
.setInputData(workDataOf("file_uri" to uri))
|
|
141
|
+
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
|
|
142
|
+
.build()
|
|
143
|
+
)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Session State Recovery
|
|
149
|
+
|
|
150
|
+
```kotlin
|
|
151
|
+
// ✅ Persist session-critical state to DataStore immediately on change
|
|
152
|
+
class SessionRepository @Inject constructor(
|
|
153
|
+
private val dataStore: DataStore<Preferences>
|
|
154
|
+
) {
|
|
155
|
+
suspend fun saveLastViewedUserId(userId: String) {
|
|
156
|
+
dataStore.edit { prefs ->
|
|
157
|
+
prefs[LAST_USER_ID] = userId
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fun observeLastViewedUserId(): Flow<String?> =
|
|
162
|
+
dataStore.data.map { it[LAST_USER_ID] }
|
|
163
|
+
|
|
164
|
+
companion object {
|
|
165
|
+
val LAST_USER_ID = stringPreferencesKey("last_user_id")
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Testing Process Death
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
# ✅ Simulate process death via ADB — put app in background first
|
|
176
|
+
adb shell am kill <package_name>
|
|
177
|
+
|
|
178
|
+
# Then return to app from Recents — should recover gracefully
|
|
179
|
+
|
|
180
|
+
# ✅ Android Studio — use "Terminate Application" button
|
|
181
|
+
# while app is in background (minimized)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
```kotlin
|
|
185
|
+
// ✅ Write tests for SavedStateHandle restoration
|
|
186
|
+
@Test
|
|
187
|
+
fun `state is restored after process death`() {
|
|
188
|
+
val savedState = SavedStateHandle(mapOf("search_query" to "kotlin"))
|
|
189
|
+
val viewModel = SearchViewModel(savedState, fakeRepository)
|
|
190
|
+
|
|
191
|
+
assertThat(viewModel.query.value).isEqualTo("kotlin")
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Anti-Patterns
|
|
198
|
+
|
|
199
|
+
- Assuming ViewModel survives process death — it doesn't
|
|
200
|
+
- Storing non-Parcelable objects in SavedStateHandle — crashes on restore
|
|
201
|
+
- Not testing process death — the most common untested scenario
|
|
202
|
+
- Saving too much in SavedStateHandle — it has a size limit (~500KB total)
|
|
203
|
+
- Relying on `onSaveInstanceState` in Activity instead of SavedStateHandle in ViewModel
|
|
204
|
+
- Starting critical operations (payments, uploads) without WorkManager fallback
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Related Skills
|
|
209
|
+
|
|
210
|
+
- `savedstatehandle` — key-value persistence across process death
|
|
211
|
+
- `state-restoration` — full state restoration strategy
|
|
212
|
+
- `workmanager` — durable background work across process death
|
|
213
|
+
- `datastore` — persisting session state
|
|
214
|
+
- `room` — persistent data that always survives
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: resources
|
|
3
|
+
description: >
|
|
4
|
+
Android resource system — strings, colors, dimensions, drawables, styles, and qualifiers.
|
|
5
|
+
Load this skill when working with res/ directory, defining or accessing resources,
|
|
6
|
+
supporting multiple screen sizes, locales, or themes.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Resources
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
Android resources are external files and static values used by the app — strings, colors, dimensions, drawables, layouts, styles, and more. They live in `res/` and are accessed via generated `R` class. Resources support qualifiers that allow automatic selection based on device configuration.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Never hardcode strings, colors, or dimensions in code or layouts — always use resources
|
|
20
|
+
- Use qualifiers for configuration-specific resources — don't detect configuration in code
|
|
21
|
+
- All user-facing strings must be in `strings.xml` — enables localization
|
|
22
|
+
- Colors must be defined in `colors.xml` and referenced via theme attributes — not directly
|
|
23
|
+
- Dimensions must be in `dimens.xml` — enables density-independent scaling
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Directory Structure
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
res/
|
|
31
|
+
├── drawable/ ← vector drawables, shape drawables
|
|
32
|
+
├── drawable-xxhdpi/ ← raster images (prefer vectors)
|
|
33
|
+
├── layout/ ← XML layouts (if using View system)
|
|
34
|
+
├── mipmap-*/ ← launcher icons only
|
|
35
|
+
├── values/
|
|
36
|
+
│ ├── strings.xml
|
|
37
|
+
│ ├── colors.xml
|
|
38
|
+
│ ├── dimens.xml
|
|
39
|
+
│ ├── styles.xml
|
|
40
|
+
│ └── themes.xml
|
|
41
|
+
├── values-night/ ← dark theme overrides
|
|
42
|
+
├── values-fa/ ← Persian strings
|
|
43
|
+
├── values-land/ ← landscape overrides
|
|
44
|
+
└── xml/ ← network_security_config, file_paths, etc.
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Strings
|
|
50
|
+
|
|
51
|
+
```xml
|
|
52
|
+
<!-- ✅ strings.xml -->
|
|
53
|
+
<resources>
|
|
54
|
+
<!-- Simple string -->
|
|
55
|
+
<string name="app_name">MyApp</string>
|
|
56
|
+
<string name="action_save">Save</string>
|
|
57
|
+
|
|
58
|
+
<!-- ✅ Formatted string with argument -->
|
|
59
|
+
<string name="welcome_message">Welcome, %1$s!</string>
|
|
60
|
+
|
|
61
|
+
<!-- ✅ Plural strings -->
|
|
62
|
+
<plurals name="item_count">
|
|
63
|
+
<item quantity="one">%d item</item>
|
|
64
|
+
<item quantity="other">%d items</item>
|
|
65
|
+
</plurals>
|
|
66
|
+
|
|
67
|
+
<!-- ✅ String array -->
|
|
68
|
+
<string-array name="days_of_week">
|
|
69
|
+
<item>Monday</item>
|
|
70
|
+
<item>Tuesday</item>
|
|
71
|
+
</string-array>
|
|
72
|
+
</resources>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```kotlin
|
|
76
|
+
// ✅ Accessing strings in code
|
|
77
|
+
val welcome = getString(R.string.welcome_message, userName)
|
|
78
|
+
val itemCount = resources.getQuantityString(R.plurals.item_count, count, count)
|
|
79
|
+
|
|
80
|
+
// ✅ In Compose
|
|
81
|
+
Text(text = stringResource(R.string.welcome_message, userName))
|
|
82
|
+
Text(text = pluralStringResource(R.plurals.item_count, count, count))
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Colors
|
|
88
|
+
|
|
89
|
+
```xml
|
|
90
|
+
<!-- ✅ colors.xml — raw color palette -->
|
|
91
|
+
<resources>
|
|
92
|
+
<color name="purple_500">#FF6200EE</color>
|
|
93
|
+
<color name="purple_700">#FF3700B3</color>
|
|
94
|
+
<color name="teal_200">#FF03DAC5</color>
|
|
95
|
+
<color name="white">#FFFFFFFF</color>
|
|
96
|
+
<color name="black">#FF000000</color>
|
|
97
|
+
</resources>
|
|
98
|
+
|
|
99
|
+
<!-- ✅ themes.xml — semantic color attributes referencing palette -->
|
|
100
|
+
<style name="Theme.App" parent="Theme.Material3.DayNight">
|
|
101
|
+
<item name="colorPrimary">@color/purple_500</item>
|
|
102
|
+
<item name="colorOnPrimary">@color/white</item>
|
|
103
|
+
<item name="colorSurface">@color/white</item>
|
|
104
|
+
</style>
|
|
105
|
+
|
|
106
|
+
<!-- ✅ values-night/themes.xml — dark theme overrides -->
|
|
107
|
+
<style name="Theme.App" parent="Theme.Material3.DayNight">
|
|
108
|
+
<item name="colorPrimary">@color/purple_200</item>
|
|
109
|
+
<item name="colorSurface">@color/black</item>
|
|
110
|
+
</style>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```kotlin
|
|
114
|
+
// ✅ Always access color via theme attribute — not directly
|
|
115
|
+
val color = MaterialColors.getColor(view, com.google.android.material.R.attr.colorPrimary)
|
|
116
|
+
|
|
117
|
+
// ✅ In Compose — use MaterialTheme tokens
|
|
118
|
+
Surface(color = MaterialTheme.colorScheme.surface) { ... }
|
|
119
|
+
|
|
120
|
+
// ❌ Never hardcode or access raw color directly in code
|
|
121
|
+
view.setBackgroundColor(Color.parseColor("#FF6200EE"))
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Dimensions
|
|
127
|
+
|
|
128
|
+
```xml
|
|
129
|
+
<!-- ✅ dimens.xml -->
|
|
130
|
+
<resources>
|
|
131
|
+
<dimen name="spacing_small">8dp</dimen>
|
|
132
|
+
<dimen name="spacing_medium">16dp</dimen>
|
|
133
|
+
<dimen name="spacing_large">24dp</dimen>
|
|
134
|
+
<dimen name="corner_radius">12dp</dimen>
|
|
135
|
+
<dimen name="text_size_body">14sp</dimen>
|
|
136
|
+
<dimen name="text_size_title">20sp</dimen>
|
|
137
|
+
</resources>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```kotlin
|
|
141
|
+
// ✅ Accessing dimensions
|
|
142
|
+
val spacing = resources.getDimensionPixelSize(R.dimen.spacing_medium)
|
|
143
|
+
|
|
144
|
+
// ✅ In Compose — use dp/sp directly or define as constants
|
|
145
|
+
val SpacingMedium = 16.dp
|
|
146
|
+
val TextSizeBody = 14.sp
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Drawables
|
|
152
|
+
|
|
153
|
+
```xml
|
|
154
|
+
<!-- ✅ Use vector drawables — scale perfectly on all densities -->
|
|
155
|
+
<!-- res/drawable/ic_arrow_back.xml -->
|
|
156
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
157
|
+
android:width="24dp"
|
|
158
|
+
android:height="24dp"
|
|
159
|
+
android:viewportWidth="24"
|
|
160
|
+
android:viewportHeight="24"
|
|
161
|
+
android:tint="?attr/colorOnSurface">
|
|
162
|
+
<path android:fillColor="@android:color/white"
|
|
163
|
+
android:pathData="M20,11H7.83l5.59-5.59L12,4l-8,8 8,8 1.41-1.41L7.83,13H20v-2z"/>
|
|
164
|
+
</vector>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
// ✅ Raster images — use only when vector isn't suitable
|
|
169
|
+
// Place in density buckets:
|
|
170
|
+
drawable-mdpi/ (1x)
|
|
171
|
+
drawable-hdpi/ (1.5x)
|
|
172
|
+
drawable-xhdpi/ (2x)
|
|
173
|
+
drawable-xxhdpi/ (3x)
|
|
174
|
+
drawable-xxxhdpi/ (4x)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Configuration Qualifiers
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
values/ ← default
|
|
183
|
+
values-fa/ ← Persian locale
|
|
184
|
+
values-night/ ← dark mode
|
|
185
|
+
values-land/ ← landscape
|
|
186
|
+
values-sw600dp/ ← tablets (600dp+ smallest width)
|
|
187
|
+
values-v33/ ← API 33+
|
|
188
|
+
|
|
189
|
+
layout/ ← default layout
|
|
190
|
+
layout-land/ ← landscape layout
|
|
191
|
+
layout-sw600dp/ ← tablet layout
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Accessing Resources in Code
|
|
197
|
+
|
|
198
|
+
```kotlin
|
|
199
|
+
// ✅ From Activity/Fragment
|
|
200
|
+
val text = getString(R.string.app_name)
|
|
201
|
+
val color = ContextCompat.getColor(this, R.color.purple_500)
|
|
202
|
+
val drawable = ContextCompat.getDrawable(this, R.drawable.ic_arrow_back)
|
|
203
|
+
val dimen = resources.getDimensionPixelSize(R.dimen.spacing_medium)
|
|
204
|
+
|
|
205
|
+
// ✅ From ViewModel (via injected Context — use ApplicationContext only)
|
|
206
|
+
class MyViewModel(private val appContext: Context) : ViewModel() {
|
|
207
|
+
fun getLabel(): String = appContext.getString(R.string.app_name)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ✅ Compose
|
|
211
|
+
Text(stringResource(R.string.app_name))
|
|
212
|
+
Icon(painterResource(R.drawable.ic_arrow_back), contentDescription = null)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Anti-Patterns
|
|
218
|
+
|
|
219
|
+
- Hardcoded strings in XML or code — breaks localization
|
|
220
|
+
- Accessing colors directly (`R.color.x`) instead of via theme attributes — breaks dark mode
|
|
221
|
+
- Using `px` instead of `dp`/`sp` in dimensions — breaks across densities
|
|
222
|
+
- Placing launcher icons in `drawable/` instead of `mipmap/` — incorrect scaling
|
|
223
|
+
- Using raster images where vectors would work — increases APK size
|
|
224
|
+
- Storing sensitive data (keys, tokens) as string resources — visible in decompiled APK
|
|
225
|
+
- Duplicate string keys — silent override, hard to debug
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Related Skills
|
|
230
|
+
|
|
231
|
+
- `manifest` — referencing resources in manifest
|
|
232
|
+
- `material3` — Material 3 theme and color system
|
|
233
|
+
- `rtl` — RTL-specific resource qualifiers
|
|
234
|
+
- `design-system` — design tokens as Android resources
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: savedstatehandle
|
|
3
|
+
description: >
|
|
4
|
+
SavedStateHandle usage in ViewModel for surviving process death and
|
|
5
|
+
configuration changes. Load this skill when persisting ViewModel state
|
|
6
|
+
across process kill, reading navigation arguments, or deciding what
|
|
7
|
+
state needs to be saved vs what can be reloaded.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# SavedStateHandle
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
SavedStateHandle is a key-value store provided to ViewModel that survives both configuration changes and process death. Unlike ViewModel which only survives rotation, SavedStateHandle persists through system-initiated process kills and is restored when the user returns to the app.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Core Principles
|
|
19
|
+
|
|
20
|
+
- Use SavedStateHandle for state that must survive **process death** — not just rotation
|
|
21
|
+
- Don't save everything — only save what can't be easily reloaded (user input, scroll position, selected IDs)
|
|
22
|
+
- Data stored in SavedStateHandle must be **Parcelable or primitive**
|
|
23
|
+
- Prefer `saveable { }` in Compose for local UI state — use SavedStateHandle for ViewModel state
|
|
24
|
+
- Navigation arguments are automatically available via SavedStateHandle
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## What to Save vs What to Reload
|
|
29
|
+
|
|
30
|
+
| State Type | Strategy |
|
|
31
|
+
| ------------------------------------ | -------------------------------------- |
|
|
32
|
+
| User input (text fields, selections) | ✅ SavedStateHandle |
|
|
33
|
+
| Scroll position | ✅ SavedStateHandle or rememberSaveable |
|
|
34
|
+
| Selected item ID | ✅ SavedStateHandle |
|
|
35
|
+
| Loaded list from network/DB | ❌ Reload from repository |
|
|
36
|
+
| Loading/error state | ❌ Derive from data loading |
|
|
37
|
+
| Navigation arguments | ✅ Read from SavedStateHandle |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Basic Usage
|
|
42
|
+
|
|
43
|
+
```kotlin
|
|
44
|
+
// ✅ Inject via Hilt — automatic with @HiltViewModel
|
|
45
|
+
@HiltViewModel
|
|
46
|
+
class UserDetailViewModel @Inject constructor(
|
|
47
|
+
savedStateHandle: SavedStateHandle,
|
|
48
|
+
private val repository: UserRepository
|
|
49
|
+
) : ViewModel() {
|
|
50
|
+
|
|
51
|
+
// ✅ Read navigation argument (set by NavController)
|
|
52
|
+
private val userId: String = checkNotNull(savedStateHandle["userId"])
|
|
53
|
+
|
|
54
|
+
// ✅ StateFlow backed by SavedStateHandle — survives process death
|
|
55
|
+
val searchQuery: StateFlow<String> = savedStateHandle
|
|
56
|
+
.getStateFlow("search_query", initialValue = "")
|
|
57
|
+
|
|
58
|
+
fun onSearchQueryChanged(query: String) {
|
|
59
|
+
savedStateHandle["search_query"] = query
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Reading Navigation Arguments
|
|
67
|
+
|
|
68
|
+
```kotlin
|
|
69
|
+
// ✅ Compose Navigation — argument passed via route
|
|
70
|
+
// NavHost setup
|
|
71
|
+
composable("user/{userId}") { backStackEntry ->
|
|
72
|
+
val viewModel: UserDetailViewModel = hiltViewModel()
|
|
73
|
+
UserDetailScreen(viewModel)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ✅ ViewModel reads it from SavedStateHandle
|
|
77
|
+
@HiltViewModel
|
|
78
|
+
class UserDetailViewModel @Inject constructor(
|
|
79
|
+
savedStateHandle: SavedStateHandle
|
|
80
|
+
) : ViewModel() {
|
|
81
|
+
val userId: String = checkNotNull(savedStateHandle["userId"]) {
|
|
82
|
+
"userId argument is required"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ✅ Type-safe navigation (Compose Navigation 2.8+)
|
|
87
|
+
@Serializable
|
|
88
|
+
data class UserDetailRoute(val userId: String)
|
|
89
|
+
|
|
90
|
+
@HiltViewModel
|
|
91
|
+
class UserDetailViewModel @Inject constructor(
|
|
92
|
+
savedStateHandle: SavedStateHandle
|
|
93
|
+
) : ViewModel() {
|
|
94
|
+
private val route = savedStateHandle.toRoute<UserDetailRoute>()
|
|
95
|
+
val userId = route.userId
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## StateFlow from SavedStateHandle
|
|
102
|
+
|
|
103
|
+
```kotlin
|
|
104
|
+
@HiltViewModel
|
|
105
|
+
class SearchViewModel @Inject constructor(
|
|
106
|
+
private val savedStateHandle: SavedStateHandle,
|
|
107
|
+
private val repository: SearchRepository
|
|
108
|
+
) : ViewModel() {
|
|
109
|
+
|
|
110
|
+
// ✅ Backed by SavedStateHandle — survives process death
|
|
111
|
+
val query: StateFlow<String> = savedStateHandle
|
|
112
|
+
.getStateFlow(KEY_QUERY, initialValue = "")
|
|
113
|
+
|
|
114
|
+
// ✅ Derive results from the saved query
|
|
115
|
+
val results: StateFlow<List<Result>> = query
|
|
116
|
+
.debounce(300)
|
|
117
|
+
.flatMapLatest { q -> repository.search(q) }
|
|
118
|
+
.stateIn(
|
|
119
|
+
scope = viewModelScope,
|
|
120
|
+
started = SharingStarted.WhileSubscribed(5_000),
|
|
121
|
+
initialValue = emptyList()
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
fun onQueryChanged(newQuery: String) {
|
|
125
|
+
savedStateHandle[KEY_QUERY] = newQuery
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
companion object {
|
|
129
|
+
private const val KEY_QUERY = "search_query"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Saving Complex Types
|
|
137
|
+
|
|
138
|
+
```kotlin
|
|
139
|
+
// ✅ Parcelable types work directly
|
|
140
|
+
@Parcelize
|
|
141
|
+
data class FilterState(
|
|
142
|
+
val category: String,
|
|
143
|
+
val sortOrder: String,
|
|
144
|
+
val minPrice: Int
|
|
145
|
+
) : Parcelable
|
|
146
|
+
|
|
147
|
+
@HiltViewModel
|
|
148
|
+
class ProductViewModel @Inject constructor(
|
|
149
|
+
savedStateHandle: SavedStateHandle
|
|
150
|
+
) : ViewModel() {
|
|
151
|
+
|
|
152
|
+
var filterState: StateFlow<FilterState> = savedStateHandle
|
|
153
|
+
.getStateFlow("filter", FilterState("all", "asc", 0))
|
|
154
|
+
|
|
155
|
+
fun applyFilter(filter: FilterState) {
|
|
156
|
+
savedStateHandle["filter"] = filter
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ✅ Primitive types — no Parcelable needed
|
|
161
|
+
savedStateHandle["page"] = 1
|
|
162
|
+
savedStateHandle["isExpanded"] = true
|
|
163
|
+
savedStateHandle["selectedId"] = "user_123"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Combining SavedStateHandle with Repository
|
|
169
|
+
|
|
170
|
+
```kotlin
|
|
171
|
+
@HiltViewModel
|
|
172
|
+
class UserListViewModel @Inject constructor(
|
|
173
|
+
savedStateHandle: SavedStateHandle,
|
|
174
|
+
private val repository: UserRepository
|
|
175
|
+
) : ViewModel() {
|
|
176
|
+
|
|
177
|
+
// Persisted: selected filter
|
|
178
|
+
private val selectedFilter = savedStateHandle
|
|
179
|
+
.getStateFlow("filter", "all")
|
|
180
|
+
|
|
181
|
+
// Not persisted: loaded data — reload from DB
|
|
182
|
+
val users: StateFlow<List<User>> = selectedFilter
|
|
183
|
+
.flatMapLatest { filter ->
|
|
184
|
+
repository.observeUsers(filter)
|
|
185
|
+
}
|
|
186
|
+
.stateIn(
|
|
187
|
+
scope = viewModelScope,
|
|
188
|
+
started = SharingStarted.WhileSubscribed(5_000),
|
|
189
|
+
initialValue = emptyList()
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
fun onFilterSelected(filter: String) {
|
|
193
|
+
savedStateHandle["filter"] = filter
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Anti-Patterns
|
|
201
|
+
|
|
202
|
+
- Saving large objects (bitmaps, full lists) in SavedStateHandle — use DB or files instead
|
|
203
|
+
- Not using SavedStateHandle for user input — it's lost on process death
|
|
204
|
+
- Reading navigation args from `Intent` in Activity when ViewModel is available
|
|
205
|
+
- Using `onSaveInstanceState` in Activity/Fragment when SavedStateHandle covers it
|
|
206
|
+
- Saving non-Parcelable custom types — causes crash at restore time
|
|
207
|
+
- Duplicating state between SavedStateHandle and a separate StateFlow
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Related Skills
|
|
212
|
+
|
|
213
|
+
- `process-death-recovery` — full process death survival strategy
|
|
214
|
+
- `state-restoration` — broader state restoration patterns
|
|
215
|
+
- `navigation` — passing arguments via NavController
|
|
216
|
+
- `lifecycle` — when process death occurs
|
|
217
|
+
- `viewmodel` — ViewModel lifecycle and scope
|