DataStore / SharedPreferences 2026 - Part 1 of 2
Introduction
Below, we will look at the standard way to store preference data (or other small amounts). In Part 2 (not yet live), we will move on to how to improve it.
Part 1 of this guide is a good refresher for Android Compose users, especially those that haven't needed to use DataStore recently/yet and perhaps have been using SharedPreferences.
Full code listings are provided below so everything should work without needing any special information. For newer programmers, this should serve as a good introduction and an easy way to get this up and running fast.
I actually found it quite hard to find a full minimal working example of a Repository, ViewModel, and UI online so I am hoping this fills a niche.
Part 2 is aimed at intermediate to advanced users and should provide some useful ideas on how to improve things.
Recommended System
Rather than try to explain everything about DataStore and SharedPreferences (like a thousand other blogs and tutorials), I'll start with a very short summary.
SharedPreferences used to be how we would save small amounts of data to the device. This is generally designed for primitive data types that affect the app as a whole. From around 2020, DataStore become the preferred official recommendation. If you need more information about this topic, check out some of the official links.
In summary, ignore SharedPreferences and use DataStore. If you need to store a good amount of data, look to a proper database like Room.
DataStore Basic Example
Basic Repository
Our data repository is quite basic. We have a counter Flow, a function to update it, and a companion object to hold our constant references.
This is the standard pattern you will see in examples around the web so there shouldn't be any surprises for people used to DataStore.
class DataStoreRepoBasic(private val context: Context) {
val counterFlow: Flow<Int> =
context.dataStore.data.map { preferences ->
preferences[EXAMPLE_COUNTER] ?: 0
}
suspend fun updateCounter(value: Int) {
context.dataStore.edit { preferences ->
preferences[EXAMPLE_COUNTER] = value
}
}
companion object {
private const val USER_PREFERENCES_NAME =
"basic_datastore_preferences"
val Context.dataStore: DataStore<Preferences>
by preferencesDataStore(
name = USER_PREFERENCES_NAME
)
val EXAMPLE_COUNTER = intPreferencesKey(
"example_counter"
)
}
}
Basic ViewModel
Our ViewModel is pretty standard although I do have a unique custom factory getter for it. I'll explain that getter in Part 2 (not yet live) if you are interested, but we can just ignore that for now and know that it works and makes things easier.
The VM provides the repository counter as a StateFlow and also gives us a VM scoped update function to increase the value by one.
class BasicDataStoreViewModel(
private val repo: DataStoreRepoBasic
) : ViewModel() {
val counterState: StateFlow<Int> = repo.counterFlow
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = 0
)
fun incrementCounter() {
viewModelScope.launch {
repo.updateCounter(counterState.value + 1)
}
}
companion object {
private fun factory(repo: DataStoreRepoBasic) =
viewModelFactory {
initializer { BasicDataStoreViewModel(repo) }
}
/**
* IMPORTANT NOTE:
* Tradeoff design - incompatible for automated tests but convenient and easy to use
* Easy to adapt later if needed@Composable
fun get(): BasicDataStoreViewModel {
val context = LocalContext.current
val repo = remember { DataStoreRepoBasic(context) }
return viewModel(factory = factory( repo ))
}
}
}
Plugging this into a UI will give you a way to persist and increase a counter; restarting the app or phone will not remove this value. You can force a reset by clearing the data (via the app information).
You can find both of these classes in the DataStoreRepoBasic.kt GitHub Gist.
Basic UI
As our UI is so simple, I'll put everything directly below:
@Composable
private fun CounterUi(viewModel: BasicDataStoreViewModel) {
val counter by viewModel.counterState.collectAsState()
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "Counter: $counter",
style = MaterialTheme.typography.headlineMedium
)
Button(
onClick = { viewModel.incrementCounter() },
modifier = Modifier.padding(top = 16.dp)
) {
Text("Increment")
}
}
}
@Composable
fun App() {
val viewModel = BasicDataStoreViewModel.get()
Scaffold{
Column(
Modifier.padding(it).padding(8.dp)
) {
ComposableWidgets.TestCard(
"BasicDataStoreUi"
) {
CounterUi(viewModel)
}
}
}
}
Again, there shouldn't really be any surprises here. We are just access the ViewModel and using it to read and increment the counter.
You can get the full code listing in the DataStoreUiBasic GitHub Gist.
Composable Widgets (Shared)
You may have noticed I cheated a little with my TestCard function as you haven't seen this yet; this basically gives us a titled card and aligns everything. This is not necessary to the tutorial, but it makes things look a little nicer. This is used both for part 1 and part 2 of this blog. I wanted to avoid repeating code so I've put that into a separate file.
Click to reveal
object ComposableWidgets {
...
@Composable
fun TestCard(
testName: String,
content: @Composable () -> Unit
) {
Card {
Text(
modifier = Modifier.fillMaxWidth(),
text = testName,
textAlign = TextAlign.Center
)
Column(Modifier.padding(8.dp)) {
content()
}
}
}
}
The code list for this is in the ComposableWidgets GitHub Gist (for parts 1 and 2).
Part 2
In Part 2 (not yet live), we will look at adding another value (a username text string) and what we can do to make the code a little easier to work with.
Hope this first post served as a good introduction / reminder.
Links
Blog / Gists
- DataStoreRepoBasic.kt GitHub Gist
- DataStoreUiBasic GitHub Gist
- ComposableWidgets GitHub Gist (for parts 1 and 2)
- Blog Part 2 - Not yet live
Recommended Resources
Dependencies
build.gradle.kts module:app
Click to reveal
Add the following to your app dependency. I've left the version number details in the datastore-preferences as this is the part you may not have used before. Of course, you should move this into the toml as recommended (and automated) by Android Studio.
dependencies {
//...
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation("androidx.datastore:datastore-preferences:1.2.0")
}
libs.versions.toml
Click to reveal
Add the following to your toml. Note: I'm using lifecylce for all three of these:
[versions]...
lifecycle = "2.10.0"
[libraries]
...
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
Comments
Post a Comment