Search code examples
listperformanceandroid-jetpack-composereactive-programmingkotlin-coroutines

Any benefits of using SnapshotStateList<T> over State<List<T> in terms of recomposition?


In jetpack compose, We can convert MutableStateFlow<T> to State<T> using collectAsState() and with delegation by can directly assign T to the variable.

Now as I understand, it doesn't matter much if I used MutableState<T> or MutableStateFlow<T> to hold the state, performance-wise. But for List types however, it seems there is no special version of Flow, ie, if you really need, you can Flow<List<T>, which I've seen used most of the time. Then, if you apply collectAsState() on Flow<List<T>, you get State<List<T>.

Now, if I understand correctly, even if a single value was changed (added, removed, modified) in the list the whole list state would be considered new thus all respective list item UI components would be recomposed. While with SnapshotStateList<T>, only the respective element UI will be (re)composed. Or, Jetpack compose has some internal optimization for State<List<T> that prevents these types of useless recomposition and I should not worry about it.


Solution

  • According to my tests, LazyColumn will NOT recompose items as long as you provide the key, even if the it's from State<List<T>.

    My test sample:

    
    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                TestSampleTheme {
                    // A surface container using the 'background' color from the theme
                    Surface(
                        modifier = Modifier.fillMaxSize(),
                        color = MaterialTheme.colorScheme.background
                    ) {
                        val vm: SimpleViewModel = viewModel()
                        val list by vm.list.collectAsState()
                        SimplePage(
                            list = list,
                            onClick = {
                                vm.add()
                                vm.shuffle()
                            },
                        )
                    }
                }
            }
        }
    }
    
    @OptIn(ExperimentalFoundationApi::class)
    @Composable
    fun SimplePage(
        list: List<Int>,
        onClick: () -> Unit,
    ) {
    
        Column(
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            LazyColumn(
            ) {
                items(
                    list,
                    key = { it }
                ) {
                    Text(
                        modifier = Modifier.animateItemPlacement(),
                        text = it.toString()
                    )
                }
            }
            Button(onClick = onClick) {
            }
        }
    
    }
    
    class SimpleViewModel : ViewModel() {
        //    val list = MutableStateFlow(listOf(1, 5, 8, 3, 7, 6))
        val list = MutableStateFlow(listOf(0))
    
        fun shuffle() = list.update {
            it.toMutableList().apply {
                shuffle()
            }
        }
    
        fun add() = list.update {
            it.toMutableList().apply {
                add(max() + 1)
            }
        }
    }
    
    

    Here, Modifier.animateItemPlacement() only works when you ensure unique key other than positional. And key itself ensures no recomposition for the same item. https://stackoverflow.com/a/70596559/13519865

    https://stackoverflow.com/a/68794279/13519865

    Finally, animateItemPlacement works, thus key used nicely.

    So, we can safely assume that we can use State<List<T> for it's Flow operation benefits without thinking much about recomposition cost.