Pocket Studio Academy
HomePart 44.6

DataStore — small settings

Full course10 min read·4 questions

Not everything deserves a database. For a handful of settings — a length, a toggle, a chosen name — DataStore saves them safely, hands them back as a Flow, and never blocks the screen.

Three numbers do not need a filing cabinet

Focus Flow has exactly three settings: how long a focus round lasts, how long a break lasts, and whether the screen should stay awake.

You could store those in . You would define an entity with one row, a DAO to read that one row, a database to hold that one table, and a migration every time you add a fourth setting. It would work, and it would be ridiculous.

Settings are small, few, and always read all at once. They want a different tool.

Think of it like this

Your kitchen probably has both.

There is a filing cabinet somewhere in the house — insurance, bank letters, the boiler manual. Thousands of pages, organised, searchable, worth the effort of filing properly.

And there is a note stuck to the fridge: bins go out Tuesday, the wifi password, back at six. Four lines. You do not file those. You read all of them in one glance, and rewriting one means rewriting the note.

is the cabinet. is the fridge note. Choosing wrongly in either direction makes life harder than it needs to be.

What DataStore replaced

Android's old answer was , and it had three problems that were never fixable.

It could block the — the first read of a preferences file happens on whatever thread asks, and a slow disk froze the screen. It reported errors by not reporting them: a failed write returned false, or nothing at all, and most code never checked. And it gave you values one at a time, so you had to ask again every time you wanted to know if something had changed.

DataStore fixes all three. Writes are functions, so they cannot block. Failures throw. And reading gives you a that keeps delivering.

Declaring the store

One line, at the top level of a file — not inside a class:

data/SettingsRepository.ktkotlin
1private val Context.store: DataStore<Preferences> by
2  preferencesDataStore(name = "settings")

This adds a property called store onto every Context in the file. The by preferencesDataStore(...) part means it is created once, the first time anything reads it, and reused forever afterwards.

That "once" is not a nicety. Two DataStores open on the same file is an error, and declaring it as a top-level property is the mechanism that makes it impossible.

Typed keys

Every setting gets a key, and the key carries the type:

data/SettingsRepository.ktkotlin
1private object Keys {
2  val FOCUS = intPreferencesKey("focus_minutes")
3  val BREAK = intPreferencesKey("break_minutes")
4  val KEEP_ON =
5    booleanPreferencesKey("keep_screen_on")
6}

intPreferencesKey produces a key that only ever reads and writes an Int. Reading Keys.FOCUS as a Boolean is not a runtime surprise; it is a compiler error. There are matching functions for String, Long, Float, Double and Set<String> — and that is the complete list of what can hold.

Note

There is a second flavour, Proto DataStore, which stores any shape you like by way of a schema file. It is genuinely better for complex settings — and it adds a code generator and a build plugin.

For three values on a phone-built project, that trade is not worth it. This course uses Preferences DataStore everywhere, and says so rather than pretending the other one does not exist.

Reading: a Flow of your own type

The raw flow gives you a bag of key-value pairs. Nobody wants to work with that, so the repository turns each bag into a proper Kotlin object:

The type it produces is an ordinary data class with the defaults written twice — once here, once in the class — which is the one small ugliness of this pattern:

data/SettingsRepository.ktkotlin
1data class AppSettings(
2  val focusMinutes: Int = 25,
3  val breakMinutes: Int = 5,
4  val keepScreenOn: Boolean = false,
5)

Writing

data/SettingsRepository.ktkotlin
1suspend fun setFocusMinutes(value: Int) {
2  store.edit { prefs ->
3    prefs[Keys.FOCUS] =
4      value.coerceIn(FOCUS_RANGE)
5  }
6}

edit is suspend, takes a block, and writes the whole file atomically when the block finishes — so a crash halfway through cannot leave half a setting behind.

coerceIn(FOCUS_RANGE) is the interesting line. FOCUS_RANGE is 5..90. The rule about what counts as a sensible focus length lives here, next to the saving, not in the screen. Any future screen — a widget, a voice command, a restored backup — gets the same protection for free.

9:41▲ ▮
Settings
Focus length
One round of deep work
25
+
Break length
The breather in between
5
+
Keep screen on
Stop the display sleeping while the timer runs
Changes save the moment you make them, and survive a restart.

