Save Settings in Android with DataStore (Dark Mode Switch)
Use Preferences DataStore to save small settings like dark mode and the user's name, and make your whole Compose app follow them.
In Part 15 we saved a whole shopping list on the phone with Room. But what about tiny things, like "Is dark mode on?" or "What is the user's name?" Creating a table for that is too much. Today we use DataStore to save settings in Android. By the end, you will have a dark mode switch and a "Hello, Nadia!" greeting that are still there after you restart the app. ๐
Room or DataStore? Which One When?
Think about your home:
- Room is like a big cupboard with many shelves. Great for lists of things: shopping items, notes, messages.
- DataStore is like a small notebook by the door. You write a few short notes: "Dark mode: ON", "Name: Nadia". Great for small settings.
DataStore saves data as key/value pairs. A key is a label, and the value is what you save under that label:
| Key | Value |
|---|---|
dark_mode | true |
user_name | "Nadia" |
Tip: Old Android tutorials use SharedPreferences for this. That is the old way. It can block the screen while reading the disk. DataStore is the modern replacement from Google. It uses coroutines and Flow, just like Room.
Step 1: Add the DataStore Dependency
Open app/build.gradle.kts and add this to the dependencies { } block:
dependencies {
// ...your other dependencies
implementation("androidx.datastore:datastore-preferences:1.2.1")
}
Click Sync Now.
Tip:
1.2.1is the latest stable version right now. Android Studio may suggest a newer one later. Use stable versions, not ones with "alpha" in the name.
Step 2: Create the Settings Repository
A repository is a small helper class that hides the details of where data is saved. The screen just says "save dark mode" and doesn't care how.
Create a new file SettingsRepository.kt:
package com.example.myfirstapp
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
// One DataStore for the whole app. Keep this at the top level of the file.
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
class SettingsRepository(private val context: Context) {
private object Keys {
val DARK_MODE = booleanPreferencesKey("dark_mode")
val USER_NAME = stringPreferencesKey("user_name")
}
val darkMode: Flow<Boolean> = context.dataStore.data
.map { prefs -> prefs[Keys.DARK_MODE] ?: false }
val userName: Flow<String> = context.dataStore.data
.map { prefs -> prefs[Keys.USER_NAME] ?: "" }
suspend fun setDarkMode(enabled: Boolean) {
context.dataStore.edit { prefs ->
prefs[Keys.DARK_MODE] = enabled
}
}
suspend fun setUserName(name: String) {
context.dataStore.edit { prefs ->
prefs[Keys.USER_NAME] = name
}
}
}
Let's understand it:
val Context.dataStore by preferencesDataStore(name = "settings")creates our notebook. The file on the phone is calledsettings. It is written outside the class, at the top of the file, so there is only ever one DataStore.booleanPreferencesKey("dark_mode")makes a key for a true/false value.stringPreferencesKey("user_name")makes a key for text. There are alsointPreferencesKey,floatPreferencesKeyand more.dataStore.datais aFlow. Like Room, it sends a new value every time a setting changes.prefs[Keys.DARK_MODE] ?: falsemeans "read dark mode; if nothing is saved yet, usefalse".edit { }is how you write. Everything inside the block is saved together, safely, in the background.setDarkModeissuspendbecause saving to disk takes a moment.
Step 3: Create the Settings ViewModel
Like in Part 15, we use AndroidViewModel to get a Context easily. Create SettingsViewModel.kt:
package com.example.myfirstapp
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class SettingsViewModel(application: Application) : AndroidViewModel(application) {
private val repository = SettingsRepository(application)
val darkMode: StateFlow<Boolean> = repository.darkMode
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
val userName: StateFlow<String> = repository.userName
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "")
fun setDarkMode(enabled: Boolean) {
viewModelScope.launch { repository.setDarkMode(enabled) }
}
fun setUserName(name: String) {
viewModelScope.launch { repository.setUserName(name.trim()) }
}
}
Let's understand it:
stateIn(...)turns the Flow into aStateFlowthe screen can read. You saw this in Part 9 and Part 15.viewModelScope.launch { }runs thesuspendsave functions in the background.- In bigger apps, the repository is given to the ViewModel with dependency injection (Hilt). For now, this simple way is perfect.
Step 4: Build the Settings Screen
Create SettingsScreen.kt:
package com.example.myfirstapp
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@Composable
fun SettingsScreen(
viewModel: SettingsViewModel,
modifier: Modifier = Modifier
) {
val darkMode by viewModel.darkMode.collectAsStateWithLifecycle()
val savedName by viewModel.userName.collectAsStateWithLifecycle()
var nameInput by rememberSaveable { mutableStateOf("") }
Column(modifier = modifier.fillMaxSize().padding(16.dp)) {
Text(
text = if (savedName.isBlank()) "Hello!" else "Hello, $savedName!",
style = MaterialTheme.typography.headlineMedium
)
Spacer(Modifier.height(24.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text("Dark mode", modifier = Modifier.weight(1f))
Switch(
checked = darkMode,
onCheckedChange = { viewModel.setDarkMode(it) }
)
}
Spacer(Modifier.height(24.dp))
OutlinedTextField(
value = nameInput,
onValueChange = { nameInput = it },
label = { Text("Your name") },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(8.dp))
Button(onClick = {
viewModel.setUserName(nameInput)
nameInput = ""
}) {
Text("Save name")
}
}
}
Let's understand it:
- The
Switchshows the saveddarkModevalue. When you flip it, we callsetDarkMode. DataStore saves it, the Flow sends the new value, and the Switch moves. One clean circle. - The greeting reads the saved name, not the text field. So "Hello, Nadia!" only changes after you tap Save name.
nameInputis just the text being typed. It is not saved until you tap the button.
Use Alt+Enter to add imports. Watch out for:
androidx.datastore.preferences.preferencesDataStoreandroidx.datastore.preferences.core.edit(easy to miss,editturns red otherwise)androidx.lifecycle.compose.collectAsStateWithLifecycleandroidx.compose.material3.Switch
Step 5: Make the Whole App Follow the Setting
Right now the Switch saves a value, but the app colors don't change yet. Let's fix that.
Open ui/theme/Theme.kt. The Empty Activity template has a theme function like this:
@Composable
fun MyFirstAppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) { ... }
See darkTheme: Boolean = isSystemInDarkTheme()? By default, the app follows the phone's setting. We can pass our own value instead. (We looked at this file in Part 11.)
Now update MainActivity.kt:
package com.example.myfirstapp
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.myfirstapp.ui.theme.MyFirstAppTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
val settingsViewModel: SettingsViewModel = viewModel()
val darkMode by settingsViewModel.darkMode.collectAsStateWithLifecycle()
MyFirstAppTheme(darkTheme = darkMode) {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
SettingsScreen(
viewModel = settingsViewModel,
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
Let's understand it:
- We create the
SettingsViewModelabove the theme, so the theme can readdarkMode. MyFirstAppTheme(darkTheme = darkMode)tells the whole app: "Use dark colors if the saved setting says so."- We pass the same ViewModel to
SettingsScreen, so the Switch and the theme always agree.
In your real app, add Settings as a new tab in the bottom bar from Part 10, or as a "settings" route in your NavHost from Part 8. Just keep the MyFirstAppTheme(darkTheme = darkMode) part around everything.
Now run the app:
- Type "Nadia" and tap Save name. You see "Hello, Nadia!".
- Flip the dark mode Switch. The whole app turns dark.
- Close the app completely and open it again.
Your name and dark mode are still there! ๐
Tip: When the app opens, you may see light colors for a split second before dark mode loads. That is because DataStore reads the disk in the background and our starting value is
false. For a beginner app, this is fine. Professional apps keep the splash screen visible until settings are loaded.
Common Errors
"There are multiple DataStores active for the same file" โ You created preferencesDataStore(name = "settings") in more than one place, or inside a class or function. Keep exactly one val Context.dataStore by preferencesDataStore(...) at the top level of one file.
edit is red / "Unresolved reference: edit" โ Add the import androidx.datastore.preferences.core.edit with Alt+Enter.
The Switch flips but the colors don't change โ You probably still have MyFirstAppTheme { } without darkTheme = darkMode. Also make sure you use the same ViewModel for the theme and the screen.
Colors look strange even in dark mode โ On Android 12+, dynamicColor = true uses colors from the wallpaper. It still respects darkTheme, so this is normal. Set dynamicColor = false if you want your own colors.
Try It Yourself
- System / Light / Dark choice. Instead of a true/false Switch, save a string with
stringPreferencesKey("theme_mode"), using"system","light"or"dark". Show threeRadioButtons. In MainActivity, useval dark = when (mode) { "light" -> false; "dark" -> true; else -> isSystemInDarkTheme() }. - Add a "Reset settings" button. Hint: inside
edit { prefs -> prefs.clear() }. - Show the saved name on the Shopping List screen from Part 15: "Nadia's Shopping List".
Frequently Asked Questions
Can I save a whole list in DataStore? You can, but you shouldn't. DataStore is for small settings. Lists that grow (notes, items, messages) belong in Room.
Is DataStore safe for passwords or tokens? No. DataStore is not encrypted. Never save passwords there. Use Android's security tools (like the Keystore) for secret data.
Should I still learn SharedPreferences? Just know it exists, because you will see it in old code. For new apps, Google recommends DataStore.
Quick Recap
- Room is for lists of data. DataStore Preferences is for small settings.
- Add
androidx.datastore:datastore-preferences:1.2.1. - Create one DataStore with
val Context.dataStore by preferencesDataStore(name = "settings"). - Make keys with
booleanPreferencesKeyandstringPreferencesKey. - Read with
dataStore.data(a Flow), write withedit { }. - Pass the saved value to
MyFirstAppTheme(darkTheme = ...)so the whole app follows it. - SharedPreferences is the old way.
Next, when something goes wrong, you need to find out why. Let's learn to debug.
Part 17: Debug Your Android App: Logcat, Breakpoints and Crashes