CPS251 Android Development by Scott Shaper

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 Flow and StateFlow.

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

KeyWhat It StoresWhen to Use
USER_NAMEUser's name (String)Personalize the app
DARK_MODEOn/Off (Boolean)Theme preference
FONT_SIZENumber (Int)Accessibility, user comfort
NOTIFICATIONSOn/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

FunctionWhat It DoesWhen to Use
updateUserName()Save a new user nameUser changes their name
updateDarkMode()Toggle dark modeUser toggles theme
updateFontSize()Change font sizeUser picks a new size
updateNotifications()Toggle notificationsUser enables/disables reminders
clearAllPreferences()Reset all settingsUser 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

DataStore Demo App Screenshot DataStore Demo App Screenshot

The app is running on a Pixel 7 Pro with Android 14.

Application after the user has changed the settings (top part only)

DataStore Demo App Screenshot

Tips for Success

  • Use clear, meaningful keys for each preference
  • Always provide default values for settings
  • Use Flow and StateFlow to 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