How to use remember and rememberSaveable in Jetpack Compose

In this post we’ll cover how and when to use the remember and rememberSaveable Composable functions in Jetpack Compose. If you’ve just started to learn about state in compose, chances are the functions listed above have been introduced. When I first started ramping up on Jetpack Compose a lot of the documentation that I read had a major focus on state with these functions kind of just mentioned throughout.

remember

Lets start with remember. There are many different method signatures for the remember composable you can look at them here. However, I usually see it used like this:

 // The remember function is defined in runtime package

import androidx.compose.runtime.*

  var isEnabled by remember {
        mutableStateOf(true)
    }

// OR if you don't use 'by' 

    var isEnabled = remember {
       mutableStateOf(true)
    }

So what is remember doing? What remember is really doing is caching a result from a calculation and/or a piece of the state. Why do we want this? In the world of Compose each composable function can be called… a lot! If an expensive calculation is ran each time the composable is called UI performance will suffer. This is where remember comes in, the calculation defined in remember’s scope will only be ran when the composable is initially called. It will also keep track of the current variables value though re-compositions.

Stopwatch Demo

Lets look at a quick example demonstrating the functionality. Below is a composable for a stopwatch app that displays the screen. When you start the stopwatch, the time will be updated every second. On each update it triggers a re-composition.

@Composable
fun StopWatchScreen(viewModel: MainViewModel){

    var buttonText = if(viewModel.running) "Stop" else "Start"

    var test by remember {
        Log.d("COMPOSE_EXAMPLE", "StopwatchScreen remember called")
        mutableStateOf("Some Value")
    }

    var itemState = remember {
        Log.d("COMPOSE_EXAMPLE", "StopwatchScreen list of cards content remember called")
        generateListOfCardConent().toMutableStateList()
    }

    Log.d("COMPOSE_EXAMPLE", "Current size of item state ${itemState.size}")

    Log.d("COMPOSE_EXAMPLE", "Current value of test: $test")

    val currentTime by viewModel.flowTime.collectAsState()

    Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
        Text(modifier = Modifier.padding(10.dp), text = currentTime,
        fontSize = 50.sp)
        Row(modifier = Modifier.padding(8.dp)){
            Button(modifier = Modifier
                .height(75.dp)
                .width(150.dp)
                .padding(8.dp),
                   onClick = { if(viewModel.running)viewModel.Stop() else viewModel.Start()},
                   colors = if(viewModel.running) ButtonDefaults.buttonColors(Color.Red) else ButtonDefaults.buttonColors(Color.DarkGray)){
                Text(text = buttonText, fontSize = 10.sp, fontWeight = FontWeight.Bold)
            }
            Button(modifier = Modifier
                .height(75.dp)
                .width(150.dp)
                .padding(8.dp), onClick = {
                //FOR TEST PURPOSES TRIGGER CHANGES TO VARIABLES
                itemState.add(CardContent("t","t"))
                test = "new val"
                viewModel.Reset() },
                colors = if(viewModel.running)  ButtonDefaults.buttonColors(Color.LightGray) else ButtonDefaults.buttonColors(Color.DarkGray)){
                Text("Reset", fontSize = 10.sp, fontWeight = FontWeight.Bold)
            }
        }
    }
}

The screen that will be displayed is nothing fancy but serves for demo purposes.

In the code sample above there are two variables that are being assigned to the remember composable. logging was added in the remember scope to better visualize how remember works. We are logging what is currently stored in ‘test’ and the size of ‘itemState’ as well. Sample output is below.

