I'm having some issues with LazyColumn and re-comp...
# compose-android
j
I'm having some issues with LazyColumn and re-composition. The following, whittled down example re-composes the entire list every-time I click on any item:
Copy code
var selectedItemSnippet: String? by remember { mutableStateOf(null) }
val itemsList: List<String> = listOf("a", "b", "c")
val selectedItem = remember(selectedItemSnippet) { itemsList.firstOrNull { it == selectedItemSnippet } }

LazyColumn(
  modifier = Modifier.padding(paddingValues),
  contentPadding = PaddingValues(10.dp),
  verticalArrangement = Arrangement.spacedBy(10.dp),
) {
  items(
    items = itemsList,
    key = { item -> item }
  ) {
    Card(
      modifier = Modifier.clickable {
        selectedItemSnippet = it
      }
    ) {
      Text("Hello ${it}")
    }
  }
}
If I remove the
val selectedItem = rememb...
line, then no recomposition happens. At a loss for words as to what's happening here. Any help is greatly appreciated!
z
You’re allocating a new list instance each time, do you have strong skipping on?
j
Not sure I follow
Ah, looks like it's a build time option. I have not altered or set enableStrongSkippingMode
You’re allocating a new list instance each time
Where exactly am I doing so, and what's the implication?
z
You read
selectedItemSnippet
in the outer computable when you pass it as the key to remember for
selectedItem
. This causes the outer composable to recompose every time you select a new item. When this happens, you allocate a new
items
list since it’s not remembered. This means that the
LazyColumn
sees it as a completely new list and probably what causes every item to be recomposed. This might not happen if strong skipping is enabled, I’m not sure. Or
Removing the
selectedItem
eliminates the recompositions because it removes the state read.
j
Interesting. So I tweaked:
Copy code
val itemsList: List<String> = listOf("a", "b", "c")
to:
Copy code
val itemsList: List<String> = remember { listOf("a", "b", "c") }
Still no dice, I still see full recomposition.
z
LazyColumn
has always recomposed its contents more aggressively than it seems like it needs to, so it might just do it every time it’s called. You could make
selectedItem
a derived state.
b
Strong skipping would help, upgrade to Kotlin 2.0.20 if you haven't or enable it with that option you found. Also, I think it's the clickable. Try change
clickable
to the following to get the non-composed version
Copy code
clickable(
   interactionSource = null,
   indication = ripple(),
   onClick = { selectedItemSnippet = it }
)
j
So, interestingly, just making it a derived state where it is didn't help. Neither did the clickable change. But, moving the derivedStateOf down into the items loop absolutely fixed it!
Copy code
var selectedItemSnippet: String? by remember { mutableStateOf(null) }
  val itemsList: List<String> = remember { listOf("a", "b", "c") }

  LazyColumn(
    modifier = Modifier.padding(paddingValues),
    contentPadding = PaddingValues(10.dp),
    verticalArrangement = Arrangement.spacedBy(10.dp),
  ) {
    items(
      items = itemsList,
      key = { item -> item }
    ) {
      val wasSelected by remember { derivedStateOf { it == selectedItemSnippet } }

      Card(
        modifier = Modifier.clickable(
          interactionSource = null,
          indication = ripple()
        ) {
          selectedItemSnippet = it
        }
      ) {
        Log.e("Test", "Recomposing ${it}")
        Text("Hello ${it} ${wasSelected}")
      }
    }
  }
