Managing Persistent State in Jetpack Compose

State management in Jetpack Compose becomes challenging when dealing with persistent data. Traditionally, developers rely on Android’s DataStore, but integrating it seamlessly with Compose can require excessive boilerplate. Let’s explore how to simplify this by building our own PreferenceState class to read and write data to dataStore just as you would read and update data from a state.
If you want to skip the implementation, you can check out the PreferenceState library that I developed. It provides the same functionality without the need to write boilerplate code. You can find it here: PreferenceState Library, or continue reading to understand how it works step by step.
Problem Statement
When you need to store data in DataStore and update the UI state based on it, the typical approach involves multiple layers: a ViewModel, a repository, and then collecting DataStore values to update the state. This results in excessive boilerplate code, making the implementation unnecessarily complex.
How We Solve This Problem
To simplify state persistence in Jetpack Compose, we will create a PreferenceState
class that directly interacts with DataStore while behaving like a MutableState
. This eliminates the need for a ViewModel or repository, reducing boilerplate code while keeping the logic concise and reusable.
Step-by-Step Implementation of PreferenceState
Step 1: Creating the PreferenceState Class
abstract class PreferenceState<T>(
private val key: Preferences.Key<T>,
private val defaultValue: T,
private val dataStore: DataStore<Preferences>
) : MutableState<T> {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val _state = mutableStateOf(defaultValue)
override var value: T
get() = _state.value
set(value) {
_state.value = value
scope.launch {
dataStore.edit { it[key] = value }
}
}
init {
scope.launch {
dataStore.data.map { it[key] ?: defaultValue }
.collect { newValue ->
withContext(Dispatchers.Main) {
_state.value = newValue
}
}
}
}
override fun component1(): T = value
override fun component2(): (T) -> Unit = { value = it }
}
Step 2: Creating a Custom Preference State Class
class UsernamePreferenceState: PreferenceState<String>(
key = stringPreferencesKey("username"),
defaultValue = "Guest",
dataStore = createDataStore()
)
Since creating a DataStore instance is straightforward, we are skipping that part.
Step 3: Creating a Remember Function for Compose
@Composable
fun rememberUsernamePreferenceState(): MarketViewSwitchState = remember {
UsernamePreferenceState()
}
Using PreferenceState in a Real-Life Scenario
Let’s say we want to persist a username input field across app restarts. We can use PreferenceState
like this:
@Composable
fun UserProfileScreen(modifier: Modifier = Modifier) {
var username by rememberUsernamePreferenceState()
Column {
TextField(
value = username,
onValueChange = { username = it },
label = { Text("Username") }
)
}
}
This allows username
to persist even when the app is restarted, without manually handling Flow
collections or LaunchedEffect
calls.
Conclusion
Building a PreferenceState
class from scratch helps in understanding how DataStore integrates with Compose. However, for convenience, the PreferenceState library provides a ready-to-use implementation, making persistent state management easier in Jetpack Compose applications. You can find the library here: PreferenceState Library.