1

I learned that Compose remembers a state in a way such as:

var text by remember { mutableStateOf("") }

So in this case, it remembers a MutableState of a String. My question is why it wants to remember a thing called "MutableState" rather than just the String itself, and why it requires an extra layer?

z.g.y
  • 5,512
  • 4
  • 10
  • 36
LXJ
  • 1,180
  • 6
  • 18

3 Answers3

2

I know its late, but here's what I understand with remember.

I have a simple Todo app where a list of todos are hoisted in a viewwmodelusing a SnapshotStatelist, this list is rendered by a LazyColumn where each todo model has its own remembered state where I do some pretty basic UI functionality (e.g card elevation, visibility of some icons). Any changes I make to a todo should propagate back up to the mutableStateList (e.g deleting a todo), SnapshotStateList will then notify the LazyColumn to perform a recomposition, however when I edit a Todo (e.g, modifying the title), I also have to update the item composable that holds this todo(some UI changes), then I got stuck up as I can't figure out why the item composable is not recomposing even if I was able to verify that the SnapShotStateList item is modified by using the code below

val todoList = viewModel.todos
val snapshot = Snapshot.takeMutableSnapshot()
snapshot.enter {
    for (todo in todoList) {
        Log.e("TodoModel", todo.title)
    }
}

Code that modifies the list

val index = todos.indexOfFirst { it.id == modifiedModel.id }
todos[index] = todos[index].copy(//..some properties to be copied)

I verified that any modification I make on a todo reflects back to its host list, but the item composable that renders a todo item doesn't trigger a re-composition. I kept reading some posts and carefully thinking object references and trying to understand my code based on this object reference thought, I thought that there must be something that holds the previous state of the item and the changes is not applied to the remember, until I found that you can supply a key to a remember where it will be the thing that will decide if remember needs to re-calculate. Now I found out that remember only remembers a state (by state I dont mean compose state, but state in general) on initial composition, it will keep that initial structure/state for as long as the entire composable it is part of is still running, in this case all the way up to the parent composable (i.e my DashboardScreen), what made my remember re-calculate is I supplied the key with the todo object itself

val itemState: ItemCardState = remember(key1 = todoModel) {
    ItemCardState(todoModel = todoModel)
}

This way, when a change happens to the SnapShotStateList, an item's remember will see the same object reference (data class) but with changes applied to it. remember caches the initial state and will hold it forever unless you supply a key that you think might change and will let remember re-calculate a new initial state to be remembered, in my case I supplied the key as the todo object itself and when remember sees the same object reference but with a different value, it will re-calculate.

Having this understanding now, I can't imagine a way when there is no layer that will hold an object (remember) and prevent unnecessary re-composition when the state of an object changes.

Just sharing what I learned, also open for discussion that I may have said in a wrongful way.

z.g.y
  • 5,512
  • 4
  • 10
  • 36
1

remember is used for storing objects to have it when a recomposition happens. Mutable state is used for triggering recomopsition, you can check this answer for more details.

by is delegation that is a feature of Kotlin which translates the code

var text = remember { mutableStateOf("") }
text.value = "newString"

you basically store a trigger and value inside a remember. when you change MutableState.value new recomposition occurs and in this new recomposition you get the latest value of MutableState.

There are also usecases of remember without needing MutableState like a Paint or custom object when something else triggers the recomposition like canvas touch position for instance.

you remember object since you won't instantiate it.

val paint = remember {Paint()}
var offset by remember {mutableStateOf(Offset.Zero)

then when offset changes with user touching screen you trigger recomposition but since and you don't need to instantiate Paint object again.

remember only and remember with MutableState has different usecases.

Thracian
  • 43,021
  • 16
  • 133
  • 222
1

Mutable state is needed for two reasons:

  1. Saving mutable state between recompositions. remember is gonna save result of lambda calculation, but if you change a variable later - remember cannot save and track it. The solution is to have a state holder - an object that's created by mutableStateOf, saved by remember, will always be the same, but you can mutate it properties, in this case value (which is hidden when you're using delegation with by).
  2. Triggering recomposition. If you just create a class, save it with remember and update a property, Compose won't know that it was changed, and that view update it needed - that's why a special Compose State was created, which notifies a view that it needs to be recomposed.

You can continue deepening your knowledge with state in Compose documentation and Thinking in Compose.

Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220