HoinzeyBear
HoinzeyBear

Reputation: 75

Compose: Why does a list initiated with "remember" trigger differently to Snapshot

I've been messing around with Jetpack Compose and currently looking at different ways of creating/managing/updating State.

The full code I'm referencing is on my github

I have made a list a piece of state 3 different ways and noticed differences in behavior. When the first list button is pressed, it causes all 3 buttons to be recomposed. When either of the other 2 lists are clicked though they log that the list has changed size, update their UI but trigger no recompose of the buttons ?

To clarify my question, why is that when I press the button for the firsList I get the following log messages, along with size updates:

Drawing first DO list button
Drawing List button
Drawing second DO list button
Drawing List button
Drawing third DO list button
Drawing List button

But when I press the buttons for the other 2 lists I only get the size update log messages ?
Size of list is now: 2
Size of list is now: 2

var firstList by remember{mutableStateOf(listOf("a"))}
val secondList: SnapshotStateList<String> = remember{ mutableStateListOf("a") }
val thirdList: MutableList<String> = remember{mutableStateListOf("a")}

Row(...) {
        println("Drawing first DO list button")
        ListButton(list = firstList){
            firstList = firstList.plus("b")
        }
        println("Drawing second DO list button")
        ListButton(list = secondList){
            secondList.add("b")
        }
        println("Drawing third DO list button")
        ListButton(list = thirdList){
            thirdList.add("b")
        }
    }

When I click the button, it adds to the list and displays a value. I log what is being re-composed to help see what is happening.

@Composable
fun ListButton(modifier: Modifier = Modifier,list: List<String>, add: () -> Unit) {
    println("Drawing List button")
    Button(...,
        onClick = {
            add()
            println("Size of list is now: ${list.size}")
        }) {
        Column(...) {
            Text(text = "List button !")
            Text(text = AllAboutStateUtil.alphabet[list.size-1])
        }
    }
}

I'd appreciate if someone could point me at the right area to look so I can understand this. Thank you for taking the time.

Upvotes: 3

Views: 1644

Answers (1)

Richard Onslow Roper
Richard Onslow Roper

Reputation: 6835

I'm no expert (Well,), but this clearly related to the mutability of the lists in concern. You see, Kotlin treats mutable and immutable lists differently (the reason why ListOf<T> offers no add/delete methods), which means they fundamentally differ in their functionality.

In your first case, your are using the immutable listOf(), which once created, cannot be modified. So, the plus must technically be creating a new list under the hood.

Now, since you are declaring the immutable list in the scope of the parent Composable, when you call plus on it, a new list is created, triggering recompositions in the entire Composable. This is because, as mentioned earlier, you are reading the variable inside the parent Composable's scope, which makes Compose figure that the entire Composable needs to reflect changes in that list object. Hence, the recompositions.

On the other hand, the type of list you use in the other two approaches is a SnapshotStateList<T>, specifically designed for list operations in Compose. Now, when you call its add, or other methods that alter its contents, a new object is not created, but a recomposition signal is sent out (this is not literal, just a way for you to understand). The way internals of recomposition work, SnapshotStateList<T> is designed to only trigger recompositions when an actual content-altering operation takes place, AND when some Composable is reading it's content. Hence, the only place where it triggered a recomposition was the list button that was reading the list size, for logging purposes.

In short, first approach triggers complete recompositions since it uses an immutable list which is re-created upon modification and hence the entire Composable is notified that something it is reading has changed. On the other hand, the other two approaches use the "correct" type of lists, making them behave as expected, i.e., only the direct readers of their CONTENT are notified, and that too, when the content (elements of the list) actually changes.

Clear?

EDIT:

EXPLANATION/CORRECTION OF BELOW PROPOSED THEORIES:

You didn't mention MutableListDos in your code, but I'm guessing it is the direct parent of the code you provided. So, no, your theory is not entirely correct, as in the immutable list is not being read in the lambda (only), but the moment and the exact scope where you are declaring it, you send the message that this value is being read then and there. Hence, even if you removed the lambda (and modified it from somewhere else somehow), it will still trigger the recompositions. The Row still does have a Composable scope, i.e., it is well able to undergo independent recompositions, but the variable itself is being declared (and hence read) in the parent Composable, outside the scope of the Row, it causes a recomp on the entire parent, not just the Row Composable.

I hope we're clear now.

Upvotes: 3

Related Questions