Compose remember vs remember mutableStateOf
March 2, 2022
Let’s talk about the difference between remember
and remember { mutableStateOf(““) }
in Jetpack Compose.
In particular, something like this:
var text = remember { "" }
vs
var text by remember { mutableStateOf("") }
There are 3 concepts to understand here:
Composition
Recomposition
Recompose scope
Composition
A Composition is a tree-structure of the composables that describe your UI. Initial composition is when a Composable tree gets rendered for the first time. Any subsequent state changes may trigger recomposition.
Recomposition
Depending on what changed within a composable, parts of it can get recomposed. For instance, you may have a MyComposable function with a Column Composable and a Button Composable and want to update the button text by clicking on it. Let’s look at the code and what gets recomposed. For now it’s more like a pseudo code—we are not using remember
or remember { mutableStateOf(““) }
:
fun MyComposable() { // Recompose Scope 1 Column( modifier = Modifier .fillMaxSize() ) { // Recompose Scope 2 (Column content lambda) Button( onClick = { /** change button text **/ } ) { // Recompose Scope 3 (Button content lambda) Text(someText) } } }
Recompose scope
MyComposable
and everything inside of it gets rendered as a result of initial composition.
The 3 scopes that can be recomposed here are:
MyComposable scope
Column content lambda
Button content lambda
Compose compiler is optimized so that only what’s necessary gets recomposed. In our example, to change Text value, we only need to recompose the scope affected (Button content lambda).
In fact, in our example, nothing can cause recomposition of Column content lambda so the Column and MyComposable scopes are actually the same one (that of MyComposable)
We can do some logging to check what scope gets recomposed for each action. Something like this would suffice:
println("currentRecomposeScope $currentRecomposeScope")
which will print out the hash code of the recompose scope (e.g. androidx.compose.runtime.RecomposeScopeImpl@3030561
)
Now that we understand recomposition scopes, let’s look at the difference between remember
and remember { mutableStateOf() }
remember
remember
computes a value only once during composition and returns it during recomposition. Every inner Composable will get that value and it won’t change even if any part of that Composable gets recomposed.
private val FRUITS = listOf( "apple", "tomato", "banana" )
@Composable fun MainScreenRemember() { println("currentRecomposeScope $currentRecomposeScope") var randomFruit = remember { FRUITS.random() } Column( modifier = Modifier .fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { println("currentRecomposeScope $currentRecomposeScope") Button( modifier = Modifier .width(100.dp), onClick = { randomFruit = FRUITS.random() } ) { println("currentRecomposeScope $currentRecomposeScope") Text(randomFruit) } } }
Clicking on the button will never update its text since we used remember
which means the value was set during composition only and cannot be updated (mutated). Looking at Logcat below we see that since the text of the button text is not being updated, recomposition does not happen at all (it’s not necessary—as far as Compose compiler sees it, nothing has changed to require recomposition)
currentRecomposeScope androidx.compose.runtime.RecomposeScopeImpl@b26349 currentRecomposeScope androidx.compose.runtime.RecomposeScopeImpl@b26349 currentRecomposeScope androidx.compose.runtime.RecomposeScopeImpl@6184fe5
Remember that remember
stores objects in the Composition and destroys these objects when the composable that uses remember
is destroyed (removed from Composition).
If you want your remembered value survive configuration changes, use rememberSaveable
which will store a result of the calculation in Bundle
.
remember { mutableStateOf }
Now let’s look at remember { mutableStateOf(““) }
.
private val FRUITS = listOf( "apple", "tomato", "banana" ) @Composable fun MainScreenRememberMutableState() { println("currentRecomposeScope $currentRecomposeScope") var randomFruit by remember { mutableStateOf(FRUITS.random()) } Column( modifier = Modifier .fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { println("currentRecomposeScope $currentRecomposeScope") Button( modifier = Modifier .width(100.dp), onClick = { randomFruit = FRUITS.random() } ) { println("currentRecomposeScope $currentRecomposeScope") Text(randomFruit) } } }
By using mutableStateOf
we allow mutating the randomFruit
value which causes recomposition of Composable scope(s) that use it (Button content lambda, in this case)
Note that now our Button text changes when clicked (given that current random value is different from previous one). The log contains the following:
// initial composition currentRecomposeScope androidx.compose.runtime.RecomposeScopeImpl@b26349 currentRecomposeScope androidx.compose.runtime.RecomposeScopeImpl@b26349 currentRecomposeScope androidx.compose.runtime.RecomposeScopeImpl@6184fe5 // button clicked - recomposition currentRecomposeScope androidx.compose.runtime.RecomposeScopeImpl@6184fe5 // button clicked - recomposition currentRecomposeScope androidx.compose.runtime.RecomposeScopeImpl@6184fe5 // button clicked - recomposition currentRecomposeScope androidx.compose.runtime.RecomposeScopeImpl@6184fe5
As you see, the only thing that gets recomposed after each button click is the Button content lambda. Hope the difference is now clear.
Why remember?
remember
is a calculation and can potentially be expensive.
@Composable inline fun <T> remember(calculation: @DisallowComposableCalls () -> T): T = currentComposer.cache(false, calculation)
To ensure optimal user experience, if your calculation is expensive, you don’t want to re-recalculate every time a Composable gets recomposed.
In addition, should a recomposition occur, often you don’t want to lose a value that it’s been calculated during composition.
Also, note that you can remember
a calculation for a given key (or a vararg keys: Any?
):
@Composable inline fun <T> remember( key1: Any?, calculation: @DisallowComposableCalls () -> T ): T { return currentComposer.cache(currentComposer.changed(key1), calculation) }
Here is an example:
var text = remember(userId) { FRUITS.random() }
You can check out the source code for this blog post at https://github.com/jshvarts/ComposeRemember