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.
Files changed (176) hide show
  1. package/dist/index.js +143 -0
  2. package/package.json +27 -0
  3. package/skills/Android Ecosystem/Baseline Profile Generator/SKILL.md +277 -0
  4. package/skills/Android Ecosystem/Glance/SKILL.md +315 -0
  5. package/skills/Android Platform/Configuration/SKILL.md +201 -0
  6. package/skills/Android Platform/Filesystem/SKILL.md +216 -0
  7. package/skills/Android Platform/Lifecycle/SKILL.md +233 -0
  8. package/skills/Android Platform/Manifest/SKILL.md +226 -0
  9. package/skills/Android Platform/Process Death Recovery/SKILL.md +214 -0
  10. package/skills/Android Platform/Resources/SKILL.md +234 -0
  11. package/skills/Android Platform/SavedStateHandle/SKILL.md +217 -0
  12. package/skills/Android Platform/State Restoration/SKILL.md +210 -0
  13. package/skills/Architecture/Bounded Context/SKILL.md +207 -0
  14. package/skills/Architecture/Clean Architecture/SKILL.md +229 -0
  15. package/skills/Architecture/Domain Modeling/SKILL.md +236 -0
  16. package/skills/Architecture/Entity Design/SKILL.md +243 -0
  17. package/skills/Architecture/Feature Isolation/SKILL.md +216 -0
  18. package/skills/Architecture/MVI/SKILL.md +224 -0
  19. package/skills/Architecture/MVVM/SKILL.md +198 -0
  20. package/skills/Architecture/Modularization/SKILL.md +194 -0
  21. package/skills/Architecture/Offline First/SKILL.md +249 -0
  22. package/skills/Architecture/Repository Pattern/SKILL.md +216 -0
  23. package/skills/Architecture/Side Effect Management/SKILL.md +278 -0
  24. package/skills/Architecture/State Management/SKILL.md +229 -0
  25. package/skills/Architecture/Unidirectional Data Flow/SKILL.md +196 -0
  26. package/skills/Architecture/Use Case Design/SKILL.md +244 -0
  27. package/skills/Architecture/Value Object/SKILL.md +226 -0
  28. package/skills/Build Infrastructure/Build Orchestration/SKILL.md +257 -0
  29. package/skills/Build Infrastructure/Dependency Compatibility Resolver/SKILL.md +259 -0
  30. package/skills/Build Infrastructure/Environment Validator/SKILL.md +311 -0
  31. package/skills/Build System/Build Cache/SKILL.md +233 -0
  32. package/skills/Build System/Build Flavor Strategy/SKILL.md +171 -0
  33. package/skills/Build System/Build Variant/SKILL.md +215 -0
  34. package/skills/Build System/Convention Plugin/SKILL.md +288 -0
  35. package/skills/Build System/Dependency Management/SKILL.md +261 -0
  36. package/skills/Build System/Gradle/SKILL.md +284 -0
  37. package/skills/Build System/Incremental Build/SKILL.md +199 -0
  38. package/skills/Build System/KAPT/SKILL.md +198 -0
  39. package/skills/Build System/KSP/SKILL.md +263 -0
  40. package/skills/Build System/Module Dependency Graph Validation/SKILL.md +223 -0
  41. package/skills/Build System/Specialized/C++/SKILL.md +308 -0
  42. package/skills/Build System/Specialized/JNI/SKILL.md +306 -0
  43. package/skills/Build System/Specialized/NDK/SKILL.md +264 -0
  44. package/skills/Build System/Version Catalog/SKILL.md +304 -0
  45. package/skills/Concurrency/Background Processing/SKILL.md +185 -0
  46. package/skills/Concurrency/Channel/SKILL.md +207 -0
  47. package/skills/Concurrency/Coroutine/SKILL.md +200 -0
  48. package/skills/Concurrency/Flow/SKILL.md +179 -0
  49. package/skills/Concurrency/Mutex Strategy/SKILL.md +185 -0
  50. package/skills/Concurrency/SharedFlow/SKILL.md +171 -0
  51. package/skills/Concurrency/StateFlow/SKILL.md +175 -0
  52. package/skills/Concurrency/Structured Concurrency/SKILL.md +197 -0
  53. package/skills/Concurrency/Synchronization Policy/SKILL.md +192 -0
  54. package/skills/Core Language/Annotation Processing/SKILL.md +224 -0
  55. package/skills/Core Language/DSL/SKILL.md +186 -0
  56. package/skills/Core Language/Extension Functions Design/SKILL.md +191 -0
  57. package/skills/Core Language/Immutability/SKILL.md +156 -0
  58. package/skills/Core Language/KMP/SKILL.md +182 -0
  59. package/skills/Core Language/Kotlin/SKILL.md +187 -0
  60. package/skills/Core Language/Reactive State Management/SKILL.md +228 -0
  61. package/skills/Core Language/Reactive Streams/SKILL.md +235 -0
  62. package/skills/Core Language/Serialization/SKILL.md +191 -0
  63. package/skills/Data Layer/Cache Strategy/SKILL.md +261 -0
  64. package/skills/Data Layer/Conflict Resolution/SKILL.md +248 -0
  65. package/skills/Data Layer/DAO/SKILL.md +225 -0
  66. package/skills/Data Layer/DTO Mapping/SKILL.md +269 -0
  67. package/skills/Data Layer/DataStore/SKILL.md +264 -0
  68. package/skills/Data Layer/Database Versioning Strategy/SKILL.md +215 -0
  69. package/skills/Data Layer/Encrypted Database/SKILL.md +212 -0
  70. package/skills/Data Layer/File Storage/SKILL.md +247 -0
  71. package/skills/Data Layer/Indexing/SKILL.md +184 -0
  72. package/skills/Data Layer/Key-Value Store Strategy/SKILL.md +185 -0
  73. package/skills/Data Layer/Merge Strategy/SKILL.md +240 -0
  74. package/skills/Data Layer/Migration/SKILL.md +243 -0
  75. package/skills/Data Layer/Paging/SKILL.md +264 -0
  76. package/skills/Data Layer/Proto DataStore/SKILL.md +250 -0
  77. package/skills/Data Layer/Room/SKILL.md +244 -0
  78. package/skills/Data Layer/SQLite/SKILL.md +255 -0
  79. package/skills/Data Layer/Sync Engine/SKILL.md +268 -0
  80. package/skills/Dependency Injection/Dagger/SKILL.md +283 -0
  81. package/skills/Dependency Injection/Hilt/SKILL.md +345 -0
  82. package/skills/Dependency Injection/Koin/SKILL.md +282 -0
  83. package/skills/Developer Experience/Detekt/SKILL.md +272 -0
  84. package/skills/Developer Experience/Lint Rule/SKILL.md +281 -0
  85. package/skills/Google Ecosystem/Analytics/SKILL.md +281 -0
  86. package/skills/Google Ecosystem/Crashlytics/SKILL.md +234 -0
  87. package/skills/Google Ecosystem/Firebase/SKILL.md +200 -0
  88. package/skills/Google Ecosystem/Firebase Messaging/SKILL.md +266 -0
  89. package/skills/Media/Audio/SKILL.md +257 -0
  90. package/skills/Media/Camera/SKILL.md +229 -0
  91. package/skills/Media/CameraX/SKILL.md +295 -0
  92. package/skills/Media/ExoPlayer/SKILL.md +258 -0
  93. package/skills/Media/Video/SKILL.md +228 -0
  94. package/skills/Meta Skills/Domain Error Model/SKILL.md +238 -0
  95. package/skills/Meta Skills/Error Handling/SKILL.md +255 -0
  96. package/skills/Meta Skills/Error Mapping/SKILL.md +232 -0
  97. package/skills/Meta Skills/Failure Strategy/SKILL.md +294 -0
  98. package/skills/Meta Skills/Migration Strategy/SKILL.md +305 -0
  99. package/skills/Meta Skills/User Friendly Errors/SKILL.md +334 -0
  100. package/skills/Navigation/Deep Navigation/SKILL.md +209 -0
  101. package/skills/Navigation/Navigation/SKILL.md +215 -0
  102. package/skills/Navigation/Nested Navigation/SKILL.md +214 -0
  103. package/skills/Networking/API Contract/SKILL.md +220 -0
  104. package/skills/Networking/Authentication/SKILL.md +210 -0
  105. package/skills/Networking/Certificate Pinning/SKILL.md +167 -0
  106. package/skills/Networking/Fallback Strategy/SKILL.md +182 -0
  107. package/skills/Networking/Ktor/SKILL.md +219 -0
  108. package/skills/Networking/Multipart Upload/SKILL.md +213 -0
  109. package/skills/Networking/OkHttp/SKILL.md +193 -0
  110. package/skills/Networking/REST/SKILL.md +178 -0
  111. package/skills/Networking/Rate Limiting/SKILL.md +170 -0
  112. package/skills/Networking/Retrofit/SKILL.md +241 -0
  113. package/skills/Networking/Retry-Backoff/SKILL.md +181 -0
  114. package/skills/Networking/Server-Sent Events (SSE)/SKILL.md +196 -0
  115. package/skills/Networking/WebSocket/SKILL.md +224 -0
  116. package/skills/Observability/Crash Reporting/SKILL.md +219 -0
  117. package/skills/Observability/Logging/SKILL.md +168 -0
  118. package/skills/Observability/Metrics/SKILL.md +227 -0
  119. package/skills/Observability/Structured Logging/SKILL.md +234 -0
  120. package/skills/Performance/ANR Prevention/SKILL.md +192 -0
  121. package/skills/Performance/Allocation Optimization/SKILL.md +179 -0
  122. package/skills/Performance/App Startup/SKILL.md +183 -0
  123. package/skills/Performance/Baseline Profile/SKILL.md +205 -0
  124. package/skills/Performance/Battery Optimization/SKILL.md +192 -0
  125. package/skills/Performance/Benchmark/SKILL.md +182 -0
  126. package/skills/Performance/Bitmap Optimization/SKILL.md +178 -0
  127. package/skills/Performance/Compose Optimization/SKILL.md +187 -0
  128. package/skills/Performance/Heap Management/SKILL.md +184 -0
  129. package/skills/Performance/Macrobenchmark/SKILL.md +214 -0
  130. package/skills/Performance/Memory Leak Prevention/SKILL.md +218 -0
  131. package/skills/Performance/Rendering Performance/SKILL.md +205 -0
  132. package/skills/Performance/Startup Optimization/SKILL.md +219 -0
  133. package/skills/Security/Biometric/SKILL.md +224 -0
  134. package/skills/Security/Certificate Transparency/SKILL.md +158 -0
  135. package/skills/Security/Cryptography/SKILL.md +244 -0
  136. package/skills/Security/Encrypted Storage/SKILL.md +273 -0
  137. package/skills/Security/Frida Detection/SKILL.md +230 -0
  138. package/skills/Security/Hook Detection/SKILL.md +197 -0
  139. package/skills/Security/Keystore/SKILL.md +272 -0
  140. package/skills/Security/Network Security Config/SKILL.md +186 -0
  141. package/skills/Security/Obfuscation/SKILL.md +226 -0
  142. package/skills/Security/Proguard/SKILL.md +202 -0
  143. package/skills/Security/R8/SKILL.md +234 -0
  144. package/skills/Security/Reverse Engineering Resistance/SKILL.md +267 -0
  145. package/skills/Security/Root Detection/SKILL.md +220 -0
  146. package/skills/Security/Secure Networking/SKILL.md +220 -0
  147. package/skills/System Integration/AlarmManager/SKILL.md +182 -0
  148. package/skills/System Integration/App Widget/SKILL.md +182 -0
  149. package/skills/System Integration/Deep Link/SKILL.md +187 -0
  150. package/skills/System Integration/Foreground Service/SKILL.md +212 -0
  151. package/skills/System Integration/Notification/SKILL.md +237 -0
  152. package/skills/System Integration/WorkManager/SKILL.md +256 -0
  153. package/skills/System Integration/clipboard/SKILL.md +155 -0
  154. package/skills/System Integration/share-intent/SKILL.md +182 -0
  155. package/skills/Testing/Compose Testing/SKILL.md +296 -0
  156. package/skills/Testing/Espresso/SKILL.md +292 -0
  157. package/skills/Testing/Fake Data/SKILL.md +245 -0
  158. package/skills/Testing/Integration Testing/SKILL.md +288 -0
  159. package/skills/Testing/Mocking/SKILL.md +229 -0
  160. package/skills/Testing/Snapshot Testing/SKILL.md +259 -0
  161. package/skills/Testing/UI Testing/SKILL.md +293 -0
  162. package/skills/Testing/Unit Testing/SKILL.md +309 -0
  163. package/skills/UI System/Bottom Sheet Patterns/SKILL.md +279 -0
  164. package/skills/UI System/Compose/SKILL.md +296 -0
  165. package/skills/UI System/Compose Animation/SKILL.md +281 -0
  166. package/skills/UI System/Compose Multiplatform/SKILL.md +261 -0
  167. package/skills/UI System/Compose Navigation/SKILL.md +255 -0
  168. package/skills/UI System/Compose Performance/SKILL.md +274 -0
  169. package/skills/UI System/Design System/SKILL.md +217 -0
  170. package/skills/UI System/Empty State Strategy/SKILL.md +208 -0
  171. package/skills/UI System/Keyboard Navigation/SKILL.md +214 -0
  172. package/skills/UI System/Loading Strategy/SKILL.md +254 -0
  173. package/skills/UI System/Material 3/SKILL.md +279 -0
  174. package/skills/UI System/RTL/SKILL.md +179 -0
  175. package/src/index.ts +182 -0
  176. package/tsconfig.json +19 -0
