Jetpack Compose LazyColumn - How to update values of each Item seperately?
Asked Answered
R

2

1

I'm working on a shopping cart feature for my app. I'm looking to add/decrease quantity of each list item in LazyColumn individually. I'm using only one "remember" so if I click on add/decrease they all update at the same time. How do I control each Item individually?

Screenshot

@Composable
fun InventoryCartScreen(
    mainViewModel: MainViewModel = hiltViewModel()
) {

    val multiSelectValue = mutableStateOf(0)// This is the value I want to change

//random list
val shopList = listOf(
        ShoppingList(id = 0,itemNumber = "1",itemDescription = "1",currentInventory = 0,optimalInventory = 0,minInventory = 0),
        ShoppingList(id = 0,itemNumber = "2",itemDescription = "2",currentInventory = 0,optimalInventory = 0,minInventory = 0)
    )

    Column(...) {
       LazyColumn(...) {
            items(items = shopList, key = { it.id }) { item ->
                InventoryCartScreenContents(
                    onaddClick= { multiSelectValue.value ++ }, //adds for all
                    onDecreaseClick = { multiSelectValue.value -- }, //decreases for all
                    value = multiSelectValue.value //setting the initial value for all
                )

            }

        }
    }
}

Below is the contents composable to help you reproduce the issue.

@Composable
fun InventoryCartScreenContents(
    onAddClick: (Int) -> Unit,
    onDecreaseClick: () -> Unit,
    value: Int,
) {
           Row(...) {
                Button(
                    onClick = { onAddClick(itemId) }
                ) {
                    Text(text = "+")
                }
               
                Button(
                    onClick = onDecreaseClick
                ) {
                    Text(text = "-")
                }
               
                }

            }

Robotize answered 10/4, 2022 at 13:50 Comment(0)
M
13

Create a mutableStateListOf(...) (or mutableStateOf(listOf(...)) object if the former does not support your data type) in your ViewModel. Now, access this state from the composable you wish to read it from, i.e., your LazyColumn.

Inside your ViewModel, you can set the values however you wish, and they will be updated in the Composable as and when they are updated. Now, the composable, i.e., you column could use the index of the list item as the index of the lazycolumn's item. So you can store different data for different items in the viewmodel and it'll work fine.

The problem seems to be that you are missing the concept of state-hoisting here. I thought I had some good posts explaining it but it seems this one's the best I've posted. Anyway, I recommend checking this official reference, since that's where I basically found that (with a little bit of headhunting, so to speak.)

The basic idea is that everything is stored in the viewModel. Then, you divide that state into "setters" and "getters" and pass them down the heirarchy.

For example, the ViewModel may have a item called text, ok?

class VM : ViewModel(){
 var text by mutableStateOf("Initial Text")
 private set // This ensures that it cannot be directly modified by any class other than the viewmodel
 fun setText(newText: Dtring){ // Setter
  text = newText
 }
}

If you wish to update the value of text on the click of a button, this is how you will hook up that button with the viewModel

MainActivity{
 onCreate{
  setContent{
   StateButton(
    getter = viewModel.text, // To get the value
    setter = viewModel::setText  // Passing the setter directly
   )
  }
 }
}

In your Button Composable declaration

@Composable
private fun ComposeButton(
getter: String,
setter: (String) -> Unit // (receive 'String') -> return 'Unit' or 'void'
){
 Button(
 onClick = { setter("Updated Text") } // Calls the setter with value "Updated Text", updating the value in the viewModel
 ){
   Text(text = getter)
 }
}

This button reads the value 'get' from the viewModel, i.e., it reads the value of text, as it is passed down the model to the composable. Then, when we receive a click event from the button (in the onClick), we invoke the setter that we received as a parameter to the Composable, and we invoke it with a new value, in this case "Updated Text", this will go upwards all the way to the viewModel, and change the value of text in there.

Now, text was originally initialized as a state-holder, by using mutableStateOf(...) in initialization, which will trigger recompositions upon its value being changed. Now, since the value actually did change (from "Initial Text" to "Updated Text"), recompositions will be triggered on all the Composables which read the value of the text variable. Now, our ComposeButton Composable does indeed read the value of text, since we are directly passing it to the getter parameter of that Composable, that right? Hence, all of this will result in a Composable, that will read a value from a single point in the heirarchy (the viewmodel), and only update when that value changes. The Viewmodel, therefore, acts as a single source of truth for the Composable(s).

What you'll get when you run this is a Composable that reads "Initial Text" at first, but when you click it, it changes to "Updated Text". We are connecting the Composables to the main viewModel with the help of getters and setters. When Composables receive events, we invoke setters we receive from the models, and those setters continue the chain up to the viewModel, and change the value of the variables (state-holder) inside the model. Then, the Composables are already reading those variables through 'getters', hence, they recompose to reflect the updated value. This is what state-hoisting is. All the state is stored in the viewModel, and is passed down to the Composables. When a value needs to change, the Composables pass 'events' up to the viewModel (up the heirarchy), and then upon the updating of the value, the updated state is passed down to the Composables ('state' flows down the heirarchy).

