DataStore — small settings
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.
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:
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:
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.
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:
1data class AppSettings(
2 val focusMinutes: Int = 25,
3 val breakMinutes: Int = 5,
4 val keepScreenOn: Boolean = false,
5)Writing
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.
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:
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.
You will add a fourth setting to Focus Flow, end to end.
- Open Pocket Studio, tap Projects, then Focus Flow.
- Tap Editor and open
data/SettingsRepository.kt. - Add a field to
AppSettings:val chimeOn: Boolean = true,. - Add a key inside
Keys:val CHIME = booleanPreferencesKey("chime_on"). - In the
.map { prefs -> ... }block, addchimeOn = prefs[Keys.CHIME] ?: true,. - Add a write function that mirrors
setKeepScreenOn, usingKeys.CHIME. - Open
ui/settings/SettingsViewModel.ktand add a matchingfun setChime(value: Boolean)that launches the repository call. - Open
ui/settings/SettingsScreen.ktand copy theSwitchCard(...)block, changing the title to"Chime"and pointing it at your new function. - 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.
preferencesDataStore(...) 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.SettingsRepository.kt does. One declaration per file name, for the whole app.store.edit { } from ordinary code. Writing to disk suspends, and ordinary functions are not allowed to.suspend and call it from viewModelScope.launch { }. Every repository write function in the course apps follows that shape.prefs[Keys.FOCUS] is nullable, because on a fresh install nothing has been written yet.prefs[Keys.FOCUS] ?: 25. Never use !! here — a brand new install would crash on first launch, which is the worst possible time.implementation(libs.androidx.datastore.preferences) and tap Sync. The preferences package also needs import androidx.datastore.preferences.core.* for the key builders.ReplaceFileCorruptionHandler to preferencesDataStore so a damaged file falls back to defaults instead of crashing.- is for a handful of settings. is for data that grows and needs searching. Choosing wrongly makes both harder.
- Declare
preferencesDataStoreonce, 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 ;
mapit into your own data class and use?:to supply defaults for a fresh install. edit { }is and writes atomically. Put validation likecoerceInnext to the write, not in the screen.- Handle
IOExceptionby 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.