Timer

Stats

Settings

Focus Flow's settings, in dark mode. Every change writes to DataStore immediately; there is no Save button because there is nothing to lose.

Getting it to the screen

Nothing new — this is Lesson 4.3, unchanged:

ui/settings/SettingsViewModel.ktkotlin
1val settings: StateFlow<AppSettings> =
2  repo.settings.stateIn(
3    scope = viewModelScope,
4    started = SharingStarted
5      .WhileSubscribed(5_000L),
6    initialValue = AppSettings(),
7  )

initialValue = AppSettings() is why the settings screen never flashes blank on open: the defaults are drawn instantly, and the saved values replace them a few milliseconds later.

The same flow also reaches the timer, which watches it and adjusts its lengths. One store, two screens, no message passing.

Try it in Pocket Studio

You will add a fourth setting to Focus Flow, end to end.

  1. Open Pocket Studio, tap Projects, then Focus Flow.
  2. Tap Editor and open data/SettingsRepository.kt.
  3. Add a field to AppSettings: val chimeOn: Boolean = true,.
  4. Add a key inside Keys: val CHIME = booleanPreferencesKey("chime_on").
  5. In the .map { prefs -> ... } block, add chimeOn = prefs[Keys.CHIME] ?: true,.
  6. Add a write function that mirrors setKeepScreenOn, using Keys.CHIME.
  7. Open ui/settings/SettingsViewModel.kt and add a matching fun setChime(value: Boolean) that launches the repository call.
  8. Open ui/settings/SettingsScreen.kt and copy the SwitchCard(...) block, changing the title to "Chime" and pointing it at your new function.
  9. Tap Run. Toggle it off, close the app completely from the recents view, and reopen it. Still off. Three values are now four, and no migration was required.
Error Doctor5 common errors
java.lang.IllegalStateException: There are multiple DataStores active for the same file: /data/user/0/com.nativeworks.focusflow/files/datastore/settings.preferences_pb. You should either maintain your DataStore as a singleton or confirm that there is no two DataStore's active on the same file
MeanspreferencesDataStore(...) was called more than once for the same name — usually because it was written inside a class, so every new instance of that class opened the file again.
FixMove the declaration to the top level of the file, outside every class, exactly as SettingsRepository.kt does. One declaration per file name, for the whole app.
e: Suspend function 'edit' should be called only from a coroutine or another suspend function
MeansYou called store.edit { } from ordinary code. Writing to disk suspends, and ordinary functions are not allowed to.
FixMark your own function suspend and call it from viewModelScope.launch { }. Every repository write function in the course apps follows that shape.
e: Type mismatch: inferred type is Int? but Int was expected
Meansprefs[Keys.FOCUS] is nullable, because on a fresh install nothing has been written yet.
FixSupply a default with the elvis operator: prefs[Keys.FOCUS] ?: 25. Never use !! here — a brand new install would crash on first launch, which is the worst possible time.
e: Unresolved reference: preferencesDataStore
MeansThe DataStore dependency is missing, or only the core artifact was added rather than the preferences one.
FixAdd implementation(libs.androidx.datastore.preferences) and tap Sync. The preferences package also needs import androidx.datastore.preferences.core.* for the key builders.
androidx.datastore.core.CorruptionException: Unable to parse preferences proto.
MeansThe settings file on the device is damaged — most often because a build was interrupted mid-write, or the file was written by an incompatible earlier version of the app.
FixDuring development, uninstall the app to clear its files. In a shipped app, pass a ReplaceFileCorruptionHandler to preferencesDataStore so a damaged file falls back to defaults instead of crashing.
Recap
  • is for a handful of settings. is for data that grows and needs searching. Choosing wrongly makes both harder.
  • Declare preferencesDataStore once, at the top level of a file. Two stores on one file is a crash.
  • Keys carry their type: intPreferencesKey, booleanPreferencesKey, and so on.
  • Reading gives a ; map it into your own data class and use ?: to supply defaults for a fresh install.
  • edit { } is and writes atomically. Put validation like coerceIn next to the write, not in the screen.
  • Handle IOException by falling back to empty preferences, and rethrow everything else.
  • Next: both Room and DataStore are hidden behind a repository in the course apps. That is not decoration — the next lesson explains what it buys.