$ adb logcat | grep COMPOSE_EXAMPLE
02-28 21:35:47.489 16796 16796 D COMPOSE_EXAMPLE: StopwatchScreen remember called
02-28 21:35:47.489 16796 16796 D COMPOSE_EXAMPLE: StopwatchScreen list of cards content remember called
02-28 21:35:47.490 16796 16796 D COMPOSE_EXAMPLE: Current size of item state 15
02-28 21:35:47.490 16796 16796 D COMPOSE_EXAMPLE: Current value of test: Some Value
02-28 21:36:12.917 16796 16796 D COMPOSE_EXAMPLE: Current size of item state 15
02-28 21:36:12.917 16796 16796 D COMPOSE_EXAMPLE: Current value of test: Some Value
02-28 21:36:13.910 16796 16796 D COMPOSE_EXAMPLE: Current size of item state 15
02-28 21:36:13.910 16796 16796 D COMPOSE_EXAMPLE: Current value of test: Some Value
02-28 21:36:14.927 16796 16796 D COMPOSE_EXAMPLE: Current size of item state 15
02-28 21:36:14.927 16796 16796 D COMPOSE_EXAMPLE: Current value of test: Some Value
02-28 21:36:15.929 16796 16796 D COMPOSE_EXAMPLE: Current size of item state 15
02-28 21:36:15.929 16796 16796 D COMPOSE_EXAMPLE: Current value of test: Some Value
02-28 21:36:16.916 16796 16796 D COMPOSE_EXAMPLE: Current size of item state 15
02-28 21:36:16.916 16796 16796 D COMPOSE_EXAMPLE: Current value of test: Some Value
02-28 21:36:19.345 16796 16796 D COMPOSE_EXAMPLE: Current size of item state 16
02-28 21:36:19.345 16796 16796 D COMPOSE_EXAMPLE: Current value of test: new val
02-28 21:36:24.789 16796 16796 D COMPOSE_EXAMPLE: Current size of item state 16
02-28 21:36:24.789 16796 16796 D COMPOSE_EXAMPLE: Current value of test: new val
02-28 21:36:25.804 16796 16796 D COMPOSE_EXAMPLE: Current size of item state 16
02-28 21:36:25.805 16796 16796 D COMPOSE_EXAMPLE: Current value of test: new val
02-28 21:36:26.805 16796 16796 D COMPOSE_EXAMPLE: Current size of item state 16
02-28 21:36:26.805 16796 16796 D COMPOSE_EXAMPLE: Current value of test: new val
02-28 21:36:27.799 16796 16796 D COMPOSE_EXAMPLE: Current size of item state 16
02-28 21:36:27.800 16796 16796 D COMPOSE_EXAMPLE: Current value of test: new val

As we can see in lines 2 and 3 the logs from remembers scope are only emitted once. After that we can see the logs outside of the remember scope emitted roughly every second. At line 16, the reset button was clicked and a new item was added to the itemState list. Which can be seen in the logs.

Remember will not store variables once the activity is destroyed or though configuration changes. Changing a setting like light mode to dark mode will cause the activity will be re-created. When the activity is re-created remember will be called again. Same situation for rotating the screen. So how do you keep track of variables though those situations?

rememberSaveable

This is what rememberSaveable is for. To use rememberSaveable its pretty much the same syntax that was used for remember.

   //Defined in the following package
   import androidx.compose.runtime.saveable.rememberSaveable
 
   var rs_test = rememberSaveable {
        mutableStateOf("")
    }

    val test by rememberSaveable {
        mutableStateOf(true)
    }

However, there is one kind of big gotcha when using rememberSaveable vs just remember. Under the hood rememberSaveable will store the values computed in a bundle that is passed back into the activity when it resumes. What this means is that if you are returning a type that can not be passed directly into a bundle then your app will just crash when that line is hit. Take a look at the code below to get an idea of what this looks like.

