---
name: android-datastore
description: "Typed Jetpack DataStore as the modern SharedPreferences replacement: Preferences and typed stores, serializer-layer encryption, and migration. Use when persisting settings, session or tokens, encrypting key-value state, or migrating off EncryptedSharedPreferences."
metadata:
  risk: safe
  source: multi-agent-pipeline
  date_added: "2026-09-21"
---

# Android DataStore

Jetpack DataStore is the current replacement for `SharedPreferences`: an
async, transactional, coroutine- and Flow-based key-value or typed store that
never blocks the UI thread and surfaces read errors instead of swallowing them.
Targets DataStore 1.1+ with Kotlin coroutines.

Full copy-ready code for every section lives in
[`references/patterns.md`](references/patterns.md), under headings that mirror
these sections. Read a section here for the decision, load the matching
reference section when writing the implementation.

## Contents

- [Choosing a Store](#choosing-a-store)
- [Preferences DataStore](#preferences-datastore)
- [Typed DataStore with a Serializer](#typed-datastore-with-a-serializer)
- [Encrypted Typed DataStore](#encrypted-typed-datastore)
- [Migrating from SharedPreferences](#migrating-from-sharedpreferences)
- [Versioned Data Migrations](#versioned-data-migrations)
- [Reading and Writing as Flow](#reading-and-writing-as-flow)
- [Testing](#testing)
- [Do's and Don'ts](#dos-and-donts)
- [Troubleshooting](#troubleshooting)
- [Review Checklist](#review-checklist)

## Choosing a Store

Two flavors share one engine. Pick by whether the shape of the data is fixed.

| Need | Store | Why |
|------|-------|-----|
| A handful of loose keys, types known only at the call site | Preferences DataStore | No schema, `Preferences.Key<T>` accessors, quickest to adopt |
| A fixed model with named fields, validation, defaults | Typed DataStore | One `Serializer`, compile-time-safe reads, atomic whole-object writes |
| Small sensitive state (tokens, flags tied to a session) | Typed DataStore + encrypting `Serializer` | Encryption sits in the serialization layer, transparent to callers |
| Large sensitive or relational data | Not DataStore | Use `room-database` (encrypted relational storage) |
| Cryptographic keys themselves | Not DataStore | Use `android-security` (Keystore key hierarchy) |

Preferences DataStore trades the schema for flexibility: no type safety, no
defaults, no validation. Reach for the typed flavor as soon as the data has a
stable shape. "Proto DataStore" is the same typed mechanism using a protobuf
`Serializer`; nothing here requires protobuf specifically, any serialization
(a hand-written `Serializer`, kotlinx-serialization JSON, protobuf) works
behind the `Serializer<T>` interface.

Declare each store once per process as a top-level property so a single
instance owns the file; two instances over one file corrupt it.

## Preferences DataStore

Create the store with the `preferencesDataStore` delegate, define typed keys
with `stringPreferencesKey`, `intPreferencesKey`, `booleanPreferencesKey`, and
read `data` as a `Flow<Preferences>`, mapping each key with a default. Write
inside `edit { }`, a transactional block.

See [references/patterns.md#preferences-datastore](references/patterns.md#preferences-datastore)
for the store declaration, a typed read Flow, and a write.

## Typed DataStore with a Serializer

A typed store needs a `Serializer<T>` supplying a `defaultValue` and
`readFrom` / `writeTo` over streams. Model the data as an immutable type
(a `data class` or a protobuf message), then create the store with the
`dataStore` delegate passing `fileName` and `serializer`.

`readFrom` failure must throw `CorruptionException`, which the optional
`corruptionHandler` (a `ReplaceFileCorruptionHandler`) turns into a recovery
value instead of crashing the app.

See [references/patterns.md#typed-datastore-with-a-serializer](references/patterns.md#typed-datastore-with-a-serializer)
for a `data class` model, a JSON `Serializer`, and the store declaration with a
corruption handler.

## Encrypted Typed DataStore

Encrypt at the `Serializer` layer: the store persists ciphertext, callers see
the plain model, and encryption is invisible to the rest of the app. A generic
`createEncryptedDataStore(name, serializer)` factory wraps any inner
`Serializer<T>` in an encrypting one so a single implementation serves every
model.

Contract of the encrypting `Serializer`:

- **Encrypt per write.** Serialize with the inner serializer, encrypt the
  bytes with an AES/GCM key from the Keystore.
- **Prepend the IV to the ciphertext.** GCM needs a fresh random IV per
  encryption; write `IV || ciphertext` so `readFrom` can split the IV back off.
  Never reuse an IV with the same key.
- **Fail safe on any read error.** A decryption failure, a truncated file, a
  key rotation, a format change: `readFrom` returns the inner serializer's
  `defaultValue` rather than throwing. Corrupt or unreadable encrypted state
  degrades to "empty", it never crashes and never leaks a partial plaintext.
- **Debug-only plaintext escape hatch (optional).** Guarded by
  `BuildConfig.DEBUG`, write plaintext instead of ciphertext so the file is
  inspectable on a developer build. Release builds always encrypt; the branch
  is compiled out by R8.

The AES/GCM Keystore key belongs to `android-security` (key hierarchy,
envelope encryption, `KeyPermanentlyInvalidatedException` handling); this
factory only consumes a `Cipher` from it. `EncryptedSharedPreferences` and the
`androidx.security-crypto` `MasterKey` it depends on are deprecated; a typed
DataStore with an encrypting `Serializer` is the current-best-practice
replacement and keeps encryption and typing in one place.

See [references/patterns.md#encrypted-typed-datastore](references/patterns.md#encrypted-typed-datastore)
for the full `EncryptingSerializer<T>` (IV split/prepend, fail-safe read,
debug plaintext branch) and the `createEncryptedDataStore` factory.

## Migrating from SharedPreferences

Pass a `SharedPreferencesMigration` in the store's `produceMigrations`. On
first read after upgrade, DataStore copies the named keys out of the legacy
`SharedPreferences` into the store, then deletes the migrated keys so the copy
runs exactly once. List the keys explicitly to migrate a subset; omit the set
to migrate all keys.

For a typed store, the migration maps the flat `SharedPreferences` values onto
the model's fields inside the migration lambda.

See [references/patterns.md#migrating-from-sharedpreferences](references/patterns.md#migrating-from-sharedpreferences)
for a Preferences-store migration and a typed-store migration.

## Versioned Data Migrations

Carry a `version` (or `schemaVersion`) field in the typed model and register a
`DataMigration<T>` whose `shouldMigrate` compares the stored version, whose
`migrate` returns the upgraded value, and whose `cleanUp` releases any
transient resources. Chain one migration per version step so an old install
walks forward one version at a time. Migrations run in order before the first
`data` emission.

See [references/patterns.md#versioned-data-migrations](references/patterns.md#versioned-data-migrations)
for a versioned model and a stepwise `DataMigration`.

## Reading and Writing as Flow

`data` is a cold `Flow<T>` that re-emits the whole value on every write.

- **Observe** by collecting `data`; map to the field you need and add
  `distinctUntilChanged()` to drop no-op re-emissions.
- **Read once** with `data.first()` inside a coroutine.
- **Handle IO errors in the stream**: catch `IOException` from `data` with
  `.catch { }` and emit the default; let other exceptions propagate.
- **Write** with `updateData { current -> ... }` (typed) or `edit { }`
  (Preferences). Both suspend, run on the DataStore dispatcher, and are atomic:
  the transform receives the current value and returns the next, so
  read-modify-write races cannot interleave. Never mutate and re-store from a
  separately read snapshot; always transform inside the block.

Do the work in `viewModelScope` / an injected scope with structured
concurrency; DataStore confines its own IO, so no manual `Dispatchers.IO`
wrapping is needed around `updateData` or `edit`.

See [references/patterns.md#reading-and-writing-as-flow](references/patterns.md#reading-and-writing-as-flow)
for an observed Flow with error handling, a one-shot read, and an atomic update.

## Testing

Point the store at a `TemporaryFolder` file and drive it with a `TestScope`.

- Build the store with `DataStoreFactory.create(...)` (or the typed factory)
  and a `scope` of `TestScope(...) + Job()`, backed by a file under a JUnit
  `TemporaryFolder` so each test is isolated and self-cleaning.
- Drive time with `runTest`; assert on `data.first()` after an `updateData`.
- For the encrypting serializer, inject a fake/in-memory `Cipher` provider so
  the test does not touch the real Keystore, and assert a corrupt file yields
  the default (fail-safe), not an exception.
- No `Thread.sleep`; `runTest` advances the virtual clock. No shared global
  store between tests; one file per test.

See [references/patterns.md#testing](references/patterns.md#testing)
for a `TemporaryFolder`-backed store, a `runTest` round-trip, and a fail-safe
corruption test.

## Do's and Don'ts

### Do's
- Declare one DataStore instance per file, as a top-level property
- Prefer a typed DataStore once the data has a stable shape
- Encrypt in the `Serializer` layer so callers stay plaintext-only
- Prepend a fresh random IV to every GCM ciphertext
- Return the default value on any encrypted-read failure (fail safe)
- Read/write through `data`, `updateData`, and `edit` only
- Transform inside `updateData { }` / `edit { }`, never mutate a snapshot
- Add `distinctUntilChanged()` when observing a single field
- Migrate legacy keys with `SharedPreferencesMigration`
- Version the typed model and migrate stepwise
- Back tests with a `TemporaryFolder` file and a `TestScope`

### Don'ts
- Do not create two DataStore instances over one file
- Do not use `runBlocking` to read on the main thread
- Do not reuse an IV with the same AES/GCM key
- Do not throw from an encrypted `readFrom`; degrade to the default
- Do not write plaintext in release builds (gate the branch on `BuildConfig.DEBUG`)
- Do not store cryptographic keys in DataStore (Keystore owns those)
- Do not put large or relational data in DataStore (use Room)
- Do not wrap `updateData`/`edit` in `Dispatchers.IO`; DataStore confines IO
- Do not read a snapshot, mutate, and store it back (race)
- Do not use `EncryptedSharedPreferences`; it is deprecated

## Troubleshooting

| Problem | Cause | Fix |
|---------|-------|-----|
| `IllegalStateException: multiple DataStores active for the same file` | Two instances over one file | Keep a single top-level instance |
| `CorruptionException` on first read | Unreadable file / schema change | Add a `corruptionHandler` (`ReplaceFileCorruptionHandler`) or fail safe to default |
| Decrypt fails after re-enroll / OS update | Keystore key invalidated | Catch in `readFrom`, return default; recreate the key (see android-security) |
| `data` never emits | Collected off a cancelled scope, or a migration threw | Collect in a live scope; check migration `shouldMigrate`/`migrate` |
| SharedPreferences values not appearing | Keys not listed, or already migrated once | List the keys; migration copies each key only once |
| Stale value after write | Read from a cached snapshot, not `data` | Always observe `data`; write via `updateData` |
| `GeneralSecurityException` / `AEADBadTagException` on read | IV/ciphertext split wrong or tampered file | Verify `IV || ciphertext` framing; on failure return default |
| Flaky test, wrong value | Shared store or real dispatcher | One `TemporaryFolder` file per test; `TestScope` + `runTest` |

## Review Checklist

- [ ] One DataStore instance per file, declared top-level
- [ ] Typed DataStore used where the data shape is stable
- [ ] Encryption lives in the `Serializer`, callers see plaintext
- [ ] Fresh random IV per write, prepended to the ciphertext
- [ ] Encrypted `readFrom` fails safe to the default value, never throws
- [ ] Any debug plaintext path is gated on `BuildConfig.DEBUG`
- [ ] Cryptographic keys come from the Keystore, not DataStore (android-security)
- [ ] Large / relational data uses Room, not DataStore (room-database)
- [ ] `SharedPreferencesMigration` covers the legacy keys
- [ ] Typed model is versioned with stepwise `DataMigration`s
- [ ] Reads/writes go through `data`, `updateData`, `edit`; no cached snapshots
- [ ] Tests use a `TemporaryFolder` file and a `TestScope`, no `sleep`
