I have a list of rows where each row has a checkbox icon that can be clicked or not. I want to be able to have a user check the box and then my app should save that state. My current approach may be incorrect which is what is causing this transient bug.
Current Approach: I have a room db that stores the state of each item's checkbox status. Then, I display each row inside a lazy column composable. Each row's checkbox value is set initially with the current db item's status, then the user can change that value by clicking on the button. When the button is clicked I save the new value to a view model that holds all the current updated values and when the user saves their selections, the view model with all the updated values writes its values to the room db.
The row code looks like:
@Composable
fun RowItemDisplay(item: RowItem) {
val updateDataViewModel: UpdateDataViewModel = viewModel()
val checkBoxStatus = remember { mutableStateOf(
item.checkBoxStatus
) }
val color = if (checkBoxStatus.value) { Color.Black } else { Color.LightGray }
val icon = if (checkBoxStatus.value) { Icons.Filled.CheckCircle } else { Icons.Outlined.AddCircle }
Row(modifier = Modifier
.fillMaxWidth()
.size(40.dp)
) {
Text(
text = " ${item.name}",
color = Color.Black,
modifier = Modifier.align(CenterVertically)
)
Spacer(Modifier.weight(1f))
IconButton(onClick = {
checkBoxStatus.value = !(checkBoxStatus.value)
updateDataViewModel.updateMap[item.id] = checkBoxStatus.value
}) {
Icon(icon, tint = color, contentDescription = "Add",
modifier = Modifier
.align(CenterVertically)
.size(24.dp)
)
}
}
}
The area of issue here is the checkBoxStatus
. Sometimes the checkBoxStatus for a row is using the memory reference from another (based on debug print logs) which causes an erroneous display for the checkbox status. If I scroll away from the row and back to the row it recomposes and gets its own checkBoxStatus variable which shows the correct information.
I was expecting that each row would instantiate its own variables but as I have said, based on logs (the memory location for the variable is identical across rows), sometimes a row reuses another row's variable references. I am unsure how/why this would happen. Honestly, I feel my entire approach feels wrong. I tried to update the room db value directly in the IconButton onChange action but this fails to have the UI re-compose to show the current value.
LazyColumn (and RecyclerView) can reuse elements. It's known optimization which helps to render items faster. For example this reduces memory allocation if user scrolls list too fast.
However here it's not the reason of your problem. It seems that your bug is in line where you create checkBoxStatus
. When RowItemDisplay
called it reads value of current item and creates state. When user performs click only local state changes. Your item.checkBoxStatus
stays same. Changed occured only in checkBoxStatus
which is another variable. To fix this you can either keep State<Boolean>
in your RowItem
class instead of simply boolean, or in your click handler add
IconButton(onClick = {
checkBoxStatus.value = !(checkBoxStatus.value)
item.checkBoxStatus = !item.checkBoxStatus