Hoinzey

Javascript. Kotlin. Android. Java

Compose: Lists as state

I've been following Compose tutorials, practicing and using it in work. There is a lot to cover which is why I poke around on awful looking pages trying to do the same thing different ways to see if there is much difference.


One thing I've stumbled upon is in how using different types of lists as state can give different results. In the right scenario these could lead to more recompositions than you may have been expecting.


Our example will work of the code below. We'll have a button that adds to the list when clicked called from another composable.

                    
    @Composable
    fun ListButton(list: List<String>, add: () -> Unit) {
        println("Drawing List button")
        Button(...),
            onClick = {
                add()
                println("Size of list is now: ${list.size}")
            }) {}
        }
    }

    @Composable
    fun ListExample(...) {
        println("Creating list")
        val badList by remember{mutableStateOf(listOf("a"))}
        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")
            }
            //...and the other lists
        }
    }
                

The bad list

                    
    val badList by remember{mutableStateOf(listOf("a"))}
    onClick = { list.plus("b") }
                

This looks nice. It won't work. While Kotlin offers the plus method on an immutable list, it returns a new copy of the former list with the additional element. So as our list itself isn't being changed our composable has no reason to think it has to recompose.


The second and third lists

                    
    val secondList: SnapshotStateList = remember{ mutableStateListOf("a") }
    val thirdList: MutableList = remember{mutableStateListOf("a")}
    onClick = { lists.add("b") }//same applies to both
                

This also looks nice. The difference is it works lovely too. When this builds initially you'll get the following log messages from above:

  • Creating list...
  • Drawing second list button...
  • Drawing List button
  • Drawing second list button...

And when you click either of these buttons you'll only get the the update that "Size of list is now: X..". Thats perfect, thats exactly what we want. No extraneous recompositions.


The first list

                    
    var firstList by remember{mutableStateOf(listOf("a"))}
    onClick = { firstList = firstList.plus("b") }
                

Now this list looks an awful lot like our bad list? The difference is that in the onClick lambda we are reassigning our list to the newly returned one. This change is observed and triggers recomposition. Great.


The problem, which you'll see if you run this, is that our logs show a little more happing than we had hoped. When the page is first built, we get the same log messages as the first set above, that is a given. When you click the button and trigger the recomposition however you get:

  • Creating list...
  • Drawing first list button...
  • Drawing List button
  • Drawing second list button...
  • Drawing List button
  • Drawing second list button...
  • Size of list is now: X..

Now when this button is used, we aren't only recomposing the button we pressed. We are also recomposing the other 2 buttons if present, and even recomposing the parent method altogether ? Because our first list is immutable, whenever we change that we need to recompose all the scopes that are reading that state. In that case that goes all the way to the start of our calling method.


Lists 2 and 3 are the more "correct" way to use a list as state as they leverage the compose APIs better and can better inform where the recompositions need to take place.