1

I have a scenario where I add item(s) to a SnapshotStateList<T> inside the ViewModel. This list of items gets displayed using a LazyColumn in the UI.

This is my ViewModel class

class MainViewModel(application: Application) : AndroidViewModel(application) {

    val data = mutableStateListOf<String>()

    init {
        viewModelScope.launch { loadData() }
    }

    private suspend fun loadData() {
        // I am adding items after a delay to simulate
        // items getting added from a flow/other sources
        delay(2000L)
        data.add(sampleText(1))

        delay(2000L)
        data.add(sampleText(2))

        delay(2000L)
        data.add(sampleText(3))

        delay(2000L)
        data.add(sampleText(4))

        delay(2000L)
        data.add(sampleText(5))
    }

    private fun sampleText(id: Int): String = "Sample Text $id"
}

The UI consists of a single LazyColumn Composable.

@Composable
fun HomeScreen(viewModel: MainViewModel = viewModel()) {
    TextList(textItems = viewModel.data)
}

@Composable
fun TextList(textItems: List<String>) {
    LazyColumn {
        items(textItems, key = { it.hashCode() }) {
            println("TextList: Composing Text = $it")
            Text(text = it)
        }
    }
}

I expect to see the println() line for each item as they get added to the list. However, this is what I see in the logs:

2023-08-12 21:55:30.352 29930-29930 System.out  I  TextList: Composing Text = Sample Text 1
2023-08-12 21:55:30.895 29930-29930 System.out  I  TextList: Composing Text = Sample Text 1
2023-08-12 21:55:30.905 29930-29930 System.out  I  TextList: Composing Text = Sample Text 2
2023-08-12 21:55:32.899 29930-29930 System.out  I  TextList: Composing Text = Sample Text 1
2023-08-12 21:55:32.902 29930-29930 System.out  I  TextList: Composing Text = Sample Text 2
2023-08-12 21:55:32.912 29930-29930 System.out  I  TextList: Composing Text = Sample Text 3
2023-08-12 21:55:34.918 29930-29930 System.out  I  TextList: Composing Text = Sample Text 1
2023-08-12 21:55:34.921 29930-29930 System.out  I  TextList: Composing Text = Sample Text 2
2023-08-12 21:55:34.923 29930-29930 System.out  I  TextList: Composing Text = Sample Text 3
2023-08-12 21:55:34.931 29930-29930 System.out  I  TextList: Composing Text = Sample Text 4
2023-08-12 21:55:36.919 29930-29930 System.out  I  TextList: Composing Text = Sample Text 1
2023-08-12 21:55:36.922 29930-29930 System.out  I  TextList: Composing Text = Sample Text 2
2023-08-12 21:55:36.925 29930-29930 System.out  I  TextList: Composing Text = Sample Text 3
2023-08-12 21:55:36.931 29930-29930 System.out  I  TextList: Composing Text = Sample Text 4
2023-08-12 21:55:36.943 29930-29930 System.out  I  TextList: Composing Text = Sample Text 5

I am confused as to why all the previously added items get's re-composed every time a new item is added to the list?

This seems related to Jetpack Compose lazy column all items recomposes when a single item update, but the solution didn't help here.

Edit #1: As suggested by CommonsWare, I did flatten out the UI and moving the TextList but the results don't change

@Composable
fun HomeScreen(viewModel: MainViewModel = viewModel()) {
    LazyColumn {
        items(viewModel.data, key = { it.hashCode() }) {
            println("TextList: Composing Text = $it")
            Text(text = it)
        }
    }
}
Ashutosh Patoa
  • 294
  • 4
  • 20
  • Flatten your setup, by putting the `LazyColumn()` directly in `HomeScreen()` and getting rid of `TextList()`. That puts the reference to your `SnapshotStateList` (`viewModel.data`) in the `items()` call, instead of it being outside the `LazyColumn()` as you have it now. Do your results change? – CommonsWare Aug 12 '23 at 17:52
  • 1
    Thanks for the suggestion, but I can confirm that the results don't change. – Ashutosh Patoa Aug 12 '23 at 18:01
  • I don’t think this is causing your problem, but the lambda you’re passing for `key` is wrong. First, `it` in this case is the index of the item in the list, not the item itself, so it doesn’t make sense to use `it.hashCode` , but also, you must not use hashcodes at all because they aren’t guaranteed to be unique. – Tenfour04 Aug 13 '23 at 02:13
  • @Tenfour04 I would disagree, as per the documentation https://developer.android.com/jetpack/compose/lists#item-keys and based on the lamda definition in the source code: `key: ((item: T) -> Any)? = null`. – Ashutosh Patoa Aug 13 '23 at 12:30
  • You’re right about that—I was looking at [this other overload](https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/LazyListScope#items(kotlin.Int,kotlin.Function1,kotlin.Function1,kotlin.Function2)) of `items`. But you still shouldn’t use hashcode because a hashcode is not guaranteed unique. You could possibly use the String itself if the Strings never repeat in the list. – Tenfour04 Aug 13 '23 at 17:00

1 Answers1

0

Your code will skip Text compositions, regardless of using the key or not. To see the effect of the key in LazyColumn, modify the code slightly.

class MainViewModel(application: Application) : AndroidViewModel(application) {

    val data = mutableStateListOf<String>()

    init {
        viewModelScope.launch { loadData() }
    }

    private suspend fun loadData() {
        // I am adding items after a delay to simulate
        // items getting added from a flow/other sources
        delay(2000L)
        data.add(sampleText(1))

        delay(2000L)
        data.add(sampleText(2))

        delay(2000L)
        data.add(sampleText(3))

        delay(2000L)
        data.add(sampleText(4))

        delay(2000L)
        data.add(0 ,sampleText(5)) // add Sample Text 5 at the index 0
    }

    private fun sampleText(id: Int): String = "Sample Text $id"
}

The code will not recompositions of Text if you don't use any key on items. When Sample Text 5 is added, recompositions will happen.

Sample Text 1 // this will be recomposed to Sample Text 5
Sample Text 2 // this will be recomposed to Sample Text 1
Sample Text 3 // this will be recomposed to Sample Text 2
Sample Text 4 // this will be recomposed to Sample Text 3
// a new Text will be created as Sample Text 4

But, when you specifiy the key of items, recompositions will be skipped. You can check it on the layout inspector.

With id, recompositions skipped

The numbers on the right side are skip counts.

ObscureCookie
  • 756
  • 2
  • 10