On this page
"Where do I save this?" comes up constantly. A theme preference, a list of 5,000 transactions, the last-opened tab, an onboarding flag, cached articles. Using a database for one boolean is overkill. Using key-value storage for thousands of records is a mistake. Here's how to choose.
Quick answer
| Data | Use |
|---|---|
| Settings and flags (theme, onboarding done, last tab) | Preferences DataStore |
| A small typed settings object with schema | Proto DataStore (or DataStore with a serializer) |
| Collections, records, anything you query, sort or relate | Room |
| Large binary files (images, PDFs) | Files in app storage, with paths in Room |
| Secrets (tokens, keys) | Encrypted storage backed by the Android Keystore. See Secure Token Storage |
DataStore
DataStore replaces SharedPreferences. It's asynchronous (Flow and suspend), consistent (transactional updates), and safe to call from the UI thread because I/O happens off the main thread. It's designed for small datasets, and every update rewrites the whole file.
private val Context.settingsStore by preferencesDataStore(name = "settings") // one instance per file
enum class ThemeMode { SYSTEM, LIGHT, DARK }
data class Settings(val theme: ThemeMode, val onboardingDone: Boolean)
class SettingsRepository(private val context: Context) {
private object Keys {
val THEME = stringPreferencesKey("theme")
val ONBOARDED = booleanPreferencesKey("onboarding_done")
}
val settings: Flow<Settings> = context.settingsStore.data
.catch { e -> if (e is IOException) emit(emptyPreferences()) else throw e } // corrupted/unreadable file
.map { p ->
Settings(
theme = p[Keys.THEME]?.let { runCatching { ThemeMode.valueOf(it) }.getOrNull() } ?: ThemeMode.SYSTEM,
onboardingDone = p[Keys.ONBOARDED] ?: false,
)
}
suspend fun setTheme(mode: ThemeMode) = context.settingsStore.edit { it[Keys.THEME] = mode.name }
suspend fun completeOnboarding() = context.settingsStore.edit { it[Keys.ONBOARDED] = true }
}Room
Room is a full SQLite database: indexed queries, relations, partial updates, paging, and observation per table. Use it whenever data is a collection, or you need to filter or sort. See the Room tutorial.
Side-by-side
| Aspect | DataStore | Room |
|---|---|---|
| Data model | Key-value or one typed object | Tables, rows, relations |
| Queries | None (read everything, map in Kotlin) | SQL with compile-time checks |
| Partial updates | No: rewrites the file | Yes |
| Size | Small (settings) | Large datasets |
| Observation | Flow of whole data | Flow per query |
| Migrations | Key or schema evolution; SharedPreferences migration helper | Versioned SQL migrations |
Migrating from SharedPreferences
private val Context.settingsStore by preferencesDataStore(
name = "settings",
produceMigrations = { context -> listOf(SharedPreferencesMigration(context, "legacy_prefs")) },
)Examples
Notes app
Room for notes and folders, DataStore for sort order and theme.
Shopping app
Room for cached catalogue and cart, DataStore for recent search terms (small, capped list) and preferences.
Fitness app
Room for workout history (queried by date range), DataStore for units (km or miles) and goals.
Common mistakes
- Storing large lists as JSON strings in DataStore or SharedPreferences.
- Creating multiple DataStore instances for the same file, which throws an error at runtime.
- Blocking reads with
runBlocking { dataStore.data.first() }on the main thread. - Putting secrets in plain DataStore.
Frequently asked questions
Is SharedPreferences deprecated?
It isn't removed, but Google recommends DataStore for new code because SharedPreferences has synchronous APIs that can block the UI thread and weaker consistency guarantees.
Should I use Proto DataStore?
Use it when you want a schema and type safety for a settings object. Preferences DataStore is simpler for a handful of keys.
Can DataStore be used in Kotlin Multiplatform?
Yes. DataStore supports KMP targets in recent versions.
Conclusion
DataStore for small settings and flags, Room for collections you query, files for large binaries, and Keystore-backed encryption for secrets. Most production apps use more than one, each for what it does best.
Next step: combine local and remote data in Offline-First Android App Architecture.
Official references
๐ฑ Apps built by Yunok Tech
Real Android apps on Google Play, free to download.
Walzi4K wallpapers for lock and home screenGet it โธ
NotiVaultNotification history: recover dismissed notificationsGet it โธ
Ready to build real Android applications?
Networking, persistence and offline sync are where real apps get complicated. Practise building a data layer that handles slow networks, failures and caching, not just the happy path.
Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.
Explore the Android course and take your next step as an Android developer.