Hi, got an issue with LazyList when I scroll the `...
# compose-android
a
Hi, got an issue with LazyList when I scroll the
n-1
item slightly up and down(
n-1
item is partially visible at the bottom and doing up and down doesn’t insert or remove any visible items in the viewport) then
nth
item reattaches at each scroll(event nth item is not visible in the UI as it is below
n-1
item) Attached the code and video in the stack-trace
Code to repro this
Copy code
package com.example.composelistrecomposition

import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.example.composelistrecomposition.ui.theme.ComposeListRecompositionTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeListRecompositionTheme {
                Greeting()
            }
        }
    }
}

private val randomList = buildList {
    repeat(4) {
        add(
            if (it % 2 == 0) {
                AChild1(
                    it,
                    "${it}_c1"
                )
            } else {
                AChild2("${it}_c2")
            }
        )
        if (it % 2 == 0) {
            add(
                AChild3("${it}_c3")
            )
        }
    }
}

@Composable
private fun Greeting() {
    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        contentPadding = PaddingValues(vertical = 16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.Bottom),
    ) {
        items(
            items = randomList,
            contentType = {
                it.type
            },
            key = {
                it.uId
            }
        ) { item ->
            AbstractLayout(item)
        }
    }
}

@Composable
private fun AbstractLayout(item: UiModel) {
    when (item) {
        is AChild1 -> {
            Text(text = item.toString().repeat(20), modifier = Modifier.border(1.dp, Color.Red))
        }

        is AChild2 -> {
            // put debug here
            Column(modifier = Modifier.border(1.dp, Color.Blue)) {
                val context = LocalContext.current
                LaunchedEffect(key1 = Unit) {
                    Toast.makeText(context, "LaunchedEffect called for ${item.uId}", Toast.LENGTH_SHORT)
                        .show()
                    Log.d("ListTag", "LaunchedEffect called for  ${item.uId}")
                }
                Text(text = "This should not recompose")
                Spacer(modifier = Modifier.height(10.dp))
                Text(text = item.toString().repeat(10))
            }
        }

        is AChild3 -> {
            Surface(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(100.dp), color = Color.Green
            ) {

            }
        }
    }
}
Compose bom version:
2024.04.01
s
What's the unexpected behavior here? That the item which is not yet on the screen is pre-fetched a bit before it comes to the viewport? Do Lazy layouts give any promise that their children won't start composing when they are not yet in the viewport? I would expect them to get attached a bit before they are in the actual viewport tbh.
a
Unexpected is that whenever I scroll a bit up or down they again gets recreated. Even when they come to viewport they again gets created. Currently I am doing some calculation in
remember
(not shown in the code snippet) block so that also gets retrigger at each scroll delta and I don’t want to re do the calculation
pre-fetching should be done only once for one unique item not multiple times. Right? Or is there smth which I am not getting?
s
I could definitely be wrong, but what I see here is that it loads the item when it's about to come to the viewport, then you are leaving far enough from it that it considers that it no longer needs it so it disposes it, and so on. What would you want to happen here instead?
a
Ideally once calculated it should keep it in memory and create a LRU cache type of thing. Also added one more video where I am just going downwards(with stops) then also it is creating multiple composables
a
We had noticed too when you scroll up a bit and down in a lazy layout any remembers are lost. Essentially you need to remember saveable that state
Lazy layouts reuse and don’t preserve the state of the composables when they go off screen
a
Should we create an issue for this? Or is this already in the radar?
a
I might be wrong but I believe this is WAI.
rememberSaveable
might help.
a
rememberSaveable
yes this might help but I want to highlight the issue that prefetching is not working correctly.
a
Well, the same thing happens on navigation. When you navigate to another screen, all `remember`ed state is lost. I would prefetch in a VM or similar.
HorizontalPager works the same, I guess.
You can try submitting an issue if you think this is something that should be improved: https://issuetracker.google.com/issues/new?component=856989&template=1425922
a
Yes that I know.. but if you see the video at https://kotlinlang.slack.com/archives/C04TPPEQKEJ/p1715344106436269?thread_ts=1715341704.351489&cid=C04TPPEQKEJ you will find that it is prefetching the next same item multiple times
a
In the original message you mentioned that you slightly scroll up and down, which suggests that the nth item is repeatedly moving above and below the threshold, which causes it to enter and leave the composition. This looks like WAI. However, on the video you just scroll in one direction. I would expect the item to be attached only once in that particular case. But there might be more that I can't see straightaway. Maybe you have duplicated items, keys, or something like that.
a
There is also video of slightly scrolling up and down, when Stylianos mentioned due to coming and going away from the next item LazyList might be prefetching and then dropping the item. Then I tried the second scenario. I checked
key
is different in all cases(actually
LazyList
throws when you use same id twice). For content type I am using the class name of the class BTW what is WAI 😅
s
"Working As Intended"
thank you color 1