0

I'm learning compose and as an exercise I'm trying to show a list of countdown timers. One button will add a new timer to the list, then for every timer on that list a lazycolumn will show a row with a text displaying the time of the countdown and a button to start the countdown.

Here i added three timers to the list and they all show up just fine: Before

Then when i start the countdown all the other rows get updated even though they are not being changed: After

My viewmodel:

data class MyClass(val text: String, val isClickable: Boolean)

class VM : ViewModel() {
    val myList = mutableStateListOf<MyClass>()

    fun addTimer() {
        myList.add(myList.size, MyClass("0", true))
    }

    fun changeText(index: Int, tempo: String) {
        myList[index] = myList[index].copy(text = tempo)
    }

    fun changeIsClickable(index: Int, click: Boolean) {
        myList[index] = myList[index].copy(isClickable = click)
    }
}

My composable screen:

val viewModel: VM by viewModels()
(...)
@Composable
fun MainScreen(viewModel: VM) {
    val scope = CoroutineScope(Dispatchers.Main)
    Column(verticalArrangement = Arrangement.spacedBy(15.dp)) {
        Row() {
                Button(onClick = { viewModel.addTimer() }
                ) {
                    Text(text = "Add Timer")
                }
        }
        LazyColumn {
            itemsIndexed(viewModel.myList) { index, list ->
                Row(modifier = Modifier.border(8.dp, randomColor())) {
                    Text(
                        text = list.text, modifier = Modifier
                            .padding(20.dp)
                    )
                    Button(
                        enabled = list.isClickable,
                        onClick = {
                            viewModel.changeIsClickable(index, false)
                            var currTime = Calendar.getInstance().timeInMillis
                            val endTime =
                                currTime + (7000L..20000L).random()  //random between 7 and 20
                            scope.launch {
                                while (endTime > currTime) {
                                    delay(100)
                                    val timeTemporary =
                                        ((endTime - currTime) / 1000).toString() + "\n" + "Secs"
                                    viewModel.changeText(index, timeTemporary)
                                    currTime = Calendar.getInstance().timeInMillis
                                }
                                viewModel.changeText(index, "Done")
                                viewModel.changeIsClickable(index, true)
                            }
                        }) {
                        Text(text = "Start")
                    }
                }
            }
        }
    }
}

I've tried many suggestions like this one and this one (hence the colorful borders to check recomposing) but I cannot get the untouched rows not to update. What am I missing? How can I stop lazycolumn from updating when it shouldn't?

Gui
  • 31
  • 2
  • Change `val scope = CoroutineScope(Dispatchers.Main)` to `val scope = rememberCoroutineScope()` and provide item keys as described in the first link in your question. – bylazy Jun 13 '23 at 18:34
  • @bylazy I tried but the result was the same, every row recomposes if one is changed. – Gui Jun 14 '23 at 18:42

1 Answers1

0

Add the item to a separate composable. Each composable becomes skippable or not depending on the parameters of the composable function. In this case, you have the index and the item as parameters. Both don't change when the timer isn't running and because both parameters don't change, the composable is skipped during the recomposition.

@Composable
fun StackOverflowScreen() {
    Column(verticalArrangement = Arrangement.spacedBy(15.dp)) {
        Row() {
            Button(onClick = { viewModel.addTimer() }
            ) {
                Text(text = "Add Timer")
            }
        }
        LazyColumn {
            itemsIndexed(viewModel.myList) { index, list ->
                TimerItem(index, list)
            }
        }
    }
}

@Composable
private fun TimerItem(index: Int, list: MyClass) {
    val scope = CoroutineScope(Dispatchers.Main)
    Row(modifier = Modifier.border(8.dp, randomColor())) {
        Text(
            text = list.text, modifier = Modifier
                .padding(20.dp)
        )
        Button(
            enabled = list.isClickable,
            onClick = {
                viewModel.changeIsClickable(index, false)
                var currTime = Calendar.getInstance().timeInMillis
                val endTime =
                    currTime + (7000L..20000L).random()  //random between 7 and 20
                scope.launch {
                    while (endTime > currTime) {
                        delay(100)
                        val timeTemporary =
                            ((endTime - currTime) / 1000).toString() + "\n" + "Secs"
                        viewModel.changeText(index, timeTemporary)
                        currTime = Calendar.getInstance().timeInMillis
                    }
                    viewModel.changeText(index, "Done")
                    viewModel.changeIsClickable(index, true)
                }
            }) {
            Text(text = "Start")
        }
    }
}
Huib
  • 214
  • 1
  • 8
  • I tried but got the same result, all "timerItem"s recompose when I change the time on one. – Gui Jun 14 '23 at 18:40
  • hmmm, I'm curious where the difference in lies, because when i run the sample that i provided, the layout inspector shows that all items except the active timer are skipped. What versions are you using? – Huib Jun 20 '23 at 10:14
  • -Electric eel 2022.1.1 -Physical phones with android 8 and 13 -Compose ui version 1.4.3 -Activity compose version 1.7.2 -Lifecycle viewmodel compose version 2.6.1 -Activity ktx version 1.7.2 – Gui Jun 20 '23 at 13:03
  • I've been trying many things since and I got it to only recompose one row when my "class VM : ViewModel()" is in my MainActivity.kt. If i move it to my viewmodel file where I have the data class it recomposes all. I also installed android studio hedgehog to try and check what was triggering the recomposition with [this](https://developer.android.com/studio/preview/features#compose-state-in-debugger) feature and saw that the parameters of the rows i'm not touching are all marked as Unchanged yet still recompose – Gui Jun 20 '23 at 13:10