Jetpack Compose LazyColumn recomposition with remember()

Ive been trying out Jetpack Compose and ran into something with the LazyColumn list and remember().

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

@Composable
fun MyApp(content: @Composable () -> Unit){
    ComposeTestTheme {
        // A surface container using the 'background' color from the theme
        Surface(color = MaterialTheme.colors.background) {
            content()
        }
    }
}

@Composable
fun MyScreen( names: List<String> = List(1000) {"Poofy #$it"}) {
    NameList( names, Modifier.fillMaxHeight())
}

@Composable
fun NameList( names: List<String>, modifier: Modifier = Modifier ){
    LazyColumn( modifier = modifier ){
        items( items = names ) { name ->
            val counter = remember{ mutableStateOf(0) }

            Row(){
                Text(text = "Hello $name")
                Counter(
                    count = counter.value,
                    updateCount = { newCount -> counter.value = newCount } )
            }
            Divider(color = Color.Black)
        }
    }
}

@Composable
fun Counter(count: Int, updateCount: (Int) -> Unit) {
    Button( onClick = {updateCount(count+1)} ){
        Text("Clicked $count times")
    }
}

This runs and creates a list of 1000 rows where each row says “Hello Poofy #N” followed by a button that says “Clicked N times”.

It all works fine but if I click a button to update its count that count will not persist when it is scrolled offscreen and back on.

The LazyColumn “recycling” recomposes the row and the count. In the above sample the counter is hoisted up into NameList() but I have tried it unhoisted in Counter(). Neither works.

What is the proper way to remember the count? Must I store it in an array in the activity or something?

Answer

The representations for items are recycled, and with the new index the value of remember is reset. This is expected behavior, and you should not expect this value to persist.

You don’t need to keep it in the activity, you just need to move it out of the LazyColumn. For example, you can store it in a mutable state list, as shown here:

val counters = remember { names.map { 0 }.toMutableStateList() }
LazyColumn( modifier = modifier ){
    itemsIndexed(items = names) { i, name ->
        Row(){
            Text(text = "Hello $name")
            Counter(
                count = counters[i],
                updateCount = { newCount -> counters[i] = newCount } )
        }
        Divider(color = Color.Black)
    }
}

Or in a mutable state map:

val counters = remember { mutableStateMapOf<Int, Int>() }
LazyColumn( modifier = modifier ){
    itemsIndexed(items = names) { i, name ->
        Row(){
            Text(text = "Hello $name")
            Counter(
                count = counters[i] ?: 0,
                updateCount = { newCount -> counters[i] = newCount } )
        }
        Divider(color = Color.Black)
    }
}

Note that remember will also be reset when screen rotates, consider using rememberSaveable instead of storing the data inside a view model.

Read more about state in Compose in documentation