All you need, is to use this method, but with a list. You can keep track of the items by using their index, and update their properties in the viewModel like this example demonstrates updating the value of a. You can store all the properties of an item in a single list.

Just create a data-class, like so

data class Item(p1: ..., p2:..., p3 : ...){}

Then, val list by mutableStateOf(listOf<Item>())

Clear?

Ok here is the explanation SPECIFIC to your use-case.

Your code seems excessively large but here's what I got down:

You have two items in a lazycolumn, both of them have three buttons each. Your question is about two buttons, increase and decrease. You wish to have the buttons modify the properties of only the item they belong to, right?

Ok so again, use state-hoisting as explained above

ViewModel{
 val list by mutableStateOf(listOf<Item>()) // Main (central) list
 val updateItem(itemIndex: Int, item: Item){
   list.set(itemIndex, item) // sets the element at 'itemIndex' to 'item' 
 } // You can fill the list with initial values if you like, for testing
}

Getters and Setters being created, you will use them to read and update ENTIRE ITEMS, even if you have to modify a single property of them. We can use convenience methods like copy() to make our lives easier here.

MainScreen(){
 //Assuming here is the LazyColumn
 list = viewModel.list // Get the central list here
 LazyColumn(...){ // Try to minimize code like this in your questions, since it does not have to compile, just an illustration.
  items(list){ item ->
   // Ditch the multiSelectValue, we have the state in the model now.
   Item( // A Composable, like you demonstrated, but with a simpler name for convenience
     item: Item,
     onItemUpdate: (Item) -> Unit // You don't need anything else
   )
  }
 }
}

Just create a data class and store everything in there. The class Item (not the Composable) I've demonstrated is the data-class. It could have a value like so

data class Item(var quantity: Int, ...){} // We will increase/decrease this

Now, in your Item Composable, where you receive the 'add' event (in the onClick of the 'Add' Button), just update the value using the setter like this

onClick = {
 updateItem(
  item // Getter
      .copy( //Convenience Method
        quantity = item.quantity + 1 // Increase current Quantity
      )
 )
}

Just do the same for decrease and delete (updateItem(0)), and you should have accomplished this well... Finally. If you have any more doubts, just ask. I could assist over facetime if nothing works.

Maggard answered 10/4, 2022 at 13:57 Comment(8)
Hi @Marsk, do you have an example code maybe? Not fully understanding your answer. MutableStateListOf - "of what?". So you can store different data for different items in the viewmodel - How?Robotize
mutableStateListOf<T>() creates a list object of containing items of type T. You can use it like val itemsList = mutableStateListOf<Item>(). Then, you when users interact with an item, pass that item to the viewModel using state-hoisting. All done!Maggard
Thank you so much for your time and effort. I'm Looking into it and will update if I am succesful.Robotize
Read the updated explanation and you should be up and running.Maggard
I understood the getters/setters and able to make it work, thanks! But I just don't, for the love of me, understand how I can use a list to separate the add/increase for each individual item. This is what I added in VM and setting it to my buttons in the composable. How do I tell that when I click on add on item 1, it only does the update for item 1? var updateInventoryBy by mutableStateOf(0) private set fun addValue(newValue: Int){ updateInventoryBy = newValue.plus(1) } fun decreaseValue(newValue: Int){ updateInventoryBy = newValue.minus(1) }Robotize
Hi @Marks, to be honest, I'm stuck at the first step you've suggested "val updateItem(itemIndex: Int, item: Item)" Property getter or setter expected, and cannot do list.set (Function declaration must have a name). I'm not giving up and will keep working at it. This is my first project and I'm trying to implement as much features as I can while I learn. Already implemented the item selection thanks to you from a different thread, you are the best. I'll reach out if anything, wouldn't want to waste your time on face time :)Robotize
Sorry it was a function, my bad. Must've missed it in all the blocks of codeMaggard
Just like the text example above,Maggard
R
1

Based on @MARSK answer I've managed to achieve the goal (Thank you!)

Add a function to update the items value:

//Creating a function to update a certain item with a new value

fun updateShoppingItem(shoppingItem: ShoppingList, newValue: Int) {
        val newInventoryMultiValue = shoppingItem.copy(currentInventory = shoppingItem.currentInventory.plus(newValue))
        updateShoppingList(newInventoryMultiValue)
    }

//Actually updating the room item with the function above

private fun updateShoppingList(shoppingItem: ShoppingList) {
        viewModelScope.launch {
            repository.updateShoppingItem(shoppingItem)
        }
    }

Then, in the Composable screen add the functions to the add and decrease buttons

val shoppingList by mainViewModel.getShoppingList.collectAsState(initial = emptyList())

LazyColumn() {

            items(items = shoppingList)
            { item ->
                     
                    InventoryCartScreenContents(
                        onAddClick = {
                            val newValue = 1
                            mainViewModel.updateShoppingItem(item, newValue)
                                     },
                        onDecreaseClick = {
                            val newValue = -1
                            mainViewModel.updateShoppingItem(item, newValue)
                                          },
                        }
                    )
                }


            }

        }

Result

Robotize answered 13/4, 2022 at 22:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.