DataStore / SharedPreferences 2026 - Part 2 of 2
DataStore / SharedPreferences (Part 2 of 2)
Introduction
In Part 1, we covered the basic implementation of a textbook DataStore and how we should no longer be using SharedPreferences. If you haven't had the chance to read that yet, I do recommend taking a quick look. It is a quick read for experienced Compose users.
In this, we will focus on improving our code to make it easier to manage, more pleasurable to work with, and more scalable (using DRY).
As this is aimed toward more experienced programmers, we can also add a bit more detail about some of my solutions. This will also be quite dense.
Again, I'm using objects to wrap things. Read this if you want to know why I wrap examples in objects.
Note: For this blog, I will not group ViewModel actions together into a sealed interface (Action Sealed Class / UI Event Sealed Class) as I don't want to introduce new concepts to people who are not already familiar with them. If you are familiar with this pattern, I would recommend implementing that yourself.
Original Code Smells
The Repository
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
}
}
The 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)
}
}
A Slightly More Complex Example
To really have this exercise make sense, we need a little more information in our preferences.
As well as our counter, I'll also add a simple username edit text box. It looks basically the same as the counter so I won't bother repeating it below.
Repo Companion
companion object {
const val TAG = "DStre_RpI"
/** Main preference filename*/
private const val USER_PREFERENCES_NAME =
"imp_datastore_user_pref"
private val Context.dataStore:
DataStore<Preferences> by preferencesDataStore(
name = USER_PREFERENCES_NAME
)
val exampleCounterKd = PrefKeyWithDefault(
key = intPreferencesKey("example_counter"),
default = 0
)
val exampleUsernameKd = PrefKeyWithDefault(
key = stringPreferencesKey("example_username"),
default = "Default User"
)
}
/** Improved Preference Data Hooks */
data class PrefKeyWithDefault<T>(
val key: Preferences.Key<T>,
val default: T
)
Repo - Getters
val counterFlow = dataStore.flow(exampleCounterKd)
val usernameFlow = dataStore.flow(exampleUsernameKd)
fun <T> DataStore<Preferences>.flow(
key: Preferences.Key<T>,
default: T
): Flow<T> =
data.map { prefs -> prefs[key] ?: default }.distinctUntilChanged()
private fun <T> DataStore<Preferences>.flow(
prefKeyWithDefault: PrefKeyWithDefault<T>,
): Flow<T> = data.map { prefs ->
prefs[prefKeyWithDefault.key] ?: prefKeyWithDefault.default
}.distinctUntilChanged()
Repo - Setters
suspend fun updateCounter(value: Int) =
dataStore.update(exampleCounterKd, value)
suspend fun updateUsername(value: String) =
dataStore.update(exampleUsernameKd, value)
private suspend fun <T> DataStore<Preferences>.update(
keyDefault: PrefKeyWithDefault<T>, value: T
) {
edit { prefs ->
prefs[keyDefault.key] = value
}
}
Repo - Getters: Improvement
data class SettingsState(
val counter: Int = exampleCounterKd.default,
val username: String = exampleUsernameKd.default
)
val settingsState = combine(
counterFlow,
usernameFlow
) { counter, username ->
SettingsState(counter, username)
}
ViewModel Companion
companion object {
const val SUBSCRIBE_TIMEOUT = 5000L
fun factory(repo: DataStoreRepoImproved) =
viewModelFactory {
initializer { DataStoreViewModelImproved(repo) }
}
@Composable
fun get(): DataStoreViewModelImproved {
val context = LocalContext.current
val repo = remember { DataStoreRepoImproved(context) }
return viewModel(factory = factory(repo))
}
/**
* IMPORTANT NOTE:
* Fixed our tradeoff issue by adding a manual injection getter
* I wanted to avoid introducing this concept to newer users
*/
@Composable
fun get(repo: DataStoreRepoImproved): DataStoreViewModelImproved {
return viewModel(factory = factory(repo))
}
}
ViewModel Flow Wrapper
val counterState = repo.counterFlow
.asState(DataStoreRepoImproved
.exampleCounterKd.default)
val usernameState = repo.usernameFlow
.asState(DataStoreRepoImproved
.exampleUsernameKd.default)val uiState = repo.settingsState
.asState(
SettingsState(
counter = DataStoreRepoImproved.
exampleCounterKd.default,
username = DataStoreRepoImproved.
exampleUsernameKd.default
)
)
private fun <T> Flow<T>.asState(
initialValue: T
): StateFlow<T> =
stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(
SUBSCRIBE_TIMEOUT
),
initialValue = initialValue
)
val uiState = repo.settingsState
.asState( SettingsState() )
Alternative
val uiState = repo.settingsState
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(
SUBSCRIBE_TIMEOUT
),
initialValue = SettingsState()
)
ViewModel Setters
fun incrementCounter() = viewModelScope.launch {
repo.updateCounter(uiState.value.counter + 1)
}
fun updateUsername(newName: String) = viewModelScope.launch {
repo.updateUsername(newName)
}
fun incrementCounter() {
viewModelScope.launch {
repo.updateCounter(uiState.value.counter + 1)
}
}
fun updateUsername(newName: String) {
viewModelScope.launch {
repo.updateUsername(newName)
}
}
The Ui
@Composable
private fun Display( viewModel: DataStoreViewModelImproved ) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val onUpdated: (String)-> Unit = { newName ->
viewModel.updateUsername(newName)
}
val onIncrement = { viewModel.incrementCounter() }
DataStoreImprovedDisplay(
uiState,
onUpdated,
onIncrement
)
}
Summary
Updated!
Links
Blog / Gists
- DataStoreRepoImproved GitHub Gist
- DataStoreUiImproved GitHub Gist
- ComposableWidgets GitHub Gist (for parts 1 and 2)
- Blog Part 1
- Part 1 - DataStoreRepoBasic.kt GitHub Gist
- Part 1 - DataStoreUiBasic GitHub Gist
- Bonus Post - Composable Only Version

Comments
Post a Comment