Home > Mobile >  LazyList's item key as LazyDSL vs LazyList's item key as composable key
LazyList's item key as LazyDSL vs LazyList's item key as composable key

Time:06-06

I have a Todo list laid out using LazyColumn, and each item is another Composable (CardView) that handles each TodoModel. I'm using rememberSaveable to manage the state of this CardView's own composable (e.g TextField, clickable images etc), so as to also survive orientation changes.

I have learned to use LazyList's key to tell the structure that each item is unique, in this case I'm using the model's id.

The way this todolist works is its a single screen that shows a list of saved todo (saved by means of persisting via room), and a simple FAB button that when clicked, it adds a temporary todo item(a new todo model with an id of -1L) in the list (it will show on top of the todos in the screen), this is where a user can edit the title and the content of the todo. To summarize, you can delete/edit/create new Todo in a single screen, all saved Todo has positive ID's and for a new one I always set it to -1. Once saved, the id of the new todo model which is -1L will be modified in a list (id that is returned by Room) hoisted in a view model of the entire screen (DashboardScreen's ViewModel)

The thing I noticed is when I define a key in the lazylist this way (LazyDSL)

 LazyColumn {
    items(key = { it.id },items = todoList) { todo ->
            TodoCardItem(
                todoModel = todo,
                ...,
                ...

and with the following sequence of actions

  • Click Fab, New todo (id of -1L), empty title
  • Save this new Todo (once saved, id will be modified by a positive number), title = "My First Todo"
  • Click Fab, New Todo (id of -1L), title is not empty, displays = "My First Todo"

it references the old state of the card.

But when I define the key this way (Composable key)

LazyColumn {
    items(items = todoList) { todo ->
        key (todo.id) {
            TodoCardItem(
                todoModel = todo, 
                ...,
                ...

it works as I expect it to, every new todo is entirely clean, also it survives configuration changes such as rotation (though Im crashing when I minimize the app, a thing I need to deal with later).

for the rememberSaveable, it is initialized with a TodoModel

.....
companion object {
    val Saver = Saver<TodoItemState, Map<String, Any>>(
        save = { instanceOfOriginal ->
            mapOf(
                KEY_MODEL to instanceOfOriginal.todoModel,
                KEY_TITLE to instanceOfOriginal.title
            )
        },
        restore = { restorer ->
            val state = TodoItemState(restorer[KEY_MODEL] as TodoModel)
            state.title = restorer[KEY_TITLE] as String
            state
        }
    )

    private const val KEY_MODEL = "key_model"
    private const val KEY_TITLE = "key_title"
   }
 }

@Composable
 internal fun rememberTodoItemState(todoModel: TodoModel) = 
 rememberSaveable(
        saver = TodoItemState.Saver
 ) {
     TodoItemState(todoModel)
 }

It's a big code so if there are further concerns about some part of it like showing the viewmodel code, how the model's id is modified, I'll paste it upon further questions.

CodePudding user response:

By doing this:

items(items = todoList) { todo ->
    key (todo.id) {

you're not only resetting the -1L view, but also all your other cells below the inserted one.

When you're not specifying key, by default it's equal to cell index, so there're kind of two keys on inside an other one, and when the index shifts all the inner keys become invalid and had to create a new view. You can check this behaviour by adding LaunchedEffect with a log inside your key.

I think the most correct way it creating a unique key for each new item, and store a map with new item ids: this will make sure that the tmp cell was reused with the new content.

private val newItemIdsMap = mutableMapOf<Long, Long>()

fun addTmpItem() {
    var tmpId: Long
    do {
        tmpId = Random.nextLong(Long.MIN_VALUE, -1)
    } while (newItemIdsMap.containsValue(tmpId))
    val tmpItem = Item(id = tmpId)
    items.add(0, tmpItem)
}

fun saveItem(tmpItem: Item) {
    val savedItem = saveToDb(tmpItem)
    newItemIdsMap[savedItem.id] = tmpItem.id
    items[0] = savedItem
}

fun getItemId(item: Item) : Long {
    val savedId = newItemIdsMap[item.id]
    return savedId ?: item.id
}
  • Related