@Composable
fun StopWatchScreen(viewModel: MainViewModel){

    var buttonText = if(viewModel.running) "Stop" else "Start"

    var test by rememberSaveable {
        Log.d("COMPOSE_EXAMPLE", "StopwatchScreen remember called")
        mutableStateOf("Some Value")
    }

    var itemState = rememberSaveable {
        Log.d("COMPOSE_EXAMPLE", "StopwatchScreen list of cards content remember called")
        generateListOfCardConent().toMutableStateList()
    }

    Log.d("COMPOSE_EXAMPLE", "Current size of item state ${itemState.size}")
    Log.d("COMPOSE_EXAMPLE", "Current value of test: $test")

The above code is the first part of the StopWatchScreen code but with rememberSaveable instead of remember. First thing I noticed was there are no warnings and if you build the app like this it will compile without issue. Only real indication that something is wrong would be if you have the render preview screen displaying this particular composable, it will break. If you were to capture some logs you would get something emitted like what is seen below.

rememberSaveable Error

--------- beginning of crash
03-01 20:39:06.314 32610 32610 E AndroidRuntime: FATAL EXCEPTION: main
03-01 20:39:06.314 32610 32610 E AndroidRuntime: Process: com.example.basiccomposeproject, PID: 32610
03-01 20:39:06.314 32610 32610 E AndroidRuntime: java.lang.IllegalArgumentException: androidx.compose.runtime.snapshots.SnapshotStateList@915044 cannot be saved using the current SaveableStateRegistry. 
The default implementation only supports types which can be stored inside the Bundle. Please consider implementing a custom Saver for this class and pass it to rememberSaveable().
03-01 20:39:06.314 32610 32610 E AndroidRuntime:        at androidx.compose.runtime.saveable.RememberSaveableKt.requireCanBeSaved(RememberSaveable.kt:171)
03-01 20:39:06.314 32610 32610 E AndroidRuntime:        at androidx.compose.runtime.saveable.RememberSaveableKt.access$requireCanBeSaved(RememberSaveable.kt:1)
03-01 20:39:06.314 32610 32610 E AndroidRuntime:        at androidx.compose.runtime.saveable.RememberSaveableKt$rememberSaveable$1.invoke(RememberSaveable.kt:105)
03-01 20:39:06.314 32610 32610 E AndroidRuntime:        at androidx.compose.runtime.saveable.RememberSaveableKt$rememberSaveable$1.invoke(RememberSaveable.kt:99)
03-01 20:39:06.314 32610 32610 E AndroidRuntime:        at androidx.compose.runtime.DisposableEffectImpl.onRemembered(Effects.kt:81)
03-01 20:39:06.314 32610 32610 E AndroidRuntime:        at androidx.compose.runtime.CompositionImpl$RememberEventDispatcher.dispatchRememberObservers(Composition.kt:1032)
03-01 20:39:06.314 32610 32610 E AndroidRuntime:        at androidx.compose.runtime.CompositionImpl.applyChangesInLocked(Composition.kt:793)
03-01 20:39:06.314 32610 32610 E AndroidRuntime:        at androidx.compose.runtime.CompositionImpl.applyChanges(Composition.kt:813)
03-01 20:39:06.314 32610 32610 E AndroidRuntime:        at androidx.compose.runtime.Recomposer.composeInitial$runtime_release(Recomposer.kt:827)
03-01 20:39:06.314 32610 32610 E AndroidRuntime:        at androidx.compose.runtime.CompositionImpl.setContent(Composition.kt:519)
03-01 20:39:06.314 32610 32610 E AndroidRuntime:        at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:140)
03-01 20:39:06.314 32610 32610 E AndroidRuntime:        at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:131)
03-01 20:39:06.314 32610 32610 E AndroidRuntime:        at androidx.compose.ui.platform.AndroidComposeView.setOnViewTreeOwnersAvailable(AndroidComposeView.android.kt:1015)
03-01 20:39:06.314 32610 32610 E AndroidRuntime:        at androidx.compose.ui.platform.WrappedComposition.setContent(Wrapper.android.kt:131)
03-01 20:39:06.314 32610 32610 E AndroidRuntime:        at androidx.compose.ui.platform.WrappedComposition.onStateChanged(Wrapper.android.kt:182)
03-01 20:39:06.314 32610 32610 E AndroidRuntime:        at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:354)
03-01 20:39:06.314 32610 32610 E AndroidRuntime:        at androidx.lifecycle.LifecycleRegistry.addObserver(LifecycleRegistry.java:196)
03-01 20:39:06.314 32610 32610 E AndroidRuntime:        at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:138)
03-01 20:39:06.314 32610 32610 E AndroidRuntime:        at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:131)
03-01 20:39:06.314 32610 32610 E AndroidRuntime:        at androidx.compose.ui.platform.AndroidComposeView.onAttachedToWindow(AndroidComposeView.android.kt:1102)

So how do you resolve this issue? There are a few ways which I will just mention. First if possible use a type that is supported instead. If that is not feasible another option would be to create a custom saver, details on how to do that can be found here. This is not easy approach for all object but some might really lend themselves to it. Another option would be to use a ViewModel instead. Migrating the data to use a View Model instead is outside the scope of this post but will be discussed in a follow up post.

Hopefully, this clears up some confusion around these two composables and after working with them for a bit it gets easier to know when to use one vs the other. Cheers!