https://kotlinlang.org logo
Title
k

karn

04/02/2022, 12:11 AM
Hey folks, has anyone run into the case with the LazyColumn where mutating the backing list causes each item in the list to be recomposed? Attached is a video example which uses the compose highlighter to track recompositions. You'll notice that when I tap the item it adds a new item to the list but the entire list recomposes. Minimal example is in the thread below:
// Use recomposeHighlighter from:
// <https://github.com/android/snippets/blob/master/compose/recomposehighlighter/src/main/java/com/example/android/compose/recomposehighlighter/RecomposeHighlighter.kt>
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApplicationTheme {
                val mutableState = remember {
                    mutableStateOf(listOf(1, 2, 3))
                }

                LazyColumn(
                    state = rememberLazyListState(),
                    modifier = Modifier.fillMaxWidth()
                ) {

                    itemsIndexed(
                        items = mutableState.value,
                        key = { index: Int, item: Int -> item }
                    ) { index, item ->
                        Text(modifier = Modifier
                            .recomposeHighlighter()
                            .clickable {
                                mutableState.value = mutableState.value + Random.nextInt(3, 100000)
                            }, text = "$item")
                    }
                }
            }
        }
    }
}
m

myanmarking

04/02/2022, 12:52 AM
Yes thats expected because every item reads the state
Extract the mutableState to outside the lazycolumn, and use a lambda to update the value. It should fix the problem
d

Dan Yang

04/03/2022, 7:35 AM
Interesting question! I was also able to repro but I don't know enough about compose to describe why. I tried @myanmarking's suggestion but I don't know how you'd be able to extract the
mutableState
if it's used to back the `lazyColumn`'s collection. The closest I got was extracting the
mustableState
reference in the
clickable
but it still causes the whole list to recompose:
val mutableState = remember {
    mutableStateOf(listOf(1, 2, 3))
}

val textClickable = remember {
    {
        mutableState.value =
            mutableState.value + Random.nextInt(3, 100000)
    }
}

LazyColumn(
    state = rememberLazyListState(),
    modifier = Modifier.fillMaxWidth()
) {

    itemsIndexed(
        items = mutableState.value,
        key = { index: Int, item: Int -> item }
    ) { index, item ->
        Text(modifier = Modifier
            .recomposeHighlighter()
            .clickable(onClick = textClickable), text = "$item"
        )
    }
}
m

myanmarking

04/03/2022, 5:32 PM
Yes. I don’t know why the text get’s recomposed in your code above. But extracting the text composable to a separate one solves the problem. My guess is that List<*> is not stable, so it schedules the recompose on all the children, but for some reason, Text won’t skip when it should (because string is the same). Code below does that. Idk, maybe some expert can explain this
var mutableState: List<Int> by remember {
mutableStateOf(
listOf(1, 2, 3)
)
}
val onClick = {
mutableState = mutableState + Random.nextInt(3, 100000)
}
LazyColumn(
state = rememberLazyListState(),
modifier = Modifier.fillMaxWidth()
) {
itemsIndexed(
items = mutableState,
key = { _: Int, item: Int -> item }
) { _, item ->
MyText(
label = item.toString(),
onClick = onClick
)
}
}
@Composable
private fun MyText(
label: String,
onClick: () -> Unit
) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 5.dp)
.recomposeHighlighter()
.clickable(onClick = onClick),
text = label
)
}
👀 1
d

Dan Yang

04/04/2022, 4:08 PM
You're right! That's so interesting. I think this has to do
skippable
composables. From https://github.com/androidx/androidx/blob/androidx-main/compose/compiler/design/compiler-metrics.md#functions-that-are-restartable-but-not-skippable:
Skippability means that when called during recomposition, compose is able to skip the function if all of the parameters are equal.
When split out,
MyText
is
skippable
but perhaps
LazyColumn
is not skippable (maybe because of the
List<*>
) therefore the whole function body is run. I tried changing
MyText
to take a non-stable param and it started recomposing when I clicked on the text again.
data class Data(val item: Int)

@Composable
fun SpecialText(item: Data, textClickable: () -> Unit) {
    Text(modifier = Modifier
        .recomposeHighlighter()
        .clickable(onClick = textClickable), text = "$item"
    )
}