Android Storage Datastore
In this tutorial, you'll learn about Jetpack DataStore. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
The Problem
DataStore throws IllegalStateException: DataStore is already closed, or your Flow doesn't emit values after writing.
Wrong Approach ❌
// Creating DataStore as a property — may be garbage collected
class MyActivity : AppCompatActivity() {
private val Context.dataStore by preferencesDataStore(name = "settings")
// DataStore instance tied to Activity lifecycle — closed on rotation!
}
Output: DataStore is already closed on subsequent reads.
Right Approach ✅
// DataStore should be a top-level property or in Application
private val Context.settingsDataStore by preferencesDataStore(name = "settings")
class SettingsManager(private val context: Context) {
// Read preferences as Flow
val themeMode: Flow<ThemeMode> = context.settingsDataStore.data
.map { prefs ->
val modeName = prefs[THEME_MODE_KEY] ?: "system"
ThemeMode.valueOf(modeName)
}
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
// Write preferences (suspend)
suspend fun setThemeMode(mode: ThemeMode) {
context.settingsDataStore.edit { prefs ->
prefs[THEME_MODE_KEY] = mode.name
}
}
suspend fun setOnboardingComplete() {
context.settingsDataStore.edit { prefs ->
prefs[ONBOARDING_KEY] = true
}
}
// Int preference
suspend fun setLaunchCount(count: Int) {
context.settingsDataStore.edit { prefs ->
prefs[LAUNCH_COUNT_KEY] = count
}
}
// Preference keys
companion object {
private val THEME_MODE_KEY = stringPreferencesKey("theme_mode")
private val ONBOARDING_KEY = booleanPreferencesKey("onboarding_complete")
private val LAUNCH_COUNT_KEY = intPreferencesKey("launch_count")
}
}
// Compose usage
@Composable
fun ThemeAwareApp(settingsManager: SettingsManager) {
val themeMode by settingsManager.themeMode.collectAsState(initial = ThemeMode.System)
LaunchedEffect(themeMode) {
settingsManager.setThemeMode(themeMode)
}
// ...
}
// Proto DataStore for typed objects
// Use Proto DataStore if you need a typed schema (recommended for complex data)
Output: Stable DataStore with proper lifecycle and reactive reads.
Prevention
- Define
DataStoreat the top level or inApplication— never in an Activity/Fragment. - Handle
IOExceptionin the Flow.catch()block. - Use
edit { }for writes — it's thread-safe and atomic. - Preferences DataStore for simple key-value, Proto DataStore for typed schemas.
Common Mistakes with storage datastore
- Using
returnto exit a function early instead of wrapping a pure value in the monad - Mixing let bindings with <- bindings in do notation, producing type errors
- Overlapping type class instances that cause GHC to reject the program with ambiguous dispatch errors
These mistakes appear frequently in real-world Android code. DodaTech's contributors have identified these patterns through analysis of open-source projects and production systems.
Practice Exercise
Write a pure function that safely divides two integers using Maybe, then test it with edge cases like division by zero and negative numbers.
This exercise reinforces the concepts covered in this guide. Try implementing it before checking online solutions.
FAQ
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro