9

Compose remember vs remember mutableStateOf

 2 years ago
source link: https://www.valueof.io/blog/jetpack-compose-remember-vs-mutablestateof
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

March 2, 2022

IMG_7277.jpg

Let’s talk about the difference between remember and remember { mutableStateOf(““) } in Jetpack Compose.

In particular, something like this:

var text = remember { "" }
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)
        }
    }
}
compose-remember-calc.gif

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)

compose-remember-mutable-state-of-calc.gif

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK