Hey, I'm trying to create a simple to-do app, but ...
# compose
a
Hey, I'm trying to create a simple to-do app, but I'm facing a state-related issue. There are 2 separate LazyColumns: One for completed tasks, another for incomplete. The data itself is stored in Room, but I'm also using MutableState for each item's TextField and CheckBox state. The 2 LazyColumns are recycling items' Mutable State (I think), even after retrieving updated lists from the database. Any pieces of advice? 😐
n
first, you don't need 2 Lazy Columns, just use the same :
Copy code
LazyColumn {
   item {
      Header("incomplete")
   }
   items {}
   item {
      Header("complete")
   }
   items {}
}
💡 3
for your issue we need your data model/code because it's probably the issue something like this :
Copy code
data class TodoItem(val completed: Boolean, val text: String)
be sure to use val and not var (don't modify your inner data basically) as it might be the issue
💡 3
a
Thanks a ton! @nitrog42 I'll make these changes and update things here :)
So, I had refactored both the lists into one lazy column as you said. And that cleans up the code a bit. The issue is still there, though. Your 2nd suggestion is to use read-only values in my data class. Here's what my data class looks like right now -> TodoItem
Copy code
@Entity(tableName = "todo_items")
data class TodoItem(
    @PrimaryKey(autoGenerate = true) val id: Int,
    @ColumnInfo var task: String,
    @ColumnInfo(name = "is_done") var isDone: Boolean
)
Because of the auto-generated primary keys, I'm simply updating the objects via database calls. The database gets updated fine. But each item's mutable state (inside of the lazy column) is not refreshed as required... causing problems. Here's the LazyColumn, and the nested Stateful Composable -> LazyColumn
Copy code
@Composable
fun TodoItemLists(
    incompleteTodos: List<TodoItem>,
    completeTodos: List<TodoItem>,
    onTaskChange: (TodoItem, String) -> Unit,
    onDoneChange: (TodoItem, Boolean) -> Unit,
    onDeleteTodo: (TodoItem) -> Unit,
    modifier: Modifier = Modifier
) {
    LazyColumn(modifier = modifier) {
        item {
            ListHeading("Incomplete")
        }
        items(items = incompleteTodos, key = { it.id }) { todoItem ->

            DismissibleStateful(onDismiss = { onDeleteTodo(todoItem) }) {
                TodoItemRowStateful(
                    task = todoItem.task,
                    onTaskChange = { updatedTask ->
                        onTaskChange(todoItem, updatedTask)
                    },
                    isDone = todoItem.isDone,
                    onDoneChange = { isDoneUpdated ->
                        onDoneChange(todoItem, isDoneUpdated)
                    }
                )
            }
        }
        item {
            ListHeading("Complete")
        }
        items(items = completeTodos, key = { it.id }) { todoItem ->

            DismissibleStateful(onDismiss = { onDeleteTodo(todoItem) }) {
                TodoItemRowStateful(
                    task = todoItem.task,
                    onTaskChange = { updatedTask ->
                        onTaskChange(todoItem, updatedTask)
                    },
                    isDone = todoItem.isDone,
                    onDoneChange = { isDoneUpdated ->
                        onDoneChange(todoItem, isDoneUpdated)
                    }
                )
            }
        }
    }
}
TodoItemRowStateful
Copy code
@Composable
fun TodoItemRowStateful(
    task: String,
    onTaskChange: (String) -> Unit,
    isDone: Boolean,
    onDoneChange: (Boolean) -> Unit
) {

    // Duplicated state for the UI to remember
    val (taskNameInUi, setTaskNameInUi) = rememberSaveable { mutableStateOf(task) }
    val (isTaskDoneInUi, setTaskDoneInUi) = rememberSaveable { mutableStateOf(isDone) }

    TodoItemRow(
        task = taskNameInUi,
        onTaskChange = { taskNameInUiUpdated ->
            setTaskNameInUi(taskNameInUiUpdated)
            onTaskChange(taskNameInUiUpdated)
        },
        isDone = isTaskDoneInUi,
        onDoneChange = { isTaskDoneInUiUpdated ->
            setTaskDoneInUi(isTaskDoneInUiUpdated)
            onDoneChange(isTaskDoneInUiUpdated)
        }
    )
}
Sorry for dumping so much code. I've just been stuck at this step for a while. 😅
n
You need to use val instead of var for your TodoItem
That's your problem right here
a
Can you pls help me understand why that is so?
n
Use .copy on those item to make a new instance with the task or is done changed Then you can insert it back in db (or use it directly)
👀 1
a
I believe inside my ViewModel, I'm applying a similar strategy. I'll check tomorrow, and provide an update. Thanks 😄
👍 1
Yep. Changed var to val, refactored 2 lines. No effect. Let me try to explain everything that's happening. • LazyColumn has 2 lists of item. • Each item is essentially a Checkbox and a TextField, and their MutableStates. • Whenever a TextField is modified, I update the state as well as the database. • Whenever a Checkbox is toggled, I update the state as well as the database. • I expect that toggling a checkbox would simply shift the item to the "other" list. Since an item is either completed or incomplete. • As expected, the items always change their list. But their state (CheckBox and TextField) go nuts. As if from a completely different item. • My guess is that I'm not handling state inside lazy column as I'm supposed to.
n
ok I was on my phone the previous time try to remove completly your "UI" state duplication :
Copy code
// Duplicated state for the UI to remember
    val (taskNameInUi, setTaskNameInUi) = rememberSaveable { mutableStateOf(task) }
    val (isTaskDoneInUi, setTaskDoneInUi) = rememberSaveable { mutableStateOf(isDone) }
use directly your todoItem attributes (task/isDone)
👍 1
if it still doesn't work, can you show "above" the lazycolumn ? like what
Copy code
onTaskChange: (TodoItem, String) -> Unit,
    onDoneChange: (TodoItem, Boolean) -> Unit,
are doing ?
a
I had previously tried removing the UI state. The items then snap back to their list as expected. But without any UI state, I can't update the UI. The textfield stays empty and checkbox also does not change (before the list swap)
I'll retry all of these and get back to you later.
n
if you can, try to post your app on github or something so we can look at it in whole
a
Absolutely, here you go: https://github.com/adizcode/Todo-Tada/tree/improve-ui Please make sure you're on the 'improve-ui' branch
n
here is what I would do : switch to val :
Copy code
@Entity(tableName = "todo_items")
data class TodoItem(
    @PrimaryKey(autoGenerate = true) val id: Int,
    @ColumnInfo val task: String,
    @ColumnInfo(name = "is_done") val isDone: Boolean
)
use copy in viewModel :
Copy code
fun updateTodoTask(todoItem: TodoItem, task: String) {
    updateTodo(todoItem.copy(task = task))
}

fun updateTodoDone(todoItem: TodoItem, isDone: Boolean) {
    updateTodo(todoItem.copy(isDone = isDone))
}
and then to the issue part : rememberSaveable is probably the root cause of the difference you see in UI. I would disable it for isDone (you can use directly the database for that), but for the task name, as the keyboard experience is something requiring a good performance, I would avoid updating the database immediatly on each character change, but instead after a delay of typing. it looks like that :
Copy code
@Composable
fun TodoItemRowStateful(
        task: String,
        onTaskChange: (String) -> Unit,
        isDone: Boolean,
        onDoneChange: (Boolean) -> Unit
) {
    // Duplicated state for the UI to remember
    val (taskNameInUi, setTaskNameInUi) = rememberSaveable(task) { mutableStateOf(task) }
    LaunchedEffect(taskNameInUi) {
        delay(300)
        onTaskChange(taskNameInUi)
    }

    TodoItemRow(
            task = taskNameInUi,
            onTaskChange = { taskNameInUiUpdated ->
                setTaskNameInUi(taskNameInUiUpdated)
            },
            isDone = isDone,
            onDoneChange = { isTaskDoneInUiUpdated ->
                onDoneChange(isTaskDoneInUiUpdated)
            }
    )
}
🔥 1
K 1
💡 1
EDIT actually forget about rememberSaveable here as it still cause issue if you change the name (change a task name then done/undone, it will swap between two names...) just use :
Copy code
val (taskNameInUi, setTaskNameInUi) = remember(task) { mutableStateOf(task) }
🙏 1
a
I can't believe my eyes. Simply removing the "Saveable" from "rememberSaveable" magically solved the issue. Thank you so much. I'm glad. But. What is going on?? What did I miss about rememberSaveable? It's only supposed to survive configuration changes, and uses Bundles for that I guess. Are those Bundles causing it to react slowly? Any reading material would be highly appreciated.
Once again, thanks a ton for going out of your way to help 😄
n
unfortunatelly I don't have many things to say about the "why" here (a bug https://issuetracker.google.com/issues/152014032, and the things I reported about this : https://issuetracker.google.com/issues/197661845), but know it won't matters in your app as anything you change is directly updated in your database
1
a
Understood. And yes, apparently I was just misusing rememberSaveable and it cost me. Lucky to be a part of this slack channel!