b
I don't think you even need it as state, you can just have it as
val wasSelected = it == selectedItemSnippet
Unrelated but also watch out for
key = { item -> item }
with that you can't have duplicate strings in your list. You can just delete it and it will use the index of the item
👍 1
j
I needed to. Without the derived bit, it was recomposing all items. With the derived state, only the one I clicked on (and the one that was previously clicked) got recomposed, correctly.
Thank you for that tip!
b
Ah of course because every item would be reading the mutable state. I think it would be cleaner to just move your item into it's own composable and then you'd have a new scope for the recomposition
Copy code
var selectedItemSnippet: String? by remember { mutableStateOf(null) }
    val itemsList: List<String> = remember { listOf("a", "b", "c", "d", "e") }

    LazyColumn(
        contentPadding = PaddingValues(10.dp),
        verticalArrangement = Arrangement.spacedBy(10.dp),
    ) {
        items(items = itemsList) {
            MyItem(
                item = it,
                selected = it == selectedItemSnippet,
                onClick = {
                    selectedItemSnippet = it
                }
            )
        }
    }
Copy code
@Composable
fun MyItem(
    item: String,
    selected: Boolean,
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Card(
        modifier = modifier.clickable(onClick = onClick)
    ) {
        Log.e("Test", "Recomposing ${item}")
        Text("Hello ${item} ${selected}")
    }
}
I think that would work, now only the current and previously selected one would be recomposed
j
Ah, that worked too
Here's another question. I modified the above to the following:
Copy code
@Composable
fun MyItem(
  item: String,
  selected: Boolean,
  onClick: () -> Unit,
  modifier: Modifier = Modifier
) {
  val item2 by remember{ derivedStateOf { item } }

  Card(
    modifier = modifier.clickable(onClick = onClick)
  ) {
    Log.e("Test", "Recomposing ${item2}")
    Text("Hello ${item2} ${selected}")
  }

  Card(
    modifier = modifier.clickable(onClick = onClick)
  ) {
    Log.e("Test", "Recomposing2 ${item2}")
    Text("Hello2 ${item2}")
  }
}
Anytime I click on one of the items (after clicking on another previously), I see this:
Copy code
10-17 21:49:46.728 32259 32259 E Test   : Recomposing a
10-17 21:49:46.729 32259 32259 E Test   : Recomposing2 a
10-17 21:49:46.729 32259 32259 E Test   : Recomposing b
10-17 21:49:46.729 32259 32259 E Test   : Recomposing2 b
I would have thought that the second Card instance wouldn't have recomposed? (which doesn't depend on selected, and only depends on
item
, which isn't changing)
b
With that one I think you are back to the clickable problem from before
.clickable(onClick = onClick)
this makes composables unskippable. My snippet works around that by moving it into it's own scope so it doesn't matter
j
No luck with this variant either:
Copy code
@Composable
fun MyItem(
  item: String,
  selected: Boolean,
  onClick: () -> Unit,
  modifier: Modifier = Modifier
) {
  val item2 by remember{ derivedStateOf { item } }
  val onClick2 = remember(onClick) { onClick }

  Card {
    Log.e("Jerry", "Recomposing2 ${item2}")
    Text("Hello2 ${item2}")
  }

  Card(
    modifier = modifier.clickable(
      interactionSource = null,
      indication = ripple(),
      onClick = onClick2
    )
  ) {
    Log.e("Jerry", "Recomposing ${item2}")
    Text("Hello ${item2} ${selected}")
  }
}
b
These single extra recompositions are exceedingly cheap btw, this is good for learning how things work but I wouldn't worry about this if you are making a real app. Fixing every item in a list recomposing is normally worth it, but your last example you would just let happen
j
I shifted to your logic, and I removed the clickable from the first card
Yeah, I figured. Mostly, I'm just trying to build a mental model of how it works and come up with some sane rules in my mind 🙂
b
OK this one is a classic gotcha, your log is causing the recomposition
Because your log reads
item2
, that invalidates that scope
j
On the first Card, item2 isn't changing though?
b
I think now you might be getting into compiler optimizations. At a guess without looking deeply, the card content lambda is being reused across composables and getting recalled with the different values of each item. I bet if you delete item item2 reads inside the first card you will see it only gets called once and then starts skipping after that
That really is a guess though
j
Gotcha. Thanks for the tips! It's pretty late here, so will call it night at this point.