Composables

Datastore

Community

rcosteira79

Use 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 per-platform file paths. Triggers on DataStore, Preferences, PreferenceDataStoreFactory, DataStoreFactory, preferencesDataStore, Serializer, SharedPreferences-to-DataStore migration, or persistent settings work.

Overview

Repository
rcosteira79/android-skills
Publisher
rcosteira79
Stars
174
Forks
21
Bundled files
0
License
MIT
Updated
Nov 10, 2026

Installation

tmp=$(mktemp -d)
git clone --depth 1 https://github.com/rcosteira79/android-skills.git "$tmp"
mkdir -p ~/.claude/skills
cp -R "$tmp/plugins/android-skills/skills/datastore" ~/.claude/skills/datastore
rm -rf "$tmp"

Instructions

Jetpack DataStore for Android and KMP

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; MIT. Related: android-skills:android-data-layer, android-skills:kmp-boundaries, android-skills:kotlin-flows.

Preferences vs Typed vs Room

Need Storage
Key-value flags (theme, locale, onboarding done) Preferences DataStore
One typed object, many related fields, schema evolution Typed DataStore + Serializer<T>
Relational data, indexes, WHERE / JOIN, >100 entries, paging Room
Payloads above ~50KB per write Room / filesystem — DataStore rewrites the whole file on every edit

Rule of thumb: if a WHERE clause would be useful, use Room.

The two traps

.catch must match IOException specifically — never a broad catch. A bare catch { emit(emptyPreferences()) } hides serializer/corruption errors behind a silent empty state.

val settings: Flow<UserSettings> = dataStore.data
    .catch { e -> if (e is IOException) emit(emptyPreferences()) else throw e }
    .map { p -> UserSettings(p[Keys.DARK_MODE] ?: false, p[Keys.LOCALE] ?: "en") }

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.

object AppSettingsSerializer : Serializer<AppSettings> {
    override val defaultValue = AppSettings()
    override suspend fun readFrom(input: InputStream): AppSettings =
        try { Json.decodeFromString(input.readBytes().decodeToString()) }
        catch (e: SerializationException) { throw CorruptionException("Cannot read AppSettings", e) }
    override suspend fun writeTo(t: AppSettings, output: OutputStream) =
        output.write(Json.encodeToString(t).encodeToByteArray())
}
val store = DataStoreFactory.create(
    serializer = AppSettingsSerializer,
    corruptionHandler = ReplaceFileCorruptionHandler { AppSettings() },
    produceFile = { File(context.filesDir, "app_settings.json") },
)

KMP factory — per-platform path (not the temp dir)

// commonMain
fun createPreferencesDataStore(producePath: () -> String): DataStore<Preferences> =
    PreferenceDataStoreFactory.createWithPath(produceFile = { producePath().toPath() })
// android: context.filesDir.resolve(PREFS_FILE).absolutePath
// ios:     NSDocumentDirectory via NSFileManager
// jvm:     File(System.getProperty("user.home"), ".myapp") — NOT java.io.tmpdir (the OS may wipe it on reboot)

One DataStore instance per file, held at app scope (a DI singleton) — a second instance on the same file throws IllegalStateException. Never build one inside remember { } or per screen.

(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.)

FAQ

What does Datastore do?

Use 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 per-platform file paths. Triggers on DataStore, Preferences, PreferenceDataStoreFactory, DataStoreFactory, preferencesDataStore, Serializer, SharedPreferences-to-DataStore migration, or persistent settings work.

Who made Datastore?

rcosteira79 (a community member) publishes it in the rcosteira79/android-skills repository on GitHub.

Is Datastore free?

Yes. Datastore is open source, published by rcosteira79 under the MIT license.

What comes with Datastore?

Just its instructions. Everything the agent needs is in one SKILL.md file.

Which agents can use Datastore?

Any coding agent that reads SKILL.md files, including Claude Code, Codex, Cursor, GitHub Copilot and Gemini CLI. The agent loads the skill when a task matches its description.

What are agent skills?

An agent skill is a folder with a SKILL.md file: instructions that teach a coding agent how to do one kind of task, and optionally files it can read while it works. The agent keeps only the skill's name and description in mind and reads the rest when a task calls for it.

How are agent skills different from MCP servers?

An MCP server gives an agent new tools to call, code that runs somewhere and returns results. A skill gives it knowledge: how to approach a task with the tools it already has. Many agents use both.

How can my agent test my Android app?

Robomori lets coding agents create, start and control Android emulators and devices from one command, so they can install your app, take screenshots and check dark mode, font sizes and languages.

You're subscribed

The next lesson lands in on Wednesday.