$npx -y skills add rcosteira79/android-skills --skill datastoreUse when persisting key-value preferences or small typed settings on Android or KMP with Jetpack DataStore — Preferences vs Typed (Proto/JSON) vs Room selection, the IOException and corruption-recovery error traps, serializers with corruption handlers, and the KMP factory with pe
| 1 | # Jetpack DataStore for Android and KMP |
| 2 | |
| 3 | Reactive coroutine-based key-value / typed storage; the same `androidx.datastore:datastore-preferences-core` runs on Android, iOS, and JVM — only the file-path producer is platform-specific. (Web support is 1.3.0-alpha only, and it's `sessionStorage`/OPFS-backed there, not file-path-based.) This reference covers the storage-choice call plus the two error/path traps, not the basics of Preferences keys, `edit { }`, or `Serializer<T>`. Adapted from [Meet-Miyani/compose-skill](https://github.com/Meet-Miyani/compose-skill); MIT. **Related:** `android-skills:android-data-layer`, `android-skills:kmp-boundaries`, `android-skills:kotlin-flows`. |
| 4 | |
| 5 | ## Preferences vs Typed vs Room |
| 6 | |
| 7 | | Need | Storage | |
| 8 | |---|---| |
| 9 | | Key-value flags (theme, locale, onboarding done) | Preferences DataStore | |
| 10 | | One typed object, many related fields, schema evolution | Typed DataStore + `Serializer<T>` | |
| 11 | | Relational data, indexes, `WHERE` / `JOIN`, >100 entries, paging | Room | |
| 12 | | Payloads above ~50KB per write | Room / filesystem — DataStore rewrites the **whole file** on every `edit` | |
| 13 | |
| 14 | Rule of thumb: if a `WHERE` clause would be useful, use Room. |
| 15 | |
| 16 | ## The two traps |
| 17 | |
| 18 | **`.catch` must match `IOException` specifically — never a broad `catch`.** A bare `catch { emit(emptyPreferences()) }` swallows `CancellationException` (breaking structured concurrency) and hides serializer/corruption errors behind a silent empty state. |
| 19 | |
| 20 | ```kotlin |
| 21 | val settings: Flow<UserSettings> = dataStore.data |
| 22 | .catch { e -> if (e is IOException) emit(emptyPreferences()) else throw e } |
| 23 | .map { p -> UserSettings(p[Keys.DARK_MODE] ?: false, p[Keys.LOCALE] ?: "en") } |
| 24 | ``` |
| 25 | |
| 26 | **Typed-DataStore corruption recovery triggers on `CorruptionException`, NOT `IOException`.** The serializer's `readFrom` must wrap parse failures in `CorruptionException`, and the store needs a `ReplaceFileCorruptionHandler` — without it, one corrupt file makes every read fail permanently. |
| 27 | |
| 28 | ```kotlin |
| 29 | object AppSettingsSerializer : Serializer<AppSettings> { |
| 30 | override val defaultValue = AppSettings() |
| 31 | override suspend fun readFrom(input: InputStream): AppSettings = |
| 32 | try { Json.decodeFromString(input.readBytes().decodeToString()) } |
| 33 | catch (e: SerializationException) { throw CorruptionException("Cannot read AppSettings", e) } |
| 34 | override suspend fun writeTo(t: AppSettings, output: OutputStream) = |
| 35 | output.write(Json.encodeToString(t).encodeToByteArray()) |
| 36 | } |
| 37 | val store = DataStoreFactory.create( |
| 38 | serializer = AppSettingsSerializer, |
| 39 | corruptionHandler = ReplaceFileCorruptionHandler { AppSettings() }, |
| 40 | produceFile = { File(context.filesDir, "app_settings.json") }, |
| 41 | ) |
| 42 | ``` |
| 43 | |
| 44 | ## KMP factory — per-platform path (not the temp dir) |
| 45 | |
| 46 | ```kotlin |
| 47 | // commonMain |
| 48 | fun createPreferencesDataStore(producePath: () -> String): DataStore<Preferences> = |
| 49 | PreferenceDataStoreFactory.createWithPath(produceFile = { producePath().toPath() }) |
| 50 | // android: context.filesDir.resolve(PREFS_FILE).absolutePath |
| 51 | // ios: NSDocumentDirectory via NSFileManager |
| 52 | // jvm: File(System.getProperty("user.home"), ".myapp") — NOT java.io.tmpdir (the OS may wipe it on reboot) |
| 53 | ``` |
| 54 | |
| 55 | (In a composable, never `runBlocking` on DataStore — it parks the main thread on disk I/O, risking an ANR, and re-runs every recomposition. Expose a `StateFlow` and collect with `collectAsStateWithLifecycle`.) |