DataStore for Simple Storage
What is DataStore?
DataStore is Android's modern way to save simple app settings and preferences. It can permently store simple settings like your name, whether dark mode is on, or your favorite font size. Whenever you change a setting, DataStore saves it for you—even if you close and reopen the app. You have persistent data without a database!
DataStore stores its data in files on the device's local filesystem, specifically within your app's private storage directory. For Preferences DataStore, this is typically a `.preferences_pb` file, and for Proto DataStore, it's a `.pb` file. This ensures the data is private to your application and provides transactional safety.
Quick Reference Table
| Type | Description | Common Use |
|---|---|---|
Preferences DataStore |
Key-value storage for simple data | User settings, app preferences, simple flags |
Proto DataStore |
Type-safe storage using Protocol Buffers | Complex data structures, user profiles, app state |
Required Dependencies
To use DataStore, add these to your project:
gradle/libs.versions.toml
datastore = "1.0.0"
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
In app/build.gradle.kts:
dependencies {
implementation(libs.androidx.datastore.preferences)
// ...other dependencies
}
How DataStore Works in This App
When you change a setting (like toggling dark mode or entering your name), here's what happens:
- The UI calls a function in the ViewModel (like
updateUserName()). - The ViewModel calls PreferencesManager, which updates DataStore.
- DataStore saves the new value in the background.
- The UI automatically updates to show the new value, thanks to Kotlin
FlowandStateFlow.
This means your app's settings are always up-to-date and persistent, with very little code!
Understanding Flows (with Analogy)
What is a Flow? Think of a Flow like a live news feed for your app's data. Whenever something changes (like a new setting is saved), the Flow "broadcasts" the update to anyone watching—like your UI. This means your app's screen always shows the latest info, without you having to refresh it manually.
Why use Flows? They make your app reactive: as soon as data changes, your UI updates automatically. This is perfect for settings and preferences!
PreferencesManager.kt: Defining and Using DataStore
When to Use
- When you need to save simple settings that can be stored in a key-value pair.
- When you want changes to be saved instantly and persistently
- When you want your UI to update automatically when data changes
Options Used in this App
| Key | What It Stores | When to Use |
|---|---|---|
| USER_NAME | User's name (String) | Personalize the app |
| DARK_MODE | On/Off (Boolean) | Theme preference |
| FONT_SIZE | Number (Int) | Accessibility, user comfort |
| NOTIFICATIONS | On/Off (Boolean) | App reminders |
Practical Example
object PreferencesKeys {
val USER_NAME = stringPreferencesKey("user_name")
val DARK_MODE = booleanPreferencesKey("dark_mode")
val FONT_SIZE = intPreferencesKey("font_size")
val NOTIFICATIONS = booleanPreferencesKey("notifications")
}
private val Context.dataStore: DataStore by preferencesDataStore(name = "simple_preferences")
class PreferencesManager(private val context: Context) {
// Reading values as Flows
val userName: Flow = context.dataStore.data.map { it[PreferencesKeys.USER_NAME] ?: "Guest" }
val darkMode: Flow = context.dataStore.data.map { it[PreferencesKeys.DARK_MODE] ?: false }
val fontSize: Flow = context.dataStore.data.map { it[PreferencesKeys.FONT_SIZE] ?: 16 }
val notifications: Flow = context.dataStore.data.map { it[PreferencesKeys.NOTIFICATIONS] ?: true }
// Writing values
suspend fun updateUserName(name: String) { context.dataStore.edit { it[PreferencesKeys.USER_NAME] = name } }
suspend fun updateDarkMode(enabled: Boolean) { context.dataStore.edit { it[PreferencesKeys.DARK_MODE] = enabled } }
suspend fun updateFontSize(size: Int) { context.dataStore.edit { it[PreferencesKeys.FONT_SIZE] = size } }
suspend fun updateNotifications(enabled: Boolean) { context.dataStore.edit { it[PreferencesKeys.NOTIFICATIONS] = enabled } }
suspend fun clearAllPreferences() { context.dataStore.edit { it.clear() } }
}
Each key is like a label. The Flow properties let your UI "watch" for changes. The update functions save new values in the background.
DataStoreViewModel.kt: Connecting DataStore to the UI
When to Use
- When you want to keep your UI and data in sync automatically
- When you want to separate business logic from UI code
Methodes Used in this App
| Function | What It Does | When to Use |
|---|---|---|
| updateUserName() | Save a new user name | User changes their name |
| updateDarkMode() | Toggle dark mode | User toggles theme |
| updateFontSize() | Change font size | User picks a new size |
| updateNotifications() | Toggle notifications | User enables/disables reminders |
| clearAllPreferences() | Reset all settings | User wants to start fresh |
Practical Example
class DataStoreViewModel(application: Application) : AndroidViewModel(application) {
private val preferencesManager = PreferencesManager(application)
val userName: StateFlow = preferencesManager.userName.stateIn(viewModelScope, SharingStarted.Lazily, "Guest")
val darkMode: StateFlow = preferencesManager.darkMode.stateIn(viewModelScope, SharingStarted.Lazily, false)
val fontSize: StateFlow = preferencesManager.fontSize.stateIn(viewModelScope, SharingStarted.Lazily, 16)
val notifications: StateFlow = preferencesManager.notifications.stateIn(viewModelScope, SharingStarted.Lazily, true)
fun updateUserName(name: String) { viewModelScope.launch { preferencesManager.updateUserName(name) } }
fun updateDarkMode(enabled: Boolean) { viewModelScope.launch { preferencesManager.updateDarkMode(enabled) } }
fun updateFontSize(size: Int) { viewModelScope.launch { preferencesManager.updateFontSize(size) } }
fun updateNotifications(enabled: Boolean) { viewModelScope.launch { preferencesManager.updateNotifications(enabled) } }
fun clearAllPreferences() { viewModelScope.launch { preferencesManager.clearAllPreferences() } }
}
The ViewModel is like a messenger between your UI and DataStore. It exposes the latest values as StateFlow so your UI always shows the current settings. The update functions are called by the UI when the user makes changes.
DataStoreScreen.kt: The UI for Preferences
When to Use
- When you want to display and update settings in your app
- When you want the UI to update instantly when data changes
Practical Example
@Composable
fun DataStoreScreen(viewModel: DataStoreViewModel) {
val userName by viewModel.userName.collectAsState()
val darkMode by viewModel.darkMode.collectAsState()
val fontSize by viewModel.fontSize.collectAsState()
val notifications by viewModel.notifications.collectAsState()
// ... UI code to display and update preferences ...
}
The UI "watches" the StateFlows from the ViewModel. When the user changes a setting, the UI calls the ViewModel's update functions. The screen updates automatically whenever the data changes—no need to refresh manually!
collectAsState() is a function that lets the UI "watch" for changes. When the data changes, the UI updates automatically. For example, in the code above we have the line val userName by viewModel.userName.collectAsState() which means the UI will watch for changes to the userName StateFlow and update the UI when the data changes.
How this example renders
Above is just a snippet of the code to view the full code, you need to go to my GitHub page and look at the chapter11/DataStoreDemo project.
Here is a screenshot of the app running:
Top and bottom of the application
The app is running on a Pixel 7 Pro with Android 14.
Application after the user has changed the settings (top part only)
Tips for Success
- Use clear, meaningful keys for each preference
- Always provide default values for settings
- Use
FlowandStateFlowto keep your UI in sync - Test your DataStore code by changing settings and restarting the app
- Keep your PreferencesManager focused on simple, small data
Common Mistakes to Avoid
- Forgetting to provide default values (can cause crashes)
- Trying to store large or complex data (use Room for that)
- Not using suspend functions for updates (can block the UI)
- Creating multiple DataStore instances for the same data
- Not observing Flows/StateFlows in the UI
Best Practices
- Use DataStore for simple, persistent settings
- Keep your PreferencesManager and ViewModel code clean and focused
- Use dependency injection for testability (advanced)
- Document your preference keys and their purpose
- Link to the full demo app for reference and exploration