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,264 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: datastore
|
|
3
|
+
description: >
|
|
4
|
+
Jetpack DataStore (Preferences) setup and usage for Android.
|
|
5
|
+
Load this skill when storing simple key-value settings, user preferences,
|
|
6
|
+
feature flags, or any small structured data that doesn't need a full database.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# DataStore (Preferences)
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
Jetpack DataStore is the modern replacement for SharedPreferences. It stores key-value pairs (Preferences DataStore) or typed objects (Proto DataStore) asynchronously using coroutines and Flow. It is safe to call from the main thread and handles concurrent access correctly.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Core Principles
|
|
17
|
+
|
|
18
|
+
- Use DataStore for **small, simple data** — user settings, session state, feature flags
|
|
19
|
+
- Use Room for **structured, queryable data** — DataStore is not a database
|
|
20
|
+
- **Never block** to read DataStore — always collect as Flow
|
|
21
|
+
- Create **one DataStore instance** per file — never multiple instances for the same file
|
|
22
|
+
- Inject DataStore via **Hilt** — never create it in a ViewModel or Repository directly
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Setup
|
|
27
|
+
|
|
28
|
+
```toml
|
|
29
|
+
# libs.versions.toml
|
|
30
|
+
[versions]
|
|
31
|
+
datastore = "1.1.1"
|
|
32
|
+
|
|
33
|
+
[libraries]
|
|
34
|
+
datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```kotlin
|
|
38
|
+
// build.gradle.kts
|
|
39
|
+
dependencies {
|
|
40
|
+
implementation(libs.datastore.preferences)
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Creating DataStore
|
|
47
|
+
|
|
48
|
+
```kotlin
|
|
49
|
+
// ✅ Create via extension — one instance per file
|
|
50
|
+
val Context.userPreferencesDataStore: DataStore<Preferences> by preferencesDataStore(
|
|
51
|
+
name = "user_preferences"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
// ✅ Provide via Hilt — singleton
|
|
55
|
+
@Module
|
|
56
|
+
@InstallIn(SingletonComponent::class)
|
|
57
|
+
object DataStoreModule {
|
|
58
|
+
|
|
59
|
+
@Provides
|
|
60
|
+
@Singleton
|
|
61
|
+
fun provideUserPreferencesDataStore(
|
|
62
|
+
@ApplicationContext context: Context
|
|
63
|
+
): DataStore<Preferences> = context.userPreferencesDataStore
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Defining Keys
|
|
70
|
+
|
|
71
|
+
```kotlin
|
|
72
|
+
// ✅ Define all keys in a companion object or object
|
|
73
|
+
object UserPreferencesKeys {
|
|
74
|
+
val IS_DARK_MODE = booleanPreferencesKey("is_dark_mode")
|
|
75
|
+
val LANGUAGE = stringPreferencesKey("language")
|
|
76
|
+
val FONT_SIZE = intPreferencesKey("font_size")
|
|
77
|
+
val LAST_SYNC = longPreferencesKey("last_sync_timestamp")
|
|
78
|
+
val ONBOARDING_COMPLETE = booleanPreferencesKey("onboarding_complete")
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Repository Pattern
|
|
85
|
+
|
|
86
|
+
```kotlin
|
|
87
|
+
// ✅ Wrap DataStore in a Repository — don't access it directly from ViewModel
|
|
88
|
+
class UserPreferencesRepository @Inject constructor(
|
|
89
|
+
private val dataStore: DataStore<Preferences>
|
|
90
|
+
) {
|
|
91
|
+
|
|
92
|
+
// ✅ Read — expose as Flow
|
|
93
|
+
val isDarkMode: Flow<Boolean> = dataStore.data
|
|
94
|
+
.catch { e ->
|
|
95
|
+
if (e is IOException) emit(emptyPreferences())
|
|
96
|
+
else throw e
|
|
97
|
+
}
|
|
98
|
+
.map { prefs -> prefs[UserPreferencesKeys.IS_DARK_MODE] ?: false }
|
|
99
|
+
|
|
100
|
+
val language: Flow<String> = dataStore.data
|
|
101
|
+
.catch { e ->
|
|
102
|
+
if (e is IOException) emit(emptyPreferences())
|
|
103
|
+
else throw e
|
|
104
|
+
}
|
|
105
|
+
.map { prefs -> prefs[UserPreferencesKeys.LANGUAGE] ?: "en" }
|
|
106
|
+
|
|
107
|
+
// ✅ Read all at once — single object
|
|
108
|
+
val userPreferences: Flow<UserPreferences> = dataStore.data
|
|
109
|
+
.catch { e ->
|
|
110
|
+
if (e is IOException) emit(emptyPreferences())
|
|
111
|
+
else throw e
|
|
112
|
+
}
|
|
113
|
+
.map { prefs ->
|
|
114
|
+
UserPreferences(
|
|
115
|
+
isDarkMode = prefs[UserPreferencesKeys.IS_DARK_MODE] ?: false,
|
|
116
|
+
language = prefs[UserPreferencesKeys.LANGUAGE] ?: "en",
|
|
117
|
+
fontSize = prefs[UserPreferencesKeys.FONT_SIZE] ?: 14
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ✅ Write — suspend function
|
|
122
|
+
suspend fun setDarkMode(enabled: Boolean) {
|
|
123
|
+
dataStore.edit { prefs ->
|
|
124
|
+
prefs[UserPreferencesKeys.IS_DARK_MODE] = enabled
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
suspend fun setLanguage(language: String) {
|
|
129
|
+
dataStore.edit { prefs ->
|
|
130
|
+
prefs[UserPreferencesKeys.LANGUAGE] = language
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ✅ Clear all preferences
|
|
135
|
+
suspend fun clearAll() {
|
|
136
|
+
dataStore.edit { it.clear() }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ✅ Domain model for preferences
|
|
141
|
+
data class UserPreferences(
|
|
142
|
+
val isDarkMode: Boolean,
|
|
143
|
+
val language: String,
|
|
144
|
+
val fontSize: Int
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Usage in ViewModel
|
|
151
|
+
|
|
152
|
+
```kotlin
|
|
153
|
+
@HiltViewModel
|
|
154
|
+
class SettingsViewModel @Inject constructor(
|
|
155
|
+
private val preferencesRepository: UserPreferencesRepository
|
|
156
|
+
) : ViewModel() {
|
|
157
|
+
|
|
158
|
+
val preferences: StateFlow<UserPreferences> = preferencesRepository.userPreferences
|
|
159
|
+
.stateIn(
|
|
160
|
+
scope = viewModelScope,
|
|
161
|
+
started = SharingStarted.WhileSubscribed(5_000),
|
|
162
|
+
initialValue = UserPreferences(isDarkMode = false, language = "en", fontSize = 14)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
fun onDarkModeToggled(enabled: Boolean) {
|
|
166
|
+
viewModelScope.launch {
|
|
167
|
+
preferencesRepository.setDarkMode(enabled)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
fun onLanguageSelected(language: String) {
|
|
172
|
+
viewModelScope.launch {
|
|
173
|
+
preferencesRepository.setLanguage(language)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Atomic Updates
|
|
182
|
+
|
|
183
|
+
```kotlin
|
|
184
|
+
// ✅ update multiple keys atomically in one edit block
|
|
185
|
+
suspend fun saveUserSession(userId: String, token: String, expiresAt: Long) {
|
|
186
|
+
dataStore.edit { prefs ->
|
|
187
|
+
prefs[USER_ID_KEY] = userId
|
|
188
|
+
prefs[TOKEN_KEY] = token
|
|
189
|
+
prefs[EXPIRES_AT_KEY] = expiresAt
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ✅ Conditional update
|
|
194
|
+
suspend fun incrementLaunchCount() {
|
|
195
|
+
dataStore.edit { prefs ->
|
|
196
|
+
val current = prefs[LAUNCH_COUNT_KEY] ?: 0
|
|
197
|
+
prefs[LAUNCH_COUNT_KEY] = current + 1
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Error Handling
|
|
205
|
+
|
|
206
|
+
```kotlin
|
|
207
|
+
// ✅ Always handle IOException — disk read/write can fail
|
|
208
|
+
val preferences: Flow<AppPreferences> = dataStore.data
|
|
209
|
+
.catch { exception ->
|
|
210
|
+
if (exception is IOException) {
|
|
211
|
+
// Emit defaults on read error
|
|
212
|
+
emit(emptyPreferences())
|
|
213
|
+
} else {
|
|
214
|
+
throw exception // re-throw unexpected errors
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
.map { prefs -> prefs.toAppPreferences() }
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## DataStore vs SharedPreferences
|
|
223
|
+
|
|
224
|
+
| | DataStore | SharedPreferences |
|
|
225
|
+
|--|-----------|------------------|
|
|
226
|
+
| Thread safety | ✅ Coroutines | ❌ Partial |
|
|
227
|
+
| Main thread safe | ✅ Yes | ❌ No (apply is async, commit blocks) |
|
|
228
|
+
| Strongly typed | ✅ Yes | ❌ No |
|
|
229
|
+
| Error handling | ✅ Flow catch | ❌ Silent |
|
|
230
|
+
| Transactions | ✅ edit {} | ❌ No |
|
|
231
|
+
| Migration | ✅ SharedPreferencesMigration | — |
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Migrating from SharedPreferences
|
|
236
|
+
|
|
237
|
+
```kotlin
|
|
238
|
+
// ✅ Migrate existing SharedPreferences data
|
|
239
|
+
val dataStore = context.createDataStore(
|
|
240
|
+
name = "user_preferences",
|
|
241
|
+
migrations = listOf(
|
|
242
|
+
SharedPreferencesMigration(context, "legacy_prefs")
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Anti-Patterns
|
|
250
|
+
|
|
251
|
+
- Multiple DataStore instances for same file — data corruption risk
|
|
252
|
+
- Calling `dataStore.data.first()` in a loop — use Flow collection instead
|
|
253
|
+
- Storing large objects or lists in DataStore — use Room instead
|
|
254
|
+
- Not catching `IOException` — unhandled disk errors crash the app
|
|
255
|
+
- Accessing DataStore directly in ViewModel — wrap in Repository
|
|
256
|
+
- Using DataStore for data that needs querying/filtering — use Room
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Related Skills
|
|
261
|
+
- `proto-datastore` — typed object storage with Protobuf
|
|
262
|
+
- `repository-pattern` — wrapping DataStore in a Repository
|
|
263
|
+
- `hilt` — providing DataStore as a singleton
|
|
264
|
+
- `key-value-store-strategy` — choosing the right storage for the use case
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: database-versioning-strategy
|
|
3
|
+
description: >
|
|
4
|
+
Strategy for managing Room database version increments across releases.
|
|
5
|
+
Load this skill when planning schema changes, deciding when to increment
|
|
6
|
+
version, managing version history, or coordinating schema changes across
|
|
7
|
+
multiple developers/branches.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Database Versioning Strategy
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
Database versioning is the discipline of managing schema changes in a controlled, predictable way. Every schema change must be planned, versioned, documented, and tested before release. A broken migration path crashes the app on existing installs.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- **One version increment per release** — batch all schema changes for a release into one migration
|
|
20
|
+
- **Never reuse a version number** — once a version is released, it's permanent
|
|
21
|
+
- **Never modify a released migration** — write a new one instead
|
|
22
|
+
- Export and commit the schema file — it's the source of truth for migration correctness
|
|
23
|
+
- Plan schema changes before implementation — migrations are hard to undo
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Version Numbering
|
|
28
|
+
|
|
29
|
+
```kotlin
|
|
30
|
+
// ✅ Increment by 1 for each release with schema changes
|
|
31
|
+
@Database(version = 5, ...) // was 4 in previous release
|
|
32
|
+
|
|
33
|
+
// ✅ Never skip version numbers
|
|
34
|
+
// Bad: 1 → 3 (skipped 2) — users on v2 have no migration path to v3
|
|
35
|
+
// Good: 1 → 2 → 3
|
|
36
|
+
|
|
37
|
+
// ✅ During development — batch changes
|
|
38
|
+
// Don't increment version for every local change during a sprint
|
|
39
|
+
// Increment once when the feature/release is ready
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Schema Export and Version Control
|
|
45
|
+
|
|
46
|
+
```kotlin
|
|
47
|
+
// ✅ Always export schema
|
|
48
|
+
@Database(
|
|
49
|
+
entities = [...],
|
|
50
|
+
version = 5,
|
|
51
|
+
exportSchema = true // generates schemas/com.example.AppDatabase/5.json
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
// build.gradle.kts
|
|
55
|
+
ksp {
|
|
56
|
+
arg("room.schemaLocation", "$projectDir/schemas")
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
// ✅ Commit schema files to version control
|
|
62
|
+
schemas/
|
|
63
|
+
com.example.AppDatabase/
|
|
64
|
+
1.json ← version 1 schema
|
|
65
|
+
2.json ← version 2 schema
|
|
66
|
+
3.json ← version 3 schema
|
|
67
|
+
4.json ← version 4 schema
|
|
68
|
+
5.json ← version 5 schema (current)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Change Planning Checklist
|
|
74
|
+
|
|
75
|
+
Before making any schema change:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
☐ What tables/columns are affected?
|
|
79
|
+
☐ Is existing data preserved? How?
|
|
80
|
+
☐ Is the migration reversible? (Usually not — plan carefully)
|
|
81
|
+
☐ Are there indices that need updating?
|
|
82
|
+
☐ Are there related DAOs that need query updates?
|
|
83
|
+
☐ Are there DTOs/mappers that need updating?
|
|
84
|
+
☐ Is a data migration needed (not just schema migration)?
|
|
85
|
+
☐ Is this the only schema change for this release?
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Multi-Developer Coordination
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
// ✅ Strategy when multiple developers change schema simultaneously
|
|
94
|
+
|
|
95
|
+
// Developer A: adds column to users table
|
|
96
|
+
// Developer B: adds new orders table
|
|
97
|
+
|
|
98
|
+
// On merge:
|
|
99
|
+
// 1. Agree on a single version number for the combined change
|
|
100
|
+
// 2. Write ONE migration that includes BOTH changes
|
|
101
|
+
// 3. Never have two separate migrations with the same version increment
|
|
102
|
+
|
|
103
|
+
// ✅ Use a migration registry file to coordinate
|
|
104
|
+
// migrations/MIGRATION_LOG.md
|
|
105
|
+
// Version 5 (Release 2.3.0):
|
|
106
|
+
// - Added phone_number to users (Developer A)
|
|
107
|
+
// - Added orders table (Developer B)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Data Migration vs Schema Migration
|
|
113
|
+
|
|
114
|
+
```kotlin
|
|
115
|
+
// Schema migration — changes structure
|
|
116
|
+
val MIGRATION_4_5 = object : Migration(4, 5) {
|
|
117
|
+
override fun migrate(database: SupportSQLiteDatabase) {
|
|
118
|
+
database.execSQL("ALTER TABLE users ADD COLUMN tier TEXT NOT NULL DEFAULT 'free'")
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ✅ Data migration — backfill or transform data after schema change
|
|
123
|
+
// Do NOT do complex data transformations in Migration.migrate()
|
|
124
|
+
// Use a one-time WorkManager job instead
|
|
125
|
+
|
|
126
|
+
class BackfillUserTierWorker(
|
|
127
|
+
context: Context,
|
|
128
|
+
params: WorkerParameters
|
|
129
|
+
) : CoroutineWorker(context, params) {
|
|
130
|
+
|
|
131
|
+
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
|
|
132
|
+
try {
|
|
133
|
+
val users = userDao.getAllOnce()
|
|
134
|
+
users.forEach { user ->
|
|
135
|
+
val tier = computeTierFromHistory(user)
|
|
136
|
+
userDao.updateTier(user.id, tier)
|
|
137
|
+
}
|
|
138
|
+
Result.success()
|
|
139
|
+
} catch (e: Exception) {
|
|
140
|
+
Result.retry()
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Schedule after migration is complete
|
|
146
|
+
fun scheduleBackfillIfNeeded(prefs: SharedPreferences) {
|
|
147
|
+
if (!prefs.getBoolean("backfill_tier_done", false)) {
|
|
148
|
+
WorkManager.getInstance(context).enqueueUniqueWork(
|
|
149
|
+
"backfill_user_tier",
|
|
150
|
+
ExistingWorkPolicy.KEEP,
|
|
151
|
+
OneTimeWorkRequestBuilder<BackfillUserTierWorker>().build()
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Emergency: Breaking Schema in Development
|
|
160
|
+
|
|
161
|
+
```kotlin
|
|
162
|
+
// ✅ During development only — if migration is too complex and data doesn't matter
|
|
163
|
+
Room.databaseBuilder(context, AppDatabase::class.java, "app_database")
|
|
164
|
+
.fallbackToDestructiveMigration() // ❌ NEVER in production release
|
|
165
|
+
.build()
|
|
166
|
+
|
|
167
|
+
// ✅ Better approach in development — clear app data manually
|
|
168
|
+
// adb shell pm clear com.example.app
|
|
169
|
+
|
|
170
|
+
// ✅ Or use a different DB name per build type
|
|
171
|
+
val dbName = if (BuildConfig.DEBUG) "app_database_debug" else "app_database"
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Version History Documentation
|
|
177
|
+
|
|
178
|
+
```markdown
|
|
179
|
+
<!-- schemas/CHANGELOG.md — maintain alongside schema files -->
|
|
180
|
+
|
|
181
|
+
## Version 5 (Release 2.3.0 — 2024-11-15)
|
|
182
|
+
- Added `phone_number TEXT` to `users` table
|
|
183
|
+
- Added `orders` table with foreign key to `users`
|
|
184
|
+
- Added index on `orders.user_id`
|
|
185
|
+
|
|
186
|
+
## Version 4 (Release 2.2.0 — 2024-09-01)
|
|
187
|
+
- Added `status TEXT NOT NULL DEFAULT 'active'` to `users`
|
|
188
|
+
|
|
189
|
+
## Version 3 (Release 2.0.0 — 2024-06-10)
|
|
190
|
+
- Renamed column `name` → `full_name` in `users`
|
|
191
|
+
|
|
192
|
+
## Version 2 (Release 1.5.0 — 2024-03-20)
|
|
193
|
+
- Added `created_at INTEGER` to `users`
|
|
194
|
+
|
|
195
|
+
## Version 1 (Initial release — 2024-01-10)
|
|
196
|
+
- Initial schema: `users` table
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Anti-Patterns
|
|
202
|
+
|
|
203
|
+
- Incrementing version during development for every small change — creates unnecessary migrations
|
|
204
|
+
- Not committing schema JSON files — migration tests fail, history is lost
|
|
205
|
+
- Writing migration without updating the version in `@Database` — causes crash
|
|
206
|
+
- Two migrations with overlapping version ranges — Room can't resolve path
|
|
207
|
+
- Complex business logic inside `Migration.migrate()` — use WorkManager instead
|
|
208
|
+
- Releasing without testing migration from the previous production version
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Related Skills
|
|
213
|
+
- `migration` — writing individual migration objects
|
|
214
|
+
- `room` — Room database configuration
|
|
215
|
+
- `workmanager` — data migration jobs post-schema-change
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: encrypted-database
|
|
3
|
+
description: >
|
|
4
|
+
Encrypted SQLite database using SQLCipher and Room for Android.
|
|
5
|
+
Load this skill when storing sensitive user data that requires encryption
|
|
6
|
+
at rest, implementing encrypted Room database, or managing encryption keys.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Encrypted Database
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
SQLCipher provides transparent AES-256 encryption for SQLite databases. When integrated with Room, the entire database file is encrypted at rest. This is required for apps handling sensitive data — health records, financial data, private messages, or enterprise security requirements.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Use SQLCipher when the database contains **sensitive user data**
|
|
20
|
+
- Encryption key must be stored in **Android Keystore** — never hardcoded
|
|
21
|
+
- Key derivation from user password uses PBKDF2 — never use raw password as key
|
|
22
|
+
- Encrypted database has a **performance overhead** — benchmark before requiring it
|
|
23
|
+
- Test encryption on real devices — emulators may behave differently
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Setup
|
|
28
|
+
|
|
29
|
+
```toml
|
|
30
|
+
[versions]
|
|
31
|
+
sqlcipher = "4.5.4"
|
|
32
|
+
security-crypto = "1.1.0-alpha06"
|
|
33
|
+
|
|
34
|
+
[libraries]
|
|
35
|
+
sqlcipher-android = { module = "net.zetetic:sqlcipher-android", version.ref = "sqlcipher" }
|
|
36
|
+
sqlite-ktx = { module = "androidx.sqlite:sqlite-ktx", version = "2.4.0" }
|
|
37
|
+
security-crypto = { module = "androidx.security:security-crypto", version.ref = "security-crypto" }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```kotlin
|
|
41
|
+
dependencies {
|
|
42
|
+
implementation(libs.sqlcipher.android)
|
|
43
|
+
implementation(libs.sqlite.ktx)
|
|
44
|
+
implementation(libs.security.crypto)
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Key Management with Android Keystore
|
|
51
|
+
|
|
52
|
+
```kotlin
|
|
53
|
+
// ✅ Generate or retrieve encryption key from Keystore
|
|
54
|
+
object DatabaseKeyManager {
|
|
55
|
+
|
|
56
|
+
private const val KEY_ALIAS = "db_encryption_key"
|
|
57
|
+
private const val PREFS_NAME = "db_key_prefs"
|
|
58
|
+
private const val PREFS_KEY = "encrypted_db_passphrase"
|
|
59
|
+
|
|
60
|
+
fun getOrCreateKey(context: Context): ByteArray {
|
|
61
|
+
val masterKey = MasterKey.Builder(context)
|
|
62
|
+
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
|
63
|
+
.build()
|
|
64
|
+
|
|
65
|
+
val prefs = EncryptedSharedPreferences.create(
|
|
66
|
+
context,
|
|
67
|
+
PREFS_NAME,
|
|
68
|
+
masterKey,
|
|
69
|
+
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
|
70
|
+
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
val existing = prefs.getString(PREFS_KEY, null)
|
|
74
|
+
if (existing != null) {
|
|
75
|
+
return Base64.decode(existing, Base64.NO_WRAP)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Generate new random key
|
|
79
|
+
val newKey = ByteArray(32).also { SecureRandom().nextBytes(it) }
|
|
80
|
+
prefs.edit()
|
|
81
|
+
.putString(PREFS_KEY, Base64.encodeToString(newKey, Base64.NO_WRAP))
|
|
82
|
+
.apply()
|
|
83
|
+
return newKey
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Room with SQLCipher
|
|
91
|
+
|
|
92
|
+
```kotlin
|
|
93
|
+
// ✅ Custom SupportSQLiteOpenHelper.Factory using SQLCipher
|
|
94
|
+
@Module
|
|
95
|
+
@InstallIn(SingletonComponent::class)
|
|
96
|
+
object DatabaseModule {
|
|
97
|
+
|
|
98
|
+
@Provides
|
|
99
|
+
@Singleton
|
|
100
|
+
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
|
|
101
|
+
val passphrase = DatabaseKeyManager.getOrCreateKey(context)
|
|
102
|
+
val factory = SupportFactory(passphrase)
|
|
103
|
+
|
|
104
|
+
return Room.databaseBuilder(
|
|
105
|
+
context,
|
|
106
|
+
AppDatabase::class.java,
|
|
107
|
+
"encrypted_app_database"
|
|
108
|
+
)
|
|
109
|
+
.openHelperFactory(factory)
|
|
110
|
+
.addMigrations(MIGRATION_1_2)
|
|
111
|
+
.build()
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## User-Password-Derived Key
|
|
119
|
+
|
|
120
|
+
```kotlin
|
|
121
|
+
// ✅ For apps where encryption key is derived from user's password
|
|
122
|
+
object PasswordKeyDerivation {
|
|
123
|
+
|
|
124
|
+
fun deriveKey(password: CharArray, salt: ByteArray): ByteArray {
|
|
125
|
+
val spec = PBEKeySpec(
|
|
126
|
+
password,
|
|
127
|
+
salt,
|
|
128
|
+
100_000, // iterations
|
|
129
|
+
256 // key length in bits
|
|
130
|
+
)
|
|
131
|
+
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
|
132
|
+
return factory.generateSecret(spec).encoded
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
fun generateSalt(): ByteArray {
|
|
136
|
+
return ByteArray(32).also { SecureRandom().nextBytes(it) }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Store the salt (not the key) in EncryptedSharedPreferences
|
|
141
|
+
// Re-derive the key each session from user's password
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Verifying Encryption
|
|
147
|
+
|
|
148
|
+
```kotlin
|
|
149
|
+
// ✅ Verify database is encrypted (for debug/test)
|
|
150
|
+
fun isDatabaseEncrypted(context: Context, dbName: String): Boolean {
|
|
151
|
+
val dbFile = context.getDatabasePath(dbName)
|
|
152
|
+
if (!dbFile.exists()) return false
|
|
153
|
+
|
|
154
|
+
return try {
|
|
155
|
+
val bytes = dbFile.readBytes().take(16).toByteArray()
|
|
156
|
+
val header = String(bytes)
|
|
157
|
+
!header.startsWith("SQLite format 3") // encrypted files don't start with this
|
|
158
|
+
} catch (e: Exception) {
|
|
159
|
+
false
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Migration with Encrypted Database
|
|
167
|
+
|
|
168
|
+
```kotlin
|
|
169
|
+
// ✅ Migrations work the same way — SQLCipher handles the encryption transparently
|
|
170
|
+
val MIGRATION_1_2 = object : Migration(1, 2) {
|
|
171
|
+
override fun migrate(database: SupportSQLiteDatabase) {
|
|
172
|
+
database.execSQL("ALTER TABLE users ADD COLUMN bio TEXT")
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// No special handling needed — Room + SQLCipher = transparent
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Performance Considerations
|
|
182
|
+
|
|
183
|
+
```kotlin
|
|
184
|
+
// ✅ SQLCipher overhead — benchmark before requiring encryption
|
|
185
|
+
// Typical overhead: 5-15% slower reads, 5-20% slower writes
|
|
186
|
+
// Acceptable for most use cases — unacceptable for high-frequency sensors/logs
|
|
187
|
+
|
|
188
|
+
// ✅ If encryption overhead is a concern:
|
|
189
|
+
// - Encrypt only the sensitive tables using separate database files
|
|
190
|
+
// - Use per-row encryption for specific columns instead of full-DB encryption
|
|
191
|
+
// - Profile with Macrobenchmark before deciding
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Anti-Patterns
|
|
197
|
+
|
|
198
|
+
- Hardcoding the encryption passphrase in code — visible in decompiled APK
|
|
199
|
+
- Using the user's password directly as the DB key — weak, changes break DB
|
|
200
|
+
- Not storing the salt — can't re-derive key from password
|
|
201
|
+
- Not using Android Keystore to protect the DB key — key accessible on rooted devices
|
|
202
|
+
- Encrypting non-sensitive data — unnecessary performance overhead
|
|
203
|
+
- Sharing encrypted DB across users on the same device — key management nightmare
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Related Skills
|
|
208
|
+
|
|
209
|
+
- `room` — Room database setup
|
|
210
|
+
- `keystore` — Android Keystore for key storage
|
|
211
|
+
- `encrypted-storage` — EncryptedSharedPreferences for key storage
|
|
212
|
+
- `security` — general Android security patterns
|