What does Jetpack Compose remember actually do, how does it work under the hood?

67.7k Views Asked by At

Checking out codelab's basic tutorial there is a snippet to increase counter on button when clicked

@Composable
fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
    val counterState = remember { mutableStateOf(0) }

    Column(modifier = Modifier.fillMaxHeight()) {
        Column(modifier = Modifier.weight(1f)) {
            for (name in names) {
                Greeting(name = name)
                Divider(color = Color.Black)
            }
        }
        Counter(
            count = counterState.value,
            updateCount = { newCount ->
                counterState.value = newCount
            }
        )
    }
}


@Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
    Button(
        onClick = { updateCount(count + 1) },
        colors = ButtonConstants.defaultButtonColors(
            backgroundColor = if (count > 5) Color.Green else Color.White
        )
    ) {
        Text("I've been clicked $count times")
    }
}

It is clear that remember { mutableStateOf(0) } stores the state/value. My question is what remember does under the hood. Using var count = remember { 0 } or mutableStateOf(0) without remember does not increase the value.

fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
   
    var count = remember { 0 }

    Column(modifier = Modifier.fillMaxHeight()) {
        Column(modifier = Modifier.weight(1f)) {
            for (name in names) {
                Greeting(name = name)
                Divider(color = Color.Black)
            }
        }
        Counter(
            count = count,
            updateCount = { newCount ->
                count = newCount
            }
        )
    }
}

Snippet above does not update the value printed on Text, does remember only work with MutableState?

4

There are 4 best solutions below

1
On BEST ANSWER

remember - just allows you to remember state from a previous recompose invocation. So, if you for instance randomize color at initial run. The randomized color is going to be calculated only once and later reused whenever re-compose is necessary.

And hence,

remember = store the value just in case recompose is called.

Now, the second important thing is knowing when reCompose should actually be triggered and there the mutable states come to help.

mutablestate = store the value and in case I update value trigger, recompose for all elements using this data.

0
On

Codelab example mentions about remember and mutableState as

Reacting to state changes is at the very heart of Compose. Compose apps transform data into UI by calling Composable functions. If your data changes, you recall these functions with the new data, creating an updated UI. Compose offers tools for observing changes in your app's data, which will automatically recall your functions—this is called recomposing. Compose also looks at what data is needed by an individual composable so that it only needs to recompose components whose data has changed and can skip composing those that are not affected.

Under the hood, Compose uses a custom Kotlin compiler plugin so when the underlying data changes, the composable functions can be re-invoked to update the UI hierarchy.

To add internal state to a composable, use the mutableStateOf function, which gives a composable mutable memory. To not have a different state for every recomposition, remember the mutable state using remember. And, if there are multiple instances of the composable at different places on the screen, each copy will get its own version of the state. You can think of internal state as a private variable in a class.

remember{X} and remember{mutableStateOf(X)} are useful in different scenarios.

First one is required when your object doesn't need to be instantiated at each recomposition, and there is another trigger that triggers composition.

An example for this is remember{Paint()}, any object that doesn't need to be instantiated more than once or memory heavy to instantiate. If a Composable that possesses this object is recomposed, properties of your object don't change thanks to remember, if you don't use remember your object is instantiated on each recomposition and all the properties previously set are reset.

If you need a trigger(mutableStateOf) and need to have the latest value(remember) like in question choose remember{mutableStateOf()}

1
On

Variables are cleared on every compositon.

Using remember will get the previous value.

I think its equivalent to declare a mutableState in ViewModel.

1
On

To learn how composition and recomposition works you can check out Under the hood of Jetpack Compose article by Leland Richardson, which describes inner works very well, also youtube video here. And most of this answer uses article as reference and quoted most from it.

The implementation of the Composer contains a data structure that is closely related to a Gap Buffer. This data structure is commonly used in text editors.

A gap buffer represents a collection with a current index or cursor. It is implemented in memory with a flat array. That flat array is larger than the collection of data that it represents, with the unused space referred to as the gap.

Basically adding space near your Composable function slot table to be able to update UI dynamically with high costs since get, move, insert, and delete — are constant time operations, except for moving the gap. Moving the gap is O(n) but this does not happen often which you need to change all UI structure, on average, UIs don’t change structure very much.

@Composable
    fun Counter() {
     var count by remember { mutableStateOf(0) }
     Button(
       text="Count: $count",
       onPress={ count += 1 }
     )
    }

When the compiler sees the Composable annotation, it inserts additional parameters and calls into the body of the function. First, the compiler adds a call to the composer.start method and passes it a compile time generated key integer.

fun Counter($composer: Composer) {
 $composer.start(123)
 var count by remember($composer) { mutableStateOf(0) }
 Button(
   $composer,
   text="Count: $count",
   onPress={ count += 1 },
 )
 $composer.end()
}

When this composer executes it does the following:

  1. Composer.start gets called and stores a group object
  2. remember inserts a group object
  3. the value that mutableStateOf returns, the state instance, is stored.
  4. Button stores a group, followed by each of its parameters.
  5. And then finally we arrive at composer.end.

enter image description here

The data structure now holds all of the objects from the composition, the entire tree in execution order, effectively a depth first traversal of the tree.

So remember needed to store a mutableState() to get value from previous composition and mutableState() is required to trigger one.

And MutableState interface uses @Stable annotation

@Stable
interface MutableState<T> : State<T> {
    override var value: T
    operator fun component1(): T
    operator fun component2(): (T) -> Unit
}

Stable is used to communicate some guarantees to the compose compiler about how a certain type or function will behave.

When applied to a class or an interface, Stable indicates that the following must be true:

  1. The result of equals will always return the same result for the same two instances.
  2. When a public property of the type changes, composition will be notified.
  3. All public property types are stable. When applied to a function or a property, the Stable]annotation indicates that the function will return the same result if the same parameters are passed in. This is only meaningful if the parameters and results are themselves Stable, Immutable, or primitive.

The invariants that this annotation implies are used for optimizations by the compose compiler, and have undefined behavior if the above assumptions are not met. As a result, one should not use this annotation unless they are certain that these conditions are satisfied.

Another source with a Video that describes how Compose works.