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:

  1. Composition

  2. Recomposition

  3. 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:

  1. MyComposable scope

  2. Column content lambda

  3. 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

 
Previous
Previous

Passing data using CompositionLocal

Next
Next

StateFlow vs SharedFlow in Compose