What is SaveableStateHolder?
Understand how saveable UI state can stay attached to content that temporarily leaves the Composition and later comes back.
What the interviewer is testing
The interviewer is really testing whether you understand the difference between UI being temporarily absent and UI being finished.
A stronger answer goes beyond saying that SaveableStateHolder saves rememberSaveable values. It explains why the state needs a stable identity to belong to, and why the code that owns that identity is usually the right place to manage its saved state.
❌ Common mistake
If they ask you to elaborate
Imagine a simple app with two tabs:
Home
SearchHome has a list and a search field. The user types "compose", scrolls halfway down the list, switches to Search, then comes back to Home.
From the user's point of view, this is still Home. They expect to come back to the same search and roughly the same place in the list.
But suppose the app only composes the selected tab:
when (currentTab) {
Tab.Home -> HomeScreen()
Tab.Search -> SearchScreen()
}When Search is selected, HomeScreen leaves the Composition.
That distinction matters. Home may still exist as a tab in the app, but the composable that was rendering it is no longer there.
State created with remember belongs to that composition:
var query by remember {
mutableStateOf("")
}If that composition is gone, the remembered value is gone with it.
For state that should be restored, the screen can use rememberSaveable:
var query by rememberSaveable {
mutableStateOf("")
}Now the value knows how to participate in saved-state restoration. But there is still a question above the screen itself:
When Home comes back, who says that this is the same Home that owned that saved state before?
This is the problem SaveableStateHolder helps with.
The code that switches between the tabs can create a holder:
val stateHolder = rememberSaveableStateHolder()and give each tab a key:
stateHolder.SaveableStateProvider(currentTab) {
when (currentTab) {
Tab.Home -> HomeScreen()
Tab.Search -> SearchScreen()
}
}Now there is a simple relationship:
Home
↓
state saved for Home
Search
↓
state saved for SearchWhen Home leaves the Composition, its saveable state can be kept under the Home key. When Home is shown again with that same key, the state can be restored.
The key becomes more important once the app can have more than one instance of the same kind of screen.
Suppose the app can open:
Details(product = 42)
Details(product = 73)This would be too broad:
stateHolder.SaveableStateProvider("details") {
DetailsScreen(productId)
}Both details screens would be presented as the same piece of UI.
Instead, the key should represent the actual entry that may come back:
stateHolder.SaveableStateProvider(entry.id) {
DetailsScreen(entry.productId)
}The useful way to think about the key is not "what string should this API receive?"
It is:
What makes this the same screen, tab, or entry when it comes back?
If the answer is the same key, the holder knows which saved state belongs there.
There is one last part of the lifetime to consider.
Suppose Details(42) is still in a back stack but another screen is currently on top of it. It is temporarily absent, so keeping its saved UI state makes sense.
Later the user presses Back and Details(42) is permanently removed from that back stack. Now there is nothing to return to.
That is when the owner can forget its state:
stateHolder.removeState(entry.id)So removeState is not just cleanup. It represents a real change in the UI lifetime:
temporarily not shown
↓
keep the saved state
permanently removed
↓
forget the saved stateAnd the holder still does not change what counts as saveable state.
Wrapping this:
val query = remember {
mutableStateOf("")
}in SaveableStateProvider does not make it restorable.
The state still needs an appropriate saveable mechanism, such as:
var query by rememberSaveable {
mutableStateOf("")
}rememberSaveable handles the value. SaveableStateHolder handles the longer-lived place that value belongs to.
Production thinking
A useful way to understand where SaveableStateHolder belongs is to build the problem up from a real app.
Start with a simple tab switcher:
var currentTab by rememberSaveable {
mutableStateOf(Tab.Home)
}
when (currentTab) {
Tab.Home -> HomeScreen()
Tab.Search -> SearchScreen()
}That is perfectly reasonable while the tabs do not have local UI state that needs to return with them.
Then Home gains a search field and a long list. Search gains its own filters and scroll position. The product requirement is simple: switching tabs should not make either tab feel like it started again.
At that point, the code that switches the tabs has an important piece of knowledge:
Home still exists even while Search is being shown, and Search still exists while Home is being shown.
That is the kind of situation where a holder can make sense:
val stateHolder = rememberSaveableStateHolder()
stateHolder.SaveableStateProvider(currentTab) {
when (currentTab) {
Tab.Home -> HomeScreen()
Tab.Search -> SearchScreen()
}
}Notice where the holder lives. It is not inside HomeScreen trying to preserve itself.
It sits with the code that knows which tabs exist and which one is currently being shown. That code knows whether Home is merely off-screen or actually gone.
Now let the app grow a little.
Suppose a tab can open a small back stack:
Home
Details(42)
Details(73)At this point the same idea still works, but the key can no longer mean only "details." Each entry needs to be distinguishable because each one may have its own scroll position, expanded sections, or partially entered UI state.
The state should follow the entry that owns it:
stateHolder.SaveableStateProvider(entry.id) {
EntryContent(entry)
}Then when an entry is genuinely removed:
stateHolder.removeState(entry.id)its saved UI state can go with it.
This is the point where the example starts to reveal something important: the app is gradually building navigation behavior.
It now has entries, stable keys, a back stack, rules for when entries stay alive, and rules for when they are removed.
If the real application already uses Navigation, that infrastructure already knows those things. It knows which back-stack entry a destination belongs to and when that entry is finished. In that case, application screens usually should not create another SaveableStateHolder around every destination. Let the navigation layer manage the saved-state lifetime it already owns.
Direct use becomes more interesting when the app is building this kind of behavior itself: a custom tab system, custom back stack, pager-like flow, or another piece of UI where several entries remain meaningful even though only some of them are currently composed.
The next question is what should actually be saved.
Imagine returning to Details(42). The user may reasonably expect to find:
the same scroll position
the same selected tab
the same expanded section
the text they had enteredThose are small pieces of UI state that help put the user back where they were.
That does not mean the app should save everything the screen happened to display:
repository results
database entities
network responses
the entire screen modelIf that data already has a proper source of truth, let the normal architecture produce it again. Save enough UI state to restore the user's place, then rebuild the rest from the state owners that already exist.
The same reasoning prevents another common overcorrection: moving every value into a ViewModel because the UI may disappear for a while.
A scroll position is still UI state. An expanded section is still UI state. A selected tab does not become business state just because its composable temporarily leaves the Composition.
So when this problem appears in a real app, work through it in this order:
- What does the state represent? Decide whether it is UI state or state that belongs to a longer-lived application owner.
- Who knows whether this screen, tab, or entry still exists? That is usually the code that should manage its saved-state lifetime.
- What makes it the same thing when it comes back? Use a key that follows that same entry.
- Is it temporarily away or actually finished? Keep state for the first case; forget it for the second.
- Is Navigation or another framework already doing this work? If so, use the lifetime it already provides instead of building another one beside it.
The goal is not to keep as much state alive as possible.
It is to keep the small amount of UI state that makes returning feel continuous, for exactly as long as the screen, tab, or entry it belongs to still exists.
Follow-up questions
What separates a senior answer
Key takeaways
Learn more
What are mutableStateListOf and mutableStateMapOf?
Understand when Compose can see changes inside a mutable collection and when replacing an immutable collection is clearer.
What is the composition lifecycle?
Understand when Compose starts keeping a part of the UI, what recomposition means while it remains there, and what happens when that part leaves.