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,240 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: merge-strategy
|
|
3
|
+
description: >
|
|
4
|
+
Merge strategies for combining local and remote data changes in Android.
|
|
5
|
+
Load this skill when syncing collections, merging list changes,
|
|
6
|
+
handling additions/deletions on both sides, or implementing CRDT-like patterns.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Merge Strategy
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
A merge strategy defines how to combine two diverged versions of a dataset into a single consistent result. This is more complex than single-record conflict resolution — it handles collections where both sides may have added, modified, and deleted different items.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Merging requires knowledge of the **base state** — the last known common version
|
|
20
|
+
- Track **deletions explicitly** — a missing item could mean "deleted" or "not yet synced"
|
|
21
|
+
- Use **stable IDs** as the merge key — never use position or timestamp alone
|
|
22
|
+
- Make merge operations **idempotent** — running merge twice yields the same result
|
|
23
|
+
- Prefer **additive merges** where possible — deletions are the hardest case
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Collection Merge Patterns
|
|
28
|
+
|
|
29
|
+
### Pattern 1: Remote-Authoritative List Merge
|
|
30
|
+
|
|
31
|
+
```kotlin
|
|
32
|
+
// ✅ Server is source of truth for the list — local additions queued for push
|
|
33
|
+
fun mergeUserList(
|
|
34
|
+
localItems: List<UserEntity>,
|
|
35
|
+
remoteItems: List<UserDto>,
|
|
36
|
+
pendingLocalCreates: List<UserEntity>
|
|
37
|
+
): List<UserEntity> {
|
|
38
|
+
val remoteById = remoteItems.associateBy { it.id }
|
|
39
|
+
val localById = localItems.associateBy { it.id }
|
|
40
|
+
|
|
41
|
+
val merged = mutableListOf<UserEntity>()
|
|
42
|
+
|
|
43
|
+
// Add all remote items (authoritative)
|
|
44
|
+
remoteItems.forEach { remote ->
|
|
45
|
+
val local = localById[remote.id]
|
|
46
|
+
merged.add(
|
|
47
|
+
if (local != null && local.updatedAt > remote.updatedAt) {
|
|
48
|
+
local // local is newer — keep, will push
|
|
49
|
+
} else {
|
|
50
|
+
remote.toEntity() // remote wins
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Add local-only creates (not yet on server)
|
|
56
|
+
pendingLocalCreates
|
|
57
|
+
.filter { it.id !in remoteById }
|
|
58
|
+
.forEach { merged.add(it) }
|
|
59
|
+
|
|
60
|
+
return merged
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
### Pattern 2: Three-Way Collection Merge
|
|
67
|
+
|
|
68
|
+
```kotlin
|
|
69
|
+
// ✅ Three-way merge — base + local + remote
|
|
70
|
+
data class CollectionDiff<T>(
|
|
71
|
+
val added: List<T>,
|
|
72
|
+
val modified: List<T>,
|
|
73
|
+
val deleted: List<String> // IDs
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
fun <T : HasId> threeWayMerge(
|
|
77
|
+
base: List<T>,
|
|
78
|
+
local: List<T>,
|
|
79
|
+
remote: List<T>
|
|
80
|
+
): List<T> {
|
|
81
|
+
val baseIds = base.map { it.id }.toSet()
|
|
82
|
+
val localIds = local.map { it.id }.toSet()
|
|
83
|
+
val remoteIds = remote.map { it.id }.toSet()
|
|
84
|
+
|
|
85
|
+
val baseById = base.associateBy { it.id }
|
|
86
|
+
val localById = local.associateBy { it.id }
|
|
87
|
+
val remoteById = remote.associateBy { it.id }
|
|
88
|
+
|
|
89
|
+
val result = mutableListOf<T>()
|
|
90
|
+
|
|
91
|
+
// Items present in remote
|
|
92
|
+
remoteIds.forEach { id ->
|
|
93
|
+
val remoteItem = remoteById[id]!!
|
|
94
|
+
val localItem = localById[id]
|
|
95
|
+
val baseItem = baseById[id]
|
|
96
|
+
|
|
97
|
+
when {
|
|
98
|
+
// New on remote — add
|
|
99
|
+
id !in baseIds -> result.add(remoteItem)
|
|
100
|
+
|
|
101
|
+
// Deleted locally — don't add (local delete wins)
|
|
102
|
+
id !in localIds -> { /* skip — locally deleted */ }
|
|
103
|
+
|
|
104
|
+
// Modified on both sides — resolve conflict
|
|
105
|
+
localItem != baseItem && remoteItem != baseItem ->
|
|
106
|
+
result.add(resolveFieldConflict(localItem!!, remoteItem))
|
|
107
|
+
|
|
108
|
+
// Only local changed
|
|
109
|
+
localItem != baseItem -> result.add(localItem!!)
|
|
110
|
+
|
|
111
|
+
// Only remote changed, or neither changed
|
|
112
|
+
else -> result.add(remoteItem)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Items added locally (not on remote or base)
|
|
117
|
+
localIds
|
|
118
|
+
.filter { it !in baseIds && it !in remoteIds }
|
|
119
|
+
.forEach { result.add(localById[it]!!) }
|
|
120
|
+
|
|
121
|
+
return result
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface HasId {
|
|
125
|
+
val id: String
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
### Pattern 3: Additive-Only Merge (No Deletions)
|
|
132
|
+
|
|
133
|
+
```kotlin
|
|
134
|
+
// ✅ Simplest merge — items are never deleted, only added
|
|
135
|
+
// Use for: event logs, audit trails, append-only feeds
|
|
136
|
+
|
|
137
|
+
fun <T : HasId> additiveMerge(
|
|
138
|
+
local: List<T>,
|
|
139
|
+
remote: List<T>
|
|
140
|
+
): List<T> {
|
|
141
|
+
val localById = local.associateBy { it.id }
|
|
142
|
+
val remoteById = remote.associateBy { it.id }
|
|
143
|
+
|
|
144
|
+
// Union of both sets — remote wins on conflict
|
|
145
|
+
return (localById + remoteById).values.toList()
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### Pattern 4: Timestamp-Based List Merge
|
|
152
|
+
|
|
153
|
+
```kotlin
|
|
154
|
+
// ✅ Each item has a timestamp — most recent version of each ID wins
|
|
155
|
+
data class TimestampedItem<T>(val id: String, val data: T, val updatedAt: Long)
|
|
156
|
+
|
|
157
|
+
fun <T> timestampMerge(
|
|
158
|
+
local: List<TimestampedItem<T>>,
|
|
159
|
+
remote: List<TimestampedItem<T>>,
|
|
160
|
+
deletedRemoteIds: Set<String> = emptySet()
|
|
161
|
+
): List<TimestampedItem<T>> {
|
|
162
|
+
val merged = (local + remote)
|
|
163
|
+
.groupBy { it.id }
|
|
164
|
+
.mapValues { (_, versions) -> versions.maxByOrNull { it.updatedAt }!! }
|
|
165
|
+
.values
|
|
166
|
+
.filter { it.id !in deletedRemoteIds }
|
|
167
|
+
.toList()
|
|
168
|
+
|
|
169
|
+
return merged
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Tracking Deletions
|
|
176
|
+
|
|
177
|
+
```kotlin
|
|
178
|
+
// ✅ Server must provide a tombstone/deleted list
|
|
179
|
+
// Without this, you can't distinguish "deleted" from "not synced yet"
|
|
180
|
+
|
|
181
|
+
@Serializable
|
|
182
|
+
data class SyncResponseDto(
|
|
183
|
+
val updated: List<UserDto>,
|
|
184
|
+
val deletedIds: List<String>, // explicit deletions
|
|
185
|
+
val syncToken: String // cursor for next delta sync
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
// ✅ Apply deletions explicitly
|
|
189
|
+
suspend fun applySync(response: SyncResponseDto) {
|
|
190
|
+
db.withTransaction {
|
|
191
|
+
// Apply updates/additions
|
|
192
|
+
userDao.upsertAll(response.updated.map { it.toEntity() })
|
|
193
|
+
// Apply deletions
|
|
194
|
+
userDao.deleteByIds(response.deletedIds)
|
|
195
|
+
// Save sync cursor
|
|
196
|
+
prefs.saveSyncToken(response.syncToken)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## CRDT-Inspired Patterns
|
|
204
|
+
|
|
205
|
+
```kotlin
|
|
206
|
+
// ✅ Grow-only Set (G-Set) — items can only be added, never removed
|
|
207
|
+
// Use for: tags, labels, read receipts
|
|
208
|
+
|
|
209
|
+
class GrowOnlySet<T>(private val items: MutableSet<T> = mutableSetOf()) {
|
|
210
|
+
fun add(item: T) { items.add(item) }
|
|
211
|
+
fun merge(other: GrowOnlySet<T>): GrowOnlySet<T> =
|
|
212
|
+
GrowOnlySet((items + other.items).toMutableSet())
|
|
213
|
+
fun contains(item: T) = items.contains(item)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ✅ Last-Write-Wins Register (LWW-Register) — per-field versioning
|
|
217
|
+
data class LWWRegister<T>(val value: T, val timestamp: Long) {
|
|
218
|
+
fun merge(other: LWWRegister<T>): LWWRegister<T> =
|
|
219
|
+
if (other.timestamp > timestamp) other else this
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Anti-Patterns
|
|
226
|
+
|
|
227
|
+
- Replacing the entire local list with remote on every sync — loses local changes
|
|
228
|
+
- No tombstones for deletions — deleted items reappear after sync
|
|
229
|
+
- Using list position as merge key — positions shift, wrong items merged
|
|
230
|
+
- Merging without a base state — can't distinguish add from conflict
|
|
231
|
+
- Merging on the main thread — use Dispatchers.IO for large collections
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Related Skills
|
|
236
|
+
|
|
237
|
+
- `conflict-resolution` — single-record conflict handling
|
|
238
|
+
- `sync-engine` — orchestrating full sync with merge
|
|
239
|
+
- `room` — persisting merged results
|
|
240
|
+
- `dto-mapping` — mapping remote items for merge
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: migration
|
|
3
|
+
description: >
|
|
4
|
+
Room database migration — writing, testing, and managing schema changes.
|
|
5
|
+
Load this skill when changing the database schema (add/remove column or table,
|
|
6
|
+
change type), writing Migration objects, or testing migrations.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Migration
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
A migration defines how to transform the database schema from one version to the next without losing user data. Room requires a migration path for every version increment. Missing migrations cause a crash at startup — Room detects the schema mismatch and throws an `IllegalStateException`.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Core Principles
|
|
17
|
+
|
|
18
|
+
- **Never use `fallbackToDestructiveMigration()`** in production — it deletes all user data
|
|
19
|
+
- Always export schema (`exportSchema = true`) — required for writing correct migrations
|
|
20
|
+
- Write a migration for **every version increment** — even if only one table changed
|
|
21
|
+
- **Test every migration** — automated migration tests catch silent data corruption
|
|
22
|
+
- Keep migration SQL simple — complex logic belongs in a one-time data migration job
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Version Increment Workflow
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
1. Change @Entity — add/remove/rename column or table
|
|
30
|
+
2. Increment version in @Database
|
|
31
|
+
3. Write Migration(oldVersion, newVersion)
|
|
32
|
+
4. Add migration to RoomDatabase builder
|
|
33
|
+
5. Export schema and commit schemas/ directory
|
|
34
|
+
6. Write migration test
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Writing Migrations
|
|
40
|
+
|
|
41
|
+
```kotlin
|
|
42
|
+
// ✅ Add a column
|
|
43
|
+
val MIGRATION_1_2 = object : Migration(1, 2) {
|
|
44
|
+
override fun migrate(database: SupportSQLiteDatabase) {
|
|
45
|
+
database.execSQL(
|
|
46
|
+
"ALTER TABLE users ADD COLUMN phone_number TEXT"
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ✅ Add a column with default value
|
|
52
|
+
val MIGRATION_2_3 = object : Migration(2, 3) {
|
|
53
|
+
override fun migrate(database: SupportSQLiteDatabase) {
|
|
54
|
+
database.execSQL(
|
|
55
|
+
"ALTER TABLE users ADD COLUMN status TEXT NOT NULL DEFAULT 'active'"
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ✅ Add a new table
|
|
61
|
+
val MIGRATION_3_4 = object : Migration(3, 4) {
|
|
62
|
+
override fun migrate(database: SupportSQLiteDatabase) {
|
|
63
|
+
database.execSQL("""
|
|
64
|
+
CREATE TABLE IF NOT EXISTS orders (
|
|
65
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
66
|
+
user_id TEXT NOT NULL,
|
|
67
|
+
total INTEGER NOT NULL,
|
|
68
|
+
created_at INTEGER NOT NULL,
|
|
69
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
70
|
+
)
|
|
71
|
+
""")
|
|
72
|
+
database.execSQL(
|
|
73
|
+
"CREATE INDEX IF NOT EXISTS index_orders_user_id ON orders(user_id)"
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ✅ Rename a column (SQLite doesn't support RENAME COLUMN before API 29)
|
|
79
|
+
val MIGRATION_4_5 = object : Migration(4, 5) {
|
|
80
|
+
override fun migrate(database: SupportSQLiteDatabase) {
|
|
81
|
+
// Create new table with correct schema
|
|
82
|
+
database.execSQL("""
|
|
83
|
+
CREATE TABLE users_new (
|
|
84
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
85
|
+
full_name TEXT NOT NULL,
|
|
86
|
+
email TEXT NOT NULL
|
|
87
|
+
)
|
|
88
|
+
""")
|
|
89
|
+
// Copy data
|
|
90
|
+
database.execSQL("""
|
|
91
|
+
INSERT INTO users_new (id, full_name, email)
|
|
92
|
+
SELECT id, name, email FROM users
|
|
93
|
+
""")
|
|
94
|
+
// Drop old table
|
|
95
|
+
database.execSQL("DROP TABLE users")
|
|
96
|
+
// Rename new table
|
|
97
|
+
database.execSQL("ALTER TABLE users_new RENAME TO users")
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ✅ Drop a column (same approach — recreate table)
|
|
102
|
+
val MIGRATION_5_6 = object : Migration(5, 6) {
|
|
103
|
+
override fun migrate(database: SupportSQLiteDatabase) {
|
|
104
|
+
database.execSQL("""
|
|
105
|
+
CREATE TABLE users_new (
|
|
106
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
107
|
+
full_name TEXT NOT NULL
|
|
108
|
+
-- removed: phone_number
|
|
109
|
+
)
|
|
110
|
+
""")
|
|
111
|
+
database.execSQL("INSERT INTO users_new SELECT id, full_name FROM users")
|
|
112
|
+
database.execSQL("DROP TABLE users")
|
|
113
|
+
database.execSQL("ALTER TABLE users_new RENAME TO users")
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Registering Migrations
|
|
121
|
+
|
|
122
|
+
```kotlin
|
|
123
|
+
// ✅ Register all migrations in database builder
|
|
124
|
+
@Module
|
|
125
|
+
@InstallIn(SingletonComponent::class)
|
|
126
|
+
object DatabaseModule {
|
|
127
|
+
|
|
128
|
+
@Provides
|
|
129
|
+
@Singleton
|
|
130
|
+
fun provideDatabase(@ApplicationContext context: Context): AppDatabase =
|
|
131
|
+
Room.databaseBuilder(context, AppDatabase::class.java, "app_database")
|
|
132
|
+
.addMigrations(
|
|
133
|
+
MIGRATION_1_2,
|
|
134
|
+
MIGRATION_2_3,
|
|
135
|
+
MIGRATION_3_4,
|
|
136
|
+
MIGRATION_4_5,
|
|
137
|
+
MIGRATION_5_6
|
|
138
|
+
)
|
|
139
|
+
.build()
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Auto Migration (Room 2.4+)
|
|
146
|
+
|
|
147
|
+
```kotlin
|
|
148
|
+
// ✅ For simple schema changes — Room generates migration automatically
|
|
149
|
+
@Database(
|
|
150
|
+
entities = [UserEntity::class],
|
|
151
|
+
version = 3,
|
|
152
|
+
exportSchema = true,
|
|
153
|
+
autoMigrations = [
|
|
154
|
+
AutoMigration(from = 1, to = 2), // simple add column — auto-generated
|
|
155
|
+
AutoMigration(from = 2, to = 3, spec = Migration2To3::class) // with rename
|
|
156
|
+
]
|
|
157
|
+
)
|
|
158
|
+
abstract class AppDatabase : RoomDatabase()
|
|
159
|
+
|
|
160
|
+
// ✅ AutoMigrationSpec — for rename/delete (needs disambiguation)
|
|
161
|
+
@RenameColumn(tableName = "users", fromColumnName = "name", toColumnName = "full_name")
|
|
162
|
+
class Migration2To3 : AutoMigrationSpec
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Testing Migrations
|
|
168
|
+
|
|
169
|
+
```kotlin
|
|
170
|
+
// ✅ MigrationTestHelper — test each migration
|
|
171
|
+
@RunWith(AndroidJUnit4::class)
|
|
172
|
+
class MigrationTest {
|
|
173
|
+
|
|
174
|
+
@get:Rule
|
|
175
|
+
val helper = MigrationTestHelper(
|
|
176
|
+
InstrumentationRegistry.getInstrumentation(),
|
|
177
|
+
AppDatabase::class.java
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
@Test
|
|
181
|
+
fun migrate1To2() {
|
|
182
|
+
// Create version 1 database
|
|
183
|
+
helper.createDatabase(TEST_DB, 1).apply {
|
|
184
|
+
execSQL("INSERT INTO users VALUES ('1', 'Ali')")
|
|
185
|
+
close()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Run migration
|
|
189
|
+
val db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2)
|
|
190
|
+
|
|
191
|
+
// Verify data integrity
|
|
192
|
+
val cursor = db.query("SELECT * FROM users WHERE id = '1'")
|
|
193
|
+
assertTrue(cursor.moveToFirst())
|
|
194
|
+
assertEquals("Ali", cursor.getString(cursor.getColumnIndex("full_name")))
|
|
195
|
+
// new column exists with default value
|
|
196
|
+
assertNotNull(cursor.getString(cursor.getColumnIndex("phone_number")))
|
|
197
|
+
cursor.close()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
companion object {
|
|
201
|
+
private const val TEST_DB = "migration_test"
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Multi-Step Migration Path
|
|
209
|
+
|
|
210
|
+
```kotlin
|
|
211
|
+
// ✅ Room applies migrations sequentially — 1→2→3→4 not 1→4
|
|
212
|
+
// All intermediate migrations must be present
|
|
213
|
+
|
|
214
|
+
// If user has version 1, Room applies:
|
|
215
|
+
// MIGRATION_1_2, then MIGRATION_2_3, then MIGRATION_3_4
|
|
216
|
+
|
|
217
|
+
// ✅ Multi-version single migration (skip versions)
|
|
218
|
+
val MIGRATION_1_4 = object : Migration(1, 4) {
|
|
219
|
+
override fun migrate(database: SupportSQLiteDatabase) {
|
|
220
|
+
// Apply all changes from 1→4 in one step
|
|
221
|
+
// Only needed if you want to optimize multi-version upgrades
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Anti-Patterns
|
|
229
|
+
|
|
230
|
+
- `fallbackToDestructiveMigration()` in production — deletes all user data
|
|
231
|
+
- Not exporting schema (`exportSchema = false`) — can't write correct migrations
|
|
232
|
+
- Running data transformations in migration SQL — use a one-time WorkManager job instead
|
|
233
|
+
- Not committing `schemas/` directory to version control — migration tests require it
|
|
234
|
+
- Incrementing version without writing migration — crashes on existing installs
|
|
235
|
+
- Writing migration without a test — silent data corruption risk
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Related Skills
|
|
240
|
+
- `room` — Room database setup
|
|
241
|
+
- `dao` — DAO queries after schema changes
|
|
242
|
+
- `database-versioning-strategy` — when and how to version the schema
|
|
243
|
+
- `workmanager` — one-time data migration jobs after schema change
|