remember vs rememberSaveable: deep dive into state management and recomposition in Jetpack Compose
Understanding the differences between remember and rememberSaveable by exploring their internal mechanisms, and how they relate to state and recomposition.

Summary
remember and rememberSaveable are key APIs for retaining state values across recompositions. In this article, you’ll learn how they differ by exploring their internal mechanisms and understanding their relationship to state and recomposition.
Jetpack Compose enables you to design and build UI components and screens in a declarative way, and this approach is powered by several key concepts. One of the most important is idempotence, the idea that a composable function will always produce the same UI output given the same input parameters. This ensures consistent rendering and helps Compose efficiently manage recompositions and UI updates.
In this article, you’ll explore the concept of recomposition in Jetpack Compose, dive into the internal workings of remember versus rememberSaveable, and learn how these APIs are used in real-world scenarios through examples from RevenueCat’s Android SDK.
Compose Phases and Recomposition
Before we dig into how remember
and rememberSaveable
work under the hood, let’s first understand why they’re needed in the first place, and what they actually do in Jetpack Compose.
The Jetpack Compose rendering pipeline is built around three core phases that define how UI is constructed and updated: Composition, Layout, and Drawing. These phases work together to form a unidirectional data flow, starting from the UI description and ending with pixels on the screen.
- Composition – What to show: Composable functions are executed to generate a UI tree that describes the current screen state.
- Layout – Where to show it: Each element in the tree is measured and positioned within a 2D space, considering constraints and child elements.
- Drawing – How to render it: The positioned elements are drawn onto a Canvas, typically mapped to the device screen.
Understanding this three-phase pipeline is key to writing a performant and maintainable UI in Compose, especially because these phases are tightly coupled with recomposition. As the name implies, recomposition restarts the pipeline from the composition phase, re-running your composable functions in response to state changes.
This means composable functions are re-invokable at any time, and you should write them assuming that they will be. However, without explicit state-handling mechanisms, every local variable inside a composable will be re-initialized on each recomposition, often leading to unexpected behavior.
That’s why the Compose Runtime introduces State to provide a way for values to survive recomposition and keep your UI in sync with the underlying data model.
Understanding State
Now, let’s take a moment to understand the nature of State in Jetpack Compose.
For example, consider the following simple function:
1@Composable
2fun Counter() {
3 var counter = 0
4
5 Button(
6 onClick = { counter++ }
7 ) {
8 Text(text = "Count: $counter")
9 }
10}
When you click the button, the counter
variable should increase by 1, and the Counter
composable function should re-render to reflect the updated state. However, it won’t behave as expected. That’s because Compose doesn’t automatically track regular variables, it relies on a special mechanism to detect changes and trigger recomposition.
State
To handle this properly, Compose provides a built-in mechanism called State. State allows the Compose runtime to observe value changes and automatically re-run the associated composable functions when needed. Once a state value changes, Compose will intelligently recompose only the affected parts of the UI.
Let’s update the function to use State below:
1@Composable
2fun Counter() {
3 var counter by mutableStateOf(0)
4
5 Button(
6 onClick = { counter++ }
7 ) {
8 Text(text = "Count: $counter")
9 }
10}
It looks like it should work, but you’ll notice a red underline under mutableStateOf
, with an error saying: “Creating a state object during composition without using remember.”
So what’s the issue? Let’s break it down.
A function is an independently executable block of code, and in Jetpack Compose, a composable function can be re-executed at any time due to state changes (i.e. recomposition). That means the counter
variable is re-initialized to 0
every time Counter()
is recomposed.
To persist the value across recompositions, you need to tell Compose to retain it in memory, and that’s exactly what remember
is for.
remember vs. rememberSaveable
To persist state across recompositions, Jetpack Compose provides two essential tools: remember
and rememberSaveable
. These APIs allow you to retain values even as your composable functions are re-executed, ensuring the UI remains consistent with user interactions.
Let’s revise the earlier example using remember
to preserve the counter value:
1@Composable
2fun Counter() {
3 var counter by remember { mutableStateOf(0) }
4
5 Button(onClick = { counter++ }) {
6 Text(text = "Count: $counter")
7 }
8}
In this case, every time the Counter
composable is recomposed, for example, after a state update, the current counter
value is preserved in memory instead of being reinitialized.
Both remember
and rememberSaveable
serve a similar purpose: they cache values in the Compose runtime memory, giving the illusion of persistence within an otherwise stateless function. However, while they may appear similar on the surface, their behaviors diverge significantly behind the scenes.
In short, remember
stores values in memory only as long as the process remains alive. If the app process is killed (e.g., backgrounded and reclaimed by the system), the state is lost. On the other hand, rememberSaveable
serializes and stores the state in a Bundle
, allowing it to survive configuration changes and process death, making it a better choice for preserving UI state across a wider range of lifecycle events.
We’ll explore those differences in more detail next.
remember
If you look into the internal implementation of remember
, you’ll see it’s quite straightforward something like this:
1@Composable
2inline fun <T> remember(
3 key1: Any?,
4 crossinline calculation: @DisallowComposableCalls () -> T
5): T {
6 return currentComposer.cache(currentComposer.changed(key1), calculation)
7}
The remember
function uses the Composer to cache a value in memory. This cached value is preserved across recompositions unless the provided key
changes, which is how remember
knows when to invalidate and recreate the value.
rememberSaveable
In Jetpack Compose, remember
is an API for maintaining state across recompositions. However, its memory is ephemeral; it does not survive configuration changes or, more critically, system-initiated process death. This is where rememberSaveable
steps in. It builds upon the foundation of remember
but adds a crucial layer of persistence, ensuring that UI state can be saved to and restored from a Bundle
, the standard Android mechanism for instance state.
A detailed analysis of its internal implementation reveals an internal mechanism that interacts with a SaveableStateRegistry
, uses a Saver
to convert objects into bundle-able types, and leverages the RememberObserver
lifecycle to manage its registration. Let’s dissect the code to understand how it achieves this resilience.
1@Composable
2fun <T : Any> rememberSaveable(
3 vararg inputs: Any?,
4 saver: Saver<T, out Any> = autoSaver(),
5 key: String? = null,
6 init: () -> T
7): T {
8 // 1. Determine a Unique Key
9 val compositeKey = currentCompositeKeyHash
10 val finalKey = if (!key.isNullOrEmpty()) {
11 key
12 } else {
13 compositeKey.toString(MaxSupportedRadix)
14 }
15
16 // 2. Access the Registry
17 val registry = LocalSaveableStateRegistry.current
18
19 // 3. Remember the Holder Object
20 val holder = remember {
21 val restored = registry?.consumeRestored(finalKey)?.let { saver.restore(it) }
22 val finalValue = restored ?: init()
23 SaveableHolder(saver, registry, finalKey, finalValue, inputs)
24 }
25
26 // 4. Value and SideEffect for Updates
27 val value = holder.getValueIfInputsDidntChange(inputs) ?: init()
28 SideEffect { holder.update(saver, registry, finalKey, value, inputs) }
29
30 return value
31}
Let’s break down this orchestration step by step:
- Determining a Unique Key: The first and most critical step is establishing a unique key for the state being saved. This key is used to store and retrieve the value from the
Bundle
. The function prioritizes a developer-providedkey
. If none is given, it falls back to using thecurrentCompositeKeyHash
, a value automatically generated by the Compose compiler based on the composable’s position in the UI tree. This ensures that the state is uniquely identified within its parent. - Accessing the Registry: It then looks up the
SaveableStateRegistry
from aCompositionLocal
. This registry is the central authority responsible for the entire save/restore process. It’s the component that actually interacts with the Android framework’sSavedStateHandle
orActivity.onSaveInstanceState
. - Remembering the
SaveableHolder
: This is the core of the implementation. Instead of just remembering the value itself, it remembers an instance of a private helper class,SaveableHolder
. This holder is where all the complex logic resides. During its initial creation insideremember
, it attempts to restore a value. It asks theregistry
if it has a previously saved value for thefinalKey
(consumeRestored
). If it does, it uses the providedsaver
object to convert the stored data back into its original typeT
(saver.restore(it)
). If no restored value is found (e.g., on first launch), it uses theinit
lambda to create the initial value. ThisSaveableHolder
is then stored in the composition’s memory. - Handling Inputs and Updates: The
vararg inputs
parameter behaves similarly to the keys inLaunchedEffect
. If the inputs change on a subsequent recomposition, it signals that the state should be reset. The lineholder.getValueIfInputsDidntChange(inputs) ?: init()
enforces this. TheSideEffect
ensures that any changes to the parameters (saver
,registry
,key
,value
,inputs
) are communicated to theholder
after every successful recomposition.
rememberSaveable
is not a simple function but an intricate system designed to bridge the declarative, in-memory world of Compose with the persistent, lifecycle-aware world of the Android framework. It uses a unique key to identify state and a SaveableStateRegistry
to manage the actual saving process.
The core logic is encapsulated within a SaveableHolder
object, which is stored in the composition via remember
. This holder acts as a RememberObserver
, using onRemembered
to register a value provider with the registry and onForgotten
to unregister it, thus perfectly synchronizing the persistence lifecycle with the composition lifecycle. By also tracking inputs
, it provides a powerful mechanism to not only survive process death but also to control when state should be reset based on external dependencies.
Now let’s explore SaveableHolder
.
The SaveableHolder: The Internal Workhorse
The SaveableHolder
class is a private implementation detail that encapsulates all the logic for interacting with the SaveableStateRegistry
. It implements RememberObserver
to hook into the composition lifecycle.
Let’s analyze its key responsibilities:
1private class SaveableHolder<T>(
2 // ... constructor properties ...
3) : SaverScope, RememberObserver {
4 private var entry: SaveableStateRegistry.Entry? = null
5 private val valueProvider = { /* saves the value */ }
6
7 // ... update function ...
8 fun update(
9 saver: Saver<T, Any>,
10 registry: SaveableStateRegistry?,
11 key: String,
12 value: T,
13 inputs: Array<out Any?>
14 ) {
15 ..
16 }
17
18 private fun register() {
19 // ... registers the valueProvider with the registry ...
20 }
21
22 override fun onRemembered() {
23 register()
24 }
25
26 override fun onForgotten() {
27 entry?.unregister()
28 }
29
30 // ... other functions ...
31}
- Registration with the Registry (
register
andonRemembered
): TheSaveableHolder
‘s most important job is to register itself with theSaveableStateRegistry
. When the holder is first “remembered” by the composition, itsonRemembered()
lifecycle callback is invoked. This, in turn, calls the privateregister()
function. Insideregister()
, it doesn’t pass its value directly to the registry. Instead, it provides a lambda,valueProvider
. This provider, when called by the registry during theonSaveInstanceState
event, will execute thesaver
logic to convert the current value into aBundle
compatible format. The registry returns anEntry
object, which is a handle that can be used later to unregister. - Unregistering (
onForgotten
): When the composable leaves the composition (or itsrememberSaveable
keys change), theonForgotten()
callback is triggered. This is the cleanup phase. The holder callsentry?.unregister()
, telling theSaveableStateRegistry
that it no longer needs to save this piece of state. This prevents memory leaks and ensures that state from disposed UI is not saved unnecessarily. - Updating State (
update
): Theupdate
function is called within theSideEffect
block. Its job is to keep the holder’s internal properties (like the currentvalue
, thesaver
, and thekey
) synchronized with the latest values from the composable function. If critical properties like thekey
or theregistry
itself change, it knows that its current registration is invalid, so it unregisters and re-registers itself to ensure correctness. - Handling Input Changes (
getValueIfInputsDidntChange
): This function is a simple but effective check. Before returning its stored value, it compares the currentinputs
from the recomposition with theinputs
it stored from the previous composition. If they don’t match, it returnsnull
, signaling to therememberSaveable
composable that the state should be reset to itsinit
value. This is howrememberSaveable
achieves the behavior of re-initializing when its keys change.
Use-cases
Now that you’ve explored the internals of remember
, rememberSaveable
, and their supporting components like SaveableStateRegistry
and SaveableHolder
, the next question is: When should you actually use each one?
At first glance, rememberSaveable
might seem like the better option, it can persist across both recompositions and process death. So should you just use it everywhere instead of remember
?
Let’s walk through some practical use cases from RevenueCat’s Android SDK to understand when each API is appropriate, and why picking the right one matters for both performance and user experience.
remember
As discussed earlier, remember
preserves state across recompositions, but it does not restore values after configuration changes (e.g., rotations or process death). In RevenueCat’s Android SDK demo, we use remember
with mutableStateOf
to hold UI state during recomposition, like this:
1var isDebugBottomSheetVisible by remember { mutableStateOf(false) }
2var showLogInDialog by remember { mutableStateOf(false) }
3var showApiKeyDialog by remember { mutableStateOf(false) }
So those values will be re-initialized to false
if a configuration change occurs, such as rotating the screen, switching to dark mode, or adjusting the system font size.
rememberSaveable
On the other hand, the RevenueCat SDK uses rememberSaveable
to persist specific state not only across recompositions, but also through configuration changes, as seen in the PaywallDialog
example.
1@Composable
2fun PaywallDialog(
3 paywallDialogOptions: PaywallDialogOptions,
4) {
5 val shouldDisplayBlock = paywallDialogOptions.shouldDisplayBlock
6 var shouldDisplayDialog by rememberSaveable { mutableStateOf(shouldDisplayBlock == null) }
7 if (shouldDisplayBlock != null) {
8 LaunchedEffect(paywallDialogOptions) {
9 launch {
10 shouldDisplayDialog = shouldDisplayPaywall(shouldDisplayBlock)
11 }
12 }
13 }
In this case, the paywall dialog should remain visible if the user isn’t entitled to access premium features, even after a configuration change. If you used remember
instead of rememberSaveable
, the shouldDisplayDialog
state could be reset to false
during such changes, like screen rotation, dark mode toggling, or font size adjustments, which might lead to unintended behavior, such as granting access to premium features without proper entitlement.
So for the same use cases of state to display a dialog, which one you should use can be very different on the situation. Then, does using rememberSaveable
everywhere instead of remember
is better since it has more features and retain better? The answer is no.
Retaining all states in every scenario can also cause unintended behavior, and as you’ve seen in the internal implementation, rememberSaveable
carries more overhead than remember
. That’s why it’s important to carefully choose the right API based on your specific requirements, balancing persistence needs against performance considerations.
Conclusion
In this article, you’ve covered the core concepts of Compose phases, recomposition, state, and the internal workings of remember
and rememberSaveable
along with guidance on selecting the right API for your needs. Together, these concepts form the cornerstone of the Compose Runtime, enabling Jetpack Compose to deliver a truly declarative UI experience.
As always, happy coding!
— Jaewoong
You might also like
- Blog post
Server-driven UI SDK on Android: how RevenueCat enables remote paywalls without app updates
Learning server-driven UI by exploring RevenueCat's Android SDK.
- Blog post
Turn Your App into Revenue: Building Paywalls in Android With Jetpack Compose
In this article, you'll learn how to seamlessly implement in-app subscriptions and paywall features in Android using Jetpack Compose and the RevenueCat SDK.
- Blog post
Ensure public interface reliability: Tracking API compatibility for Android and Kotlin
This article explores how to ensure public API reliability by tracking compatibility changes, by exploring the RevenueCat SDK.