One Off the Slack: Why Is My List So Sluggish?

Saket Narayan asked:

I have a LazyColumn with AndroidView items and I’m just noticing that AndroidView#update is getting called on every scroll/recomposition. Is this expected?

When the reaction was “no”, Saket continued:

I think I know why it’s happening. We have a background that is scrolled with the list. I am doing this by calculating the scroll offset from my LazyListState. Because the background is synced with scroll, it’s causing recompositions on every scroll. What I don’t understand is why would my LazyColumn items also get invalidated if they aren’t reading this state. Here’s a gist of what my code looks like:

@Composable
fun Content() {
  val listState = rememberLazyListState()
  val scrollOffsetPx = listState.rememberScrollOffsetPx()

  GradientBackground(scrollOffsetPx)

  LazyColumn(state = listState) {
    Row(...)
    Row(...)
    Row(...)
  }
}

fun <V : View, M> LazyListScope.Row(
  key: InvestmentEntityViewType, // Some enum
  modifier: Modifier = Modifier,
  model: M?,
  create: () -> V,
  render: V.(M) -> Unit = {}
) {
  item(key) {
    if (model != null) {
      AndroidView(
        modifier = modifier,
        factory = { create() },
        update = { view -> view.render(model) }
      )
    }
  }
}

Google’s Andrey Kulikov responded:

when your Content recomposed you also provide a new lambda as a content for LazyColumn which means that you have new lambda for each item so it has to recompose. you need to try to better localize the recomposition so it only affects GradientBackground, not the whole Content. or even better is to avoid recompositions at all and only do redrawing on each scroll if possible

for example you can make your GradientBackground to accept LazyListState, not resolved px and do offset calculation in there. this will make it to only recompose GradientBackground when the offset changes

In other words, when the scrollOffsetPx state changes, Content() is going to get recomposed, and it is going to do all the work that Content() calls for. Since the scroll position changes a lot while the user is scrolling, this means that Content() is going to get recomposed a lot. If Content() were cheap, that could work… but in this case Content() is a relatively heavyweight composable.

Andrey’s suggestion would be to have Content() be more like:

@Composable
fun Content() {
  val listState = rememberLazyListState()

  GradientBackground(listState)

  LazyColumn(state = listState) {
    Row(...)
    Row(...)
    Row(...)
  }
}

…where GradientBackground() observes the scrollOffsetPx state. That way, when that state changes, only GradientBackground() needs to get recomposed, not the whole of Content() and its LazyColumn().

Referencing State values has unusual characteristics: each individual call is very inexpensive, but if that state changes a lot, the overall impact of having that call may be rather expensive. This is a atypical pattern in Android app development, one that we will need to get used to.


Read the original thread in the kotlinlang Slack workspace. Not a member? Join that Slack workspace here!