@@ -0,0 +1,305 @@
1
+ ---
2
+ name: migration-strategy
3
+ description: >
4
+ Migration strategy for Android apps — database, API, and data format migrations.
5
+ Load this skill when planning Room database schema migrations, handling
6
+ API version changes, migrating SharedPreferences to DataStore,
7
+ or designing backward-compatible data format changes.
8
+ ---
9
+
10
+ # Migration Strategy
11
+
12
+ ## Overview
13
+ Migration strategy covers how the app evolves its data contracts without breaking existing users. This includes Room schema migrations (adding columns, renaming tables), API version transitions (v1 → v2 endpoints), and storage format migrations (SharedPreferences → DataStore, plain → encrypted). Each type requires a different approach.
14
+
15
+ ---
16
+
17
+ ## Core Principles
18
+
19
+ - **Never break existing users** — migrations must be backward-compatible or provide a clear upgrade path
20
+ - **Migrations are one-way** — don't design rollback unless specifically required
21
+ - **Test migrations** — always test the upgrade path from the previous version
22
+ - **Data loss is unacceptable** — destructive migration (`fallbackToDestructiveMigration`) is only for dev/cache data
23
+ - **Incremental** — migrate one version at a time; never skip versions
24
+
25
+ ---
26
+
27
+ ## Room Database Migration
28
+
29
+ ```kotlin
30
+ // ✅ Version 1 → Version 2: Add column
31
+ val MIGRATION_1_2 = object : Migration(1, 2) {
32
+ override fun migrate(database: SupportSQLiteDatabase) {
33
+ database.execSQL(
34
+ "ALTER TABLE users ADD COLUMN is_verified INTEGER NOT NULL DEFAULT 0"
35
+ )
36
+ }
37
+ }
38
+
39
+ // ✅ Version 2 → Version 3: Add new table
40
+ val MIGRATION_2_3 = object : Migration(2, 3) {
41
+ override fun migrate(database: SupportSQLiteDatabase) {
42
+ database.execSQL("""
43
+ CREATE TABLE IF NOT EXISTS `user_sessions` (
44
+ `id` TEXT NOT NULL,
45
+ `user_id` TEXT NOT NULL,
46
+ `token` TEXT NOT NULL,
47
+ `expires_at` INTEGER NOT NULL,
48
+ PRIMARY KEY(`id`),
49
+ FOREIGN KEY(`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
50
+ )
51
+ """.trimIndent())
52
+ database.execSQL("CREATE INDEX IF NOT EXISTS `index_user_sessions_user_id` ON `user_sessions` (`user_id`)")
53
+ }
54
+ }
55
+
56
+ // ✅ Version 3 → Version 4: Rename column (SQLite doesn't support RENAME COLUMN before API 29)
57
+ val MIGRATION_3_4 = object : Migration(3, 4) {
58
+ override fun migrate(database: SupportSQLiteDatabase) {
59
+ // SQLite < 3.25 workaround: create new table, copy, drop old
60
+ database.execSQL("""
61
+ CREATE TABLE `users_new` (
62
+ `id` TEXT NOT NULL,
63
+ `full_name` TEXT NOT NULL,
64
+ `email` TEXT NOT NULL,
65
+ `role` TEXT NOT NULL,
66
+ `is_active` INTEGER NOT NULL,
67
+ `is_verified` INTEGER NOT NULL DEFAULT 0,
68
+ `created_at` INTEGER NOT NULL,
69
+ `updated_at` INTEGER NOT NULL,
70
+ PRIMARY KEY(`id`)
71
+ )
72
+ """.trimIndent())
73
+ database.execSQL("""
74
+ INSERT INTO `users_new`
75
+ SELECT `id`, `name`, `email`, `role`, `is_active`, `is_verified`, `created_at`, `updated_at`
76
+ FROM `users`
77
+ """.trimIndent())
78
+ database.execSQL("DROP TABLE `users`")
79
+ database.execSQL("ALTER TABLE `users_new` RENAME TO `users`")
80
+ }
81
+ }
82
+
83
+ // ✅ Register all migrations
84
+ @Database(entities = [...], version = 4)
85
+ abstract class AppDatabase : RoomDatabase() {
86
+ companion object {
87
+ fun build(context: Context): AppDatabase =
88
+ Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
89
+ .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
90
+ .build()
91
+ }
92
+ }
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Auto Migration (Room 2.4+)
98
+
99
+ ```kotlin
100
+ // ✅ Simple migrations can use @AutoMigration
101
+ @Database(
102
+ entities = [UserEntity::class],
103
+ version = 5,
104
+ autoMigrations = [
105
+ AutoMigration(from = 4, to = 5) // adds new columns with defaults automatically
106
+ ]
107
+ )
108
+ abstract class AppDatabase : RoomDatabase()
109
+
110
+ // ✅ AutoMigration with spec for rename/delete
111
+ @RenameColumn(tableName = "users", fromColumnName = "name", toColumnName = "full_name")
112
+ class Migration4To5Spec : AutoMigrationSpec
113
+
114
+ @Database(
115
+ version = 5,
116
+ autoMigrations = [
117
+ AutoMigration(from = 4, to = 5, spec = Migration4To5Spec::class)
118
+ ]
119
+ )
120
+ abstract class AppDatabase : RoomDatabase()
121
+ ```
122
+
123
+ ---
124
+
125
+ ## Testing Migrations
126
+
127
+ ```kotlin
128
+ // ✅ Always test migration path
129
+ @RunWith(AndroidJUnit4::class)
130
+ class MigrationTest {
131
+
132
+ @get:Rule
133
+ val helper = MigrationTestHelper(
134
+ InstrumentationRegistry.getInstrumentation(),
135
+ AppDatabase::class.java
136
+ )
137
+
138
+ @Test
139
+ fun migrate1To2() {
140
+ // Create v1 database with test data
141
+ helper.createDatabase("test.db", 1).apply {
142
+ execSQL("INSERT INTO users VALUES ('1', 'Ali', 'ali@test.com', 'member', 1, 1000, 1000)")
143
+ close()
144
+ }
145
+
146
+ // Run migration
147
+ val db = helper.runMigrationsAndValidate("test.db", 2, true, MIGRATION_1_2)
148
+
149
+ // Verify data intact + new column has default
150
+ val cursor = db.query("SELECT * FROM users WHERE id = '1'")
151
+ cursor.moveToFirst()
152
+ assertThat(cursor.getInt(cursor.getColumnIndex("is_verified"))).isEqualTo(0)
153
+ cursor.close()
154
+ }
155
+ }
156
+ ```
157
+
158
+ ---
159
+
160
+ ## SharedPreferences → DataStore Migration
161
+
162
+ ```kotlin
163
+ // ✅ One-time migration from SharedPreferences to DataStore
164
+ class PreferencesMigration @Inject constructor(
165
+ @ApplicationContext private val context: Context
166
+ ) {
167
+ private val legacyPrefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
168
+ private val migrationKey = "prefs_migrated_v1"
169
+
170
+ suspend fun migrateIfNeeded(dataStore: DataStore<Preferences>) {
171
+ if (legacyPrefs.getBoolean(migrationKey, false)) return
172
+
173
+ dataStore.edit { prefs ->
174
+ // Copy all keys from SharedPreferences to DataStore
175
+ legacyPrefs.getString("auth_token", null)?.let {
176
+ prefs[stringPreferencesKey("auth_token")] = it
177
+ }
178
+ legacyPrefs.getBoolean("notifications_enabled", true).let {
179
+ prefs[booleanPreferencesKey("notifications_enabled")] = it
180
+ }
181
+ legacyPrefs.getString("selected_language", "en")?.let {
182
+ prefs[stringPreferencesKey("selected_language")] = it
183
+ }
184
+ }
185
+
186
+ // Mark migration complete
187
+ legacyPrefs.edit { putBoolean(migrationKey, true) }
188
+
189
+ // Optionally clear old prefs
190
+ legacyPrefs.edit { clear() }
191
+ }
192
+ }
193
+
194
+ // ✅ Run at app startup — before DataStore is used
195
+ class App : Application() {
196
+ @Inject lateinit var preferencesMigration: PreferencesMigration
197
+ @Inject lateinit var dataStore: DataStore<Preferences>
198
+
199
+ override fun onCreate() {
200
+ super.onCreate()
201
+ // Run migration in background
202
+ applicationScope.launch {
203
+ preferencesMigration.migrateIfNeeded(dataStore)
204
+ }
205
+ }
206
+ }
207
+ ```
208
+
209
+ ---
210
+
211
+ ## API Version Migration
212
+
213
+ ```kotlin
214
+ // ✅ Support multiple API versions via versioned endpoints
215
+ interface UserApiService {
216
+ @GET("v2/users/{id}")
217
+ suspend fun getUser(@Path("id") id: String): UserDtoV2
218
+
219
+ // Keep v1 for gradual migration
220
+ @GET("v1/users/{id}")
221
+ suspend fun getUserLegacy(@Path("id") id: String): UserDtoV1
222
+ }
223
+
224
+ // ✅ Repository handles version negotiation
225
+ class UserRepositoryImpl @Inject constructor(
226
+ private val api: UserApiService,
227
+ private val featureFlags: FeatureFlags
228
+ ) : UserRepository {
229
+
230
+ override suspend fun getUser(id: String): Result<User> = runCatching {
231
+ if (featureFlags.isApiV2Enabled) {
232
+ api.getUser(id).toDomain()
233
+ } else {
234
+ api.getUserLegacy(id).toDomain()
235
+ }
236
+ }
237
+ }
238
+ ```
239
+
240
+ ---
241
+
242
+ ## Data Format Migration
243
+
244
+ ```kotlin
245
+ // ✅ Version-tagged serialized data
246
+ @Serializable
247
+ data class StoredData(
248
+ val version: Int = CURRENT_VERSION,
249
+ val payload: JsonElement
250
+ ) {
251
+ companion object {
252
+ const val CURRENT_VERSION = 2
253
+
254
+ fun migrate(stored: StoredData): StoredData = when (stored.version) {
255
+ 1 -> migrateV1ToV2(stored)
256
+ CURRENT_VERSION -> stored
257
+ else -> throw IllegalStateException("Unknown version: ${stored.version}")
258
+ }
259
+
260
+ private fun migrateV1ToV2(stored: StoredData): StoredData {
261
+ val v1 = Json.decodeFromJsonElement<DataV1>(stored.payload)
262
+ val v2 = DataV2(
263
+ id = v1.id,
264
+ fullName = "${v1.firstName} ${v1.lastName}", // merge fields
265
+ email = v1.email
266
+ )
267
+ return StoredData(version = 2, payload = Json.encodeToJsonElement(v2))
268
+ }
269
+ }
270
+ }
271
+ ```
272
+
273
+ ---
274
+
275
+ ## Migration Checklist
276
+
277
+ ```
278
+ Before releasing a migration:
279
+ ✅ New @Entity version incremented in @Database
280
+ ✅ Migration object written and tested
281
+ ✅ Migration registered in Room.databaseBuilder
282
+ ✅ MigrationTest written and passing
283
+ ✅ Tested on device upgrading from previous version
284
+ ✅ No data loss for existing users
285
+ ✅ fallbackToDestructiveMigration NOT used (unless cache-only DB)
286
+ ```
287
+
288
+ ---
289
+
290
+ ## Anti-Patterns
291
+
292
+ - `fallbackToDestructiveMigration()` for user data — wipes all data on missing migration
293
+ - Skipping migration versions — always chain migrations 1→2→3, never jump 1→3
294
+ - Not testing migration path — migration bugs only appear on upgrade, not fresh install
295
+ - Modifying old migrations — never change a released migration; add a new one
296
+ - Storing migration state in the migrated DB — if migration fails, the state check fails too
297
+
298
+ ---
299
+
300
+ ## Related Skills
301
+ - `room` — Room database setup
302
+ - `dao` — DAO patterns affected by migrations
303
+ - `entity-design` — entity changes that trigger migrations
304
+ - `datastore` — DataStore as migration target from SharedPreferences
305
+ - `gradle` — build versioning that tracks DB version
@@ -0,0 +1,334 @@
1
+ ---
2
+ name: user-friendly-errors
3
+ description: >
4
+ Displaying user-friendly error messages in Android Compose UI.
5
+ Load this skill when designing error states in composables,
6
+ writing error string resources, building reusable error components,
7
+ or deciding between inline errors, snackbars, and full error screens.
8
+ ---
9
+
10
+ # User Friendly Errors
11
+
12
+ ## Overview
13
+ User-friendly errors translate technical failures into clear, actionable messages. The goal is to tell the user what happened, why it matters, and what they can do. Error display varies by context: inline field errors, snackbars for transient failures, and full error screens for blocking failures.
14
+
15
+ ---
16
+
17
+ ## Core Principles
18
+
19
+ - **Clear language** — no technical jargon; no stack traces shown to users
20
+ - **Actionable** — tell the user what to do, not just what went wrong
21
+ - **Contextual** — field-level errors inline; screen-level errors as states; transient as snackbars
22
+ - **Consistent** — same error type always looks the same across screens
23
+ - **Honest** — don't overpromise; "Try again" only when retry might work
24
+
25
+ ---
26
+
27
+ ## Error Display Decision
28
+
29
+ ```
30
+ Error type → Display method
31
+ ─────────────────────────────────────────────────────
32
+ Form validation → Inline below field
33
+ Transient (network) → Snackbar with action
34
+ Blocking (load fail) → Full error state in screen
35
+ Permission denied → Inline message, no retry
36
+ Session expired → Navigate to login
37
+ Critical (corrupt) → Dialog, then navigate away
38
+ ```
39
+
40
+ ---
41
+
42
+ ## Error String Resources
43
+
44
+ ```xml
45
+ <!-- res/values/strings_errors.xml -->
46
+ <resources>
47
+ <!-- Network -->
48
+ <string name="error_no_connection">No internet connection. Please check your network.</string>
49
+ <string name="error_timeout">Request timed out. Please try again.</string>
50
+ <string name="error_server">Server error. Please try again later.</string>
51
+ <string name="error_session_expired">Your session has expired. Please log in again.</string>
52
+ <string name="error_no_permission">You don\'t have permission to do this.</string>
53
+ <string name="error_not_found">The item you\'re looking for doesn\'t exist.</string>
54
+ <string name="error_rate_limited">Too many requests. Please wait a moment and try again.</string>
55
+
56
+ <!-- Data -->
57
+ <string name="error_item_not_found">Item not found.</string>
58
+ <string name="error_conflict">"%1$s" already exists.</string>
59
+ <string name="error_invalid_state">This action isn\'t available right now.</string>
60
+
61
+ <!-- Storage -->
62
+ <string name="error_disk_full">Your device storage is full. Free up space and try again.</string>
63
+ <string name="error_data_corrupted">Some data was corrupted. Please reinstall the app.</string>
64
+
65
+ <!-- Generic -->
66
+ <string name="error_unexpected">Something went wrong. Please try again.</string>
67
+ <string name="error_retry">Try Again</string>
68
+ <string name="error_dismiss">Dismiss</string>
69
+ </resources>
70
+ ```
71
+
72
+ ---
73
+
74
+ ## Reusable Error Components
75
+
76
+ ```kotlin
77
+ // ✅ Full-screen error state
78
+ @Composable
79
+ fun FullScreenError(
80
+ message: String,
81
+ onRetry: (() -> Unit)? = null,
82
+ modifier: Modifier = Modifier
83
+ ) {
84
+ Column(
85
+ modifier = modifier
86
+ .fillMaxSize()
87
+ .padding(horizontal = 32.dp),
88
+ horizontalAlignment = Alignment.CenterHorizontally,
89
+ verticalArrangement = Arrangement.Center
90
+ ) {
91
+ Icon(
92
+ imageVector = Icons.Outlined.ErrorOutline,
93
+ contentDescription = null,
94
+ modifier = Modifier.size(64.dp),
95
+ tint = MaterialTheme.colorScheme.error
96
+ )
97
+ Spacer(Modifier.height(16.dp))
98
+ Text(
99
+ text = message,
100
+ style = MaterialTheme.typography.bodyLarge,
101
+ textAlign = TextAlign.Center,
102
+ color = MaterialTheme.colorScheme.onSurface
103
+ )
104
+ if (onRetry != null) {
105
+ Spacer(Modifier.height(24.dp))
106
+ Button(onClick = onRetry) {
107
+ Text(stringResource(R.string.error_retry))
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ // ✅ Inline error — below a field
114
+ @Composable
115
+ fun FieldError(
116
+ message: String,
117
+ modifier: Modifier = Modifier
118
+ ) {
119
+ Text(
120
+ text = message,
121
+ style = MaterialTheme.typography.bodySmall,
122
+ color = MaterialTheme.colorScheme.error,
123
+ modifier = modifier.padding(start = 16.dp, top = 4.dp)
124
+ )
125
+ }
126
+
127
+ // ✅ Error banner — non-blocking top/bottom banner
128
+ @Composable
129
+ fun ErrorBanner(
130
+ message: String,
131
+ onDismiss: (() -> Unit)? = null,
132
+ modifier: Modifier = Modifier
133
+ ) {
134
+ Surface(
135
+ modifier = modifier.fillMaxWidth(),
136
+ color = MaterialTheme.colorScheme.errorContainer,
137
+ tonalElevation = 2.dp
138
+ ) {
139
+ Row(
140
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
141
+ verticalAlignment = Alignment.CenterVertically
142
+ ) {
143
+ Icon(
144
+ imageVector = Icons.Outlined.Warning,
145
+ contentDescription = null,
146
+ tint = MaterialTheme.colorScheme.onErrorContainer,
147
+ modifier = Modifier.size(20.dp)
148
+ )
149
+ Spacer(Modifier.width(12.dp))
150
+ Text(
151
+ text = message,
152
+ style = MaterialTheme.typography.bodyMedium,
153
+ color = MaterialTheme.colorScheme.onErrorContainer,
154
+ modifier = Modifier.weight(1f)
155
+ )
156
+ if (onDismiss != null) {
157
+ IconButton(onClick = onDismiss, modifier = Modifier.size(24.dp)) {
158
+ Icon(Icons.Default.Close, contentDescription = stringResource(R.string.error_dismiss))
159
+ }
160
+ }
161
+ }
162
+ }
163
+ }
164
+ ```
165
+
166
+ ---
167
+
168
+ ## Snackbar Error Pattern
169
+
170
+ ```kotlin
171
+ // ✅ Transient error via snackbar
172
+ @Composable
173
+ fun ProductScreen(
174
+ viewModel: ProductViewModel = hiltViewModel()
175
+ ) {
176
+ val snackbarHostState = remember { SnackbarHostState() }
177
+ val context = LocalContext.current
178
+
179
+ // Collect one-time error events
180
+ LaunchedEffect(Unit) {
181
+ viewModel.events.collect { event ->
182
+ when (event) {
183
+ is ProductEvent.ShowError -> {
184
+ val result = snackbarHostState.showSnackbar(
185
+ message = event.message,
186
+ actionLabel = if (event.canRetry) context.getString(R.string.error_retry) else null,
187
+ duration = SnackbarDuration.Long
188
+ )
189
+ if (result == SnackbarResult.ActionPerformed && event.canRetry) {
190
+ viewModel.onRetry()
191
+ }
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) { padding ->
198
+ ProductContent(modifier = Modifier.padding(padding))
199
+ }
200
+ }
201
+ ```
202
+
203
+ ---
204
+
205
+ ## Form Validation Errors
206
+
207
+ ```kotlin
208
+ // ✅ Inline validation errors on form fields
209
+ @Composable
210
+ fun EmailField(
211
+ value: String,
212
+ error: String?,
213
+ onValueChange: (String) -> Unit,
214
+ modifier: Modifier = Modifier
215
+ ) {
216
+ Column(modifier = modifier) {
217
+ OutlinedTextField(
218
+ value = value,
219
+ onValueChange = onValueChange,
220
+ label = { Text("Email") },
221
+ isError = error != null,
222
+ supportingText = {
223
+ if (error != null) {
224
+ Text(
225
+ text = error,
226
+ color = MaterialTheme.colorScheme.error
227
+ )
228
+ }
229
+ },
230
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
231
+ singleLine = true,
232
+ modifier = Modifier.fillMaxWidth()
233
+ )
234
+ }
235
+ }
236
+
237
+ // ✅ UiState with per-field errors
238
+ data class RegistrationUiState(
239
+ val email: String = "",
240
+ val password: String = "",
241
+ val isLoading: Boolean = false,
242
+ val emailError: String? = null,
243
+ val passwordError: String? = null,
244
+ val generalError: String? = null
245
+ )
246
+ ```
247
+
248
+ ---
249
+
250
+ ## AppError → User Message Mapping
251
+
252
+ ```kotlin
253
+ // ✅ Centralized mapping — use string resources
254
+ @Composable
255
+ fun AppError.toMessage(): String = when (this) {
256
+ is AppError.Network.NoConnection -> stringResource(R.string.error_no_connection)
257
+ is AppError.Network.Timeout -> stringResource(R.string.error_timeout)
258
+ is AppError.Network.Unauthorized -> stringResource(R.string.error_session_expired)
259
+ is AppError.Network.Forbidden -> stringResource(R.string.error_no_permission)
260
+ is AppError.Network.NotFound -> stringResource(R.string.error_not_found)
261
+ is AppError.Network.ServerError -> stringResource(R.string.error_server)
262
+ is AppError.Network.RateLimited -> stringResource(R.string.error_rate_limited)
263
+ is AppError.Data.NotFound -> stringResource(R.string.error_item_not_found)
264
+ is AppError.Data.Conflict -> stringResource(R.string.error_conflict, field)
265
+ is AppError.Data.Validation -> errors.values.first()
266
+ is AppError.Data.InvalidState -> reason
267
+ is AppError.Storage.DiskFull -> stringResource(R.string.error_disk_full)
268
+ is AppError.Storage.Corrupted -> stringResource(R.string.error_data_corrupted)
269
+ is AppError.Storage.Unknown,
270
+ is AppError.Unexpected -> stringResource(R.string.error_unexpected)
271
+ else -> stringResource(R.string.error_unexpected)
272
+ }
273
+
274
+ // ✅ Non-composable mapping (for ViewModel)
275
+ fun AppError.toMessageResId(): Int = when (this) {
276
+ is AppError.Network.NoConnection -> R.string.error_no_connection
277
+ is AppError.Network.Timeout -> R.string.error_timeout
278
+ is AppError.Network.Unauthorized -> R.string.error_session_expired
279
+ // ...
280
+ else -> R.string.error_unexpected
281
+ }
282
+ ```
283
+
284
+ ---
285
+
286
+ ## Screen-Level Error Handling Pattern
287
+
288
+ ```kotlin
289
+ // ✅ Composable that handles all UiState cases including error
290
+ @Composable
291
+ fun ProductListScreen(
292
+ viewModel: ProductListViewModel = hiltViewModel()
293
+ ) {
294
+ val state by viewModel.state.collectAsStateWithLifecycle()
295
+
296
+ when (val s = state) {
297
+ is ProductListUiState.Loading -> {
298
+ LoadingIndicator(modifier = Modifier.fillMaxSize())
299
+ }
300
+ is ProductListUiState.Success -> {
301
+ ProductList(
302
+ products = s.products,
303
+ onProductClick = viewModel::onProductClick
304
+ )
305
+ }
306
+ is ProductListUiState.Error -> {
307
+ FullScreenError(
308
+ message = s.message,
309
+ onRetry = if (s.canRetry) viewModel::loadProducts else null
310
+ )
311
+ }
312
+ }
313
+ }
314
+ ```
315
+
316
+ ---
317
+
318
+ ## Anti-Patterns
319
+
320
+ - Showing raw exception messages (`e.message`) to users — technical and unhelpful
321
+ - Same snackbar for every error — `Unauthorized` needs login redirect, not a snackbar
322
+ - Error messages without action — "Error occurred" with no retry button frustrates users
323
+ - Hardcoding error strings in code — use string resources for localization
324
+ - Generic "Something went wrong" for every error — obscures actionable information
325
+
326
+ ---
327
+
328
+ ## Related Skills
329
+ - `domain-error-model` — typed errors being displayed
330
+ - `error-handling` — how errors reach the UI
331
+ - `failure-strategy` — which display method to use per error
332
+ - `state-management` — modeling error state in UiState
333
+ - `compose` — Compose fundamentals for error composables
334
+ - `material3` — error colors and components from Material 3