SaveableStateHolder in Jetpack Compose
SaveableStateHolder in Jetpack Compose
SaveableStateHolder is a Compose interface that preserves and restores the rememberSaveable state of composable subtrees independently, keyed by a unique identifier. While rememberSaveable persists individual values across configuration changes, SaveableStateHolder manages entire scopes of saveable state, making it possible to save, remove, and restore state for composables that have independent lifecycles, such as screens in a navigation system. By the end of this lesson, you will be able to:
- Explain how
SaveableStateHolderdiffers fromrememberSaveablein scope and purpose. - Describe how
SaveableStateProviderassociates a unique key with a composable subtree's saveable state. - Apply
rememberSaveableStateHolder()to build navigation systems that preserve per screen state. - Identify when state should be scoped to a
SaveableStateHolderversus a ViewModel. - Explain the lifecycle of saved state as composable subtrees enter and leave the composition.
- Manage memory by removing saved state for permanently discarded screens.
How SaveableStateHolder Works
SaveableStateHolder is created via rememberSaveableStateHolder(). It provides a SaveableStateProvider(key, content) composable that wraps a subtree and associates all rememberSaveable calls within that subtree with the given key. When the subtree is removed from the composition, the state is retained in the holder. When the subtree is added back with the same key, the state is restored.
@Composable
fun SimpleNavigation(currentRoute: String) {
val stateHolder = rememberSaveableStateHolder()
stateHolder.SaveableStateProvider(currentRoute) {
when (currentRoute) {
"home" -> HomeScreen()
"settings" -> SettingsScreen()
}
}
}
When the user navigates from "home" to "settings", the HomeScreen composable leaves the composition. Normally, all rememberSaveable state inside HomeScreen would be lost. But because it is wrapped in SaveableStateProvider("home"), the state holder retains that state under the "home" key. When the user navigates back to "home", SaveableStateProvider("home") restores all the saved state, and the HomeScreen appears exactly as the user left it.
State Scoping with Unique Keys
The key passed to SaveableStateProvider determines the identity of the state scope. Each unique key maps to an independent set of saved state. This means two screens with different keys maintain completely separate state, even if they use the same composable function. The key can be any type that implements proper equals() and hashCode(), though strings and data classes are the most common choices.
Keys must be stable and unique across the holder's scope. Using a list index as a key breaks when items are reordered, because the state from position 0 would be restored to whatever item now occupies position 0, regardless of identity.
@Composable
fun TabLayout(tabs: List<String>, selectedTab: String) {
val stateHolder = rememberSaveableStateHolder()
tabs.forEach { tab ->
if (tab == selectedTab) {
stateHolder.SaveableStateProvider(tab) {
TabContent(tabId = tab)
}
}
}
}
@Composable
fun TabContent(tabId: String) {
var scrollPosition by rememberSaveable { mutableIntStateOf(0) }
var searchQuery by rememberSaveable { mutableStateOf("") }
LazyColumn(state = rememberLazyListState(scrollPosition)) {
// Each tab preserves its own scroll position and query
}
}
This interview continues for subscribers
Subscribe to Dove Letter for full access to exclusive interviews about Android and Kotlin development.
Become a Sponsor