Reputation: 67
I have a mutableTodosList
that I want to edit from my composables:
class ViewModelll : ViewModel() {
var mutableTodosList = mutableStateOf(emptyList<ToDoItemData>())
var todosList: List<ToDoItemData> = mutableTodosList.value
init { // One-time initialization
mutableTodosList.value = listOf(
ToDoItemData(
1, "Example 1",
),
ToDoItemData(2, "Create a todo list"),
// ... more items
)
}
fun addTodoItems(todoItems: List<ToDoItemData>) {
val newList = mutableTodosList.value.toMutableList().apply {
for (todoItem in todoItems) {
add(todoItem)
}
}
mutableTodosList.value = newList
}
fun modifyTodoItem(todoItemId: Int, todoItemParameterIndex: Int, modification: Any) {
val todoItem = findTodoItemById(todoItemId)
val newList = mutableTodosList.value.toMutableList().apply {
when (todoItemParameterIndex) {
0 -> this[indexOf(todoItem)].mainText = modification as String
12 -> this[indexOf(todoItem)].isChecked = modification as Boolean
else -> throw IllegalArgumentException("Invalid parameter index")
}
}
mutableTodosList.value = newList
}
fun removeTodoItem(todoItemId: Int) {
val newList = mutableTodosList.value.toMutableList().apply {
remove(findTodoItemById(todoItemId))
}
mutableTodosList.value = newList
}
}
This is my GoalsScreen
:
@Composable
fun GoalsScreen(navController: NavController) {
val viewModel: ViewModelll = viewModel()
Box(
modifier = Modifier.fillMaxSize(),
) {
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Top,
) {
ToDoList(
modifier = Modifier
.width(500.dp)
.height(200.dp),
todos = viewModel.mutableTodosList,
)
Text(
text = "Click this to check",
modifier = Modifier.clickable {
viewModel.modifyTodoItem(
todoItemId = 2,
todoItemParameterIndex = 12,
modification = !viewModel.mutableTodosList.value[1].isChecked,
)
},
)
Text("isChecked1: ${viewModel.mutableTodosList.value[0].isChecked}")
Text("isChecked2: ${viewModel.mutableTodosList.value[1].isChecked}")
}
}
}
@Composable
fun ToDoList(
modifier: Modifier,
todos: MutableState<List<ToDoItemData>> = remember { mutableStateOf(emptyList()) },
) {
Box(modifier = modifier) {
if (backgroundImage != null) {
Image(
modifier = Modifier.fillMaxSize(),
painter = backgroundImage,
contentDescription = null,
contentScale = ContentScale.Crop,
)
}
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(
todos.value
) { todo ->
ToDoItem(
todo = todo,
)
}
}
}
}
@Composable
fun GlassToDoItem(
todo: ToDoItemData,
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 4.dp, horizontal = 4.dp)
.clip(RoundedCornerShape(16.dp))
.clickable { },
verticalArrangement = Arrangement.Top,
) {
Row(
modifier = Modifier
.fillMaxSize(),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
CircularCheckbox(
todo = todo,
modifier = Modifier
.size(45.dp)
.padding(10.dp),
)
Spacer(modifier = Modifier.width(5.dp))
Text(
text = todo.mainText,
fontSize = 17.sp,
)
}
}
}
@Composable
fun ToDoItem(
todo: ToDoItemData,
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 4.dp, horizontal = 4.dp)
.clip(RoundedCornerShape(16.dp))
.background(Color.White)
.clickable { },
verticalArrangement = Arrangement.Top,
) {
Row(
modifier = Modifier
.fillMaxSize(),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
CircularCheckbox(
todo = todo,
modifier = Modifier
.size(45.dp)
.padding(10.dp),
)
Spacer(modifier = Modifier.width(5.dp))
Text(
text = todo.mainText,
fontSize = 17.sp,
)
}
}
}
@Composable
fun CircularCheckbox(
todo: ToDoItemData,
modifier: Modifier,
animationDurationMiliseconds: Int = 700,
unCheckedCircleColor: Color = Color.Blue,
unCheckedLineSize: Dp = 2.dp,
checkedCircleColor: Color = Color.Blue,
checkedLineSize: Dp = 2.dp,
checkmarkSize: Dp = 20.dp,
checkmarkColor: Color = Color.Blue,
) {
val viewModel: ViewModelll = viewModel()
Box(modifier = modifier)
{
if (todo.isChecked) {
CircularCheckedCheckbox(
modifier = Modifier
.fillMaxSize()
.clickable(
onClick = {
viewModel.modifyTodoItem(
todoItemId = todo.id,
todoItemParameterIndex = 12,
modification = !todo.isChecked,
)
}
),
circleColor = checkedCircleColor,
lineSize = checkedLineSize,
checkmarkSize = checkmarkSize,
checkmarkColor = checkmarkColor,
)
} else {
CircularUncheckedCheckbox(
modifier = Modifier
.fillMaxSize()
.clickable(
onClick = {
viewModel.modifyTodoItem(
todoItemId = todo.id,
todoItemParameterIndex = 12,
modification = !todo.isChecked,
)
}
),
circleColor = unCheckedCircleColor,
lineSize = unCheckedLineSize,
)
}
}
}
@Composable
fun CircularCheckedCheckbox(
modifier: Modifier,
circleColor: Color = Color.Blue,
lineSize: Dp = 2.dp,
checkmarkSize: Dp = 20.dp,
checkmarkColor: Color = Color.Blue,
) {
Canvas(modifier = modifier) {
val cSize = checkmarkSize.toPx()
val checkmark = Path().apply {
moveTo(cSize / 3f, cSize / 2f)
lineTo(cSize / 2f, cSize * 3f / 4f)
lineTo(cSize * 2f / 3f, cSize / 4f)
}
drawPath(
path = checkmark,
color = checkmarkColor,
)
drawArc(
color = circleColor,
startAngle = 0f,
sweepAngle = 360f,
useCenter = true,
style = Stroke(width = lineSize.toPx()),
)
}
}
@Composable
fun CircularUncheckedCheckbox(
modifier: Modifier,
circleColor: Color = Color.Blue,
lineSize: Dp = 2.dp,
) {
Canvas(modifier = modifier) {
drawArc(
color = circleColor,
startAngle = 0f,
sweepAngle = 360f,
useCenter = true,
style = Stroke(width = lineSize.toPx()),
)
}
}
data class ToDoItemData(
val id: Int,
var mainText: String,
var isChecked: Boolean = false,
)
The app does not update the isChecked value of the ToDoItemData
in the viewModel's mutableTodosList
to its opposite. Therefore this also doesn't update (re-compose) the UI to make the CircularCheckbox
change to a Checked or UnChecked version of itself, or so I think, from what I have observed through:
Debugging:
When the CircularCheckedCheckbox
or CircularUnCheckedCheckbox
are clicked:
The value of the isChecked value does not change for the ToDoItemData
in the viewModel's mutableTodosList
, however the isChecked value of todo of the CircularCheckbox
composable function does change, though it obviously doesn't update the UI as the todo is not a mutableStateOf, unlike the mutableTodosList
from the viewModel.
Debugging trough code: I added some additional Text elements to show the state of the isChecked value in the app:
Text(
"Click this to check",
modifier = Modifier.clickable {
viewModel.modifyTodoItem(
todoItemId = 2,
todoItemParameterIndex = 12,
modification = !viewModel.mutableTodosList.value[1].isChecked,
)
}
)
Text("isChecked1: ${viewModel.mutableTodosList.value[0].isChecked}")
Text("isChecked2: ${viewModel.mutableTodosList.value[1].isChecked}")
With this, the same result is shown as in the debug.
While searching for a solution to this problem I couldn't find any answers on the web, and all AI LLMs also don't know the answer.
Upvotes: 1
Views: 105
Reputation: 7278
This is a typical problem of mutable data structures in compose. When you reassign the same value to MutableState
, recomposition won't be scheduled. From the docs:
If
value
is written to with the same value, no recompositions will be scheduled.
By default, StructuralEqualityPolicy
is used to compare the old and new value. You can change that when calling mutableStateOf()
(but don't do that if you don't have a good reason to).
When you do this:
val newList = mutableTodosList.value.toMutableList().apply {
this[index].mainText = modification
}
you create a new instance of the list with the same objects inside. When you add/remove an item from this list, the original list is not affected. But when you modify an item in this list, the item in the original list is also modified (it's the same item). This means that the original and new list are still structurally equal and thus recomposition is not scheduled.
You should instead create a copy of ToDoItemData
with your modifications. The best way to do this is by using a data class
. It could look like this:
data class ToDoItemData(val mainText: String)
val list = mutableStateOf(listOf(ToDoItemData("original")))
val newList = list.value.toMutableList().apply {
this[0] = this[0].copy(mainText = "modified")
}
list.value = newList
With this, the original list keeps the original instance of ToDoItemData
and the new list has a new instance of ToDoItemData
with changed mainText
, so the lists are not structurally equal.
Upvotes: 1