LazyColumn and mutable list - how to update? - android-jetpack-compose

I'm new to Jetpack Compose and I've spent some hours to find how to make a LazyColumn update what I update my list. I've read that it needs to be a immutable list to update LazyColumn, but I can't seem to get it to work.
The code looks like:
#Composable
fun CreateList() {
var myList : List<DailyItem> by remember { mutableStateOf(listOf())}
myList = getDailyItemList() // Returns a List<DailyItem> with latest values and uses mutable list internally
// Function to refresh the list
val onUpdateClick = {
// Do something that updates the list
...
// Get the updated list to trigger a recompose
myList = getDailyItemList()
}
// Create the lazy column
...
}
I have tried several things and either is the list never updated when tapping the update button or only the first item is updated but not the rest of the items in the list. I looked in the documentation and there it says this, but I don't understand it:
Instead of using non-observable mutable objects, we recommend you use
an observable data holder such as State<List> and the immutable
listOf().
How to update the list so the LazyColumn is updated?

Use SnapshotStateList, the list is mutable. Any modification (add, remove, clear, ...) to the list will trigger an update in LazyColumn.
Similar to mutableListOf() (for MutableList) there is mutableStateListOf() to create a SnapshotStateList.
Extention function swapList() just combines clear() and addAll() calls to replace old list with new list.
fun <T> SnapshotStateList<T>.swapList(newList: List<T>){
clear()
addAll(newList)
}
#Composable
fun CreateList() {
val myList = remember { mutableStateListOf<DailyItem>() }
myList.swapList(getDailyItemList()) // Returns a List<DailyItem> with latest values and uses mutable list internally
// Function to refresh the list
val onUpdateClick = {
// Do something that updates the list
...
// Get the updated list to trigger a recompose
myList.swapList(getDailyItemList())
}
// Create the lazy column
...
}

See the basic idea is to get compose treat the list as state. Now that, you are able to achieve using mutableStateOf(initialValue),
Okay, the process is this,.
We create a variable, initialising it as a mutable state of something
Then we assign that variable to the lazy column. It is not necessary to assign it to the items parameter of the column, but that is our use case here. Otherwise, inside the Composable containing the lazy column, you could just type the name of the variable and even that will work since all we want, is compose to get a message that this variable is being read by the Composable.
Back to the question,
We create a variable, say val mList: List<Int> by remember { mutableStateOf (listOf()) }
Lazycolumn{
items(items = mList){
Text(it)
}
}
Button(onClick = { mList = mList + listOf(mList.size())})
Clicking the button adds a new number to the list, which is reflected in the LazyColumn's UI.

Related

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

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 = "-")
}
}
}
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.
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

Compare the two lists and change the value of the properties of the objects in one of the lists

I have the two lists of AddItem objects. AddItem is a custom object made of data downloaded from the web. The second one list is also AddItem, but this one is saved in the database. I use it to create another list, but in this case user of the app decides which object are important for him.
This want I want to achieve is to mark every single object of the first AddItem list (not saved in the database, create during the start of the view), to show in the TableView which one is saved in the database, so I already use him in another view. You know what I mean. There is a TableView list and if I am interested in a cell I select it and add it to the database.
I hope I have described it clearly. If not, ask for questions.
The first AddItem list (not saved in the database):
func setAddItemList(stations: [Station], sensors: [Sensor]) {
var addItems = [AddItem]()
var sensorItems = [SensorItem]()
let falseValue = RealmOptional<Bool>(false)
addList = try persistenceService.fetchAddItems().toArray(ofType: AddItem.self) //The second list with saved data in the database
let addItem = stations.map { station in
AddItem(
id: station.id,
stationId: station.id,
cityName: station.city?.name ?? "",
addressStreet: station.addressStreet!,
added: falseValue,
sensor: [])
}
addItems.append(contentsOf: addItem)
As you can see, it's create by already downloaded data. I decided to add the property - added, which is the bool property and mark it as true if selected the right cell. Unfortunately I don't know how to do this when creating a list of AddItem objects. The saved array is almost the same. There is only more data, but ids, names, addresses and so on are same, so there are loads of the same data for comparison
I made the solution myself:
addItem.forEach { item in
guard let index = addList2.firstIndex(where: { $0.id == item.id})
else {
print("Failed to find the SavedAddItem for the AddItem \(item.id)")
return
}
addItems[index + 1].added = trueValue
}

How to name an instance of a struct the contents of a variable - Swift

There is almost certainly a better way of doing this and I'd love to know but I can't phrase it in a question so essentially here is my problem:
I am creating an app that presents a list of items(In a table view) which have various bits of data that come along with the item(String Int Date ect). I've decided that the best way to store this data is in a struct because it allows me to store lost of different types of data as well as run processes on it.
The problem is that I want to have theoretically an infinite number of items in the list and so I need to make lost of instances of the Item struct without predetermining the names of each instance.
I would then store these instance names in an array so that they can be listed in the table view.
I'm completely stuck at this point I've spent hours looking and I just can't make sense of it I'm sure its stupidly easy because hundreds of apps must need to do this. I'm Open to anything thanks.
Currently, I have a struct:
struct Item() {
var data1: String
var data2: String // (But Should be Int)
var data3: String
func setDate() {
// code
}
func returnDate() {
// code
}
}
and then in the view controller I have:
#IBAction func SubmitButton(_ sender: UIButton) {
var textField1 = Item(data1: textField1.text!, data2: textFeild2.text!, data3: "Units")
print(textField1.data1)
}
I am not completely sure what your goal is but I guess the final goal is to have an array of your Item Objects, which then can be used to populate an UITableView??? Without setting up a name for this Instances?
The most easiest solution would be to create a array as a Class Property for storing all the Items which is empty like this:
var items : [Item] = []
And then in viewDidLoad(), you can call a function which populates the item array with all the available Items.
This Item Array can then be used to populate the UITableView.
Second Part:
You got this IBAction which creates a new Item. I guess this is supposed to add a new Item to the existing ones.
You can add this new Item to the existing Item Array by using
items.append(Item(<parameters>))
And don't forget to reload the UITableView to include the new data.
tableView.reloadData()

Trying to append text to a property inside a loop in Swift

I have an array of struct objects. These objects have a property for title (which is a String), and a property for location (of type Location).
What I would like to is append a double value for distance derived from the location property and the distance function with another, existing Location object, to the title property. Here is the code I am working with:
self.myList.map({$0.title.append("\($0.location.distance(from: location!)/1000)")})
The problem here is that the map function returns a new array, however, I need to make the change to the existing array, since I'm using this array as the datasource for my UITableView. The other problem is that despite me making the title property a var, I am always told the following error message:
Cannot use mutating member on immutable value: '$0' is immutable.
Can anyone figure out how to do this?
You should iterate over the array like this:
for (i, _) in myList.enumerated() {
myList[i].title.append("\(myList[i].location.distance(from: location) / 1000)")
}
You'd have to use a temporary mutable variable:
myList = myList.map { (myStruct: MyStruct) -> MyStruct in
var mutableStruct = myStruct
mutableStruct.title.append("\(element.location.distance(from: location) / 1000)")
return mutableStruct
}

"Cannot assign to" error iterating through array of struct

I have an array of structs:
struct CalendarDate {
var date: NSDate?
var selected = false
}
private var collectionData = [CalendarDate]()
Which I simply populate with a date like this:
for _ in 1...7 {
collectionData.append(CalendarDate(date: NSDate(), selected: false))
}
So when you tap on a collectionView, I simply want to loop through the data and mark them all as False.
for c in collectionData {
c.selected = false ///ERROR: Cannot assign to 'selected' in 'c'
}
Why do I get this error?
If I do this, it works fine but I want to know what I did wrong above:
for i in 0..<collectionData.count {
collectionData[i].selected = false
}
As I understand it, the iterator
for c in collectionData
returns copies of the items in collectionData - (structs are value types, not reference types, see http://www.objc.io/issue-16/swift-classes-vs-structs.html), whereas the iteration
for i in 0..<collectionData.count
accesses the actual values. If I am right in that, it is pointless to assign to the c returned from the iterator... it does not "point" at the original value, whereas the
collectionData[i].selected = false
in the iteration is the original value.
Some of the other commentators suggested
for (var c) in collectionData
but although this allows you to assign to c, it is still a copy, not a pointer to the original, and though you can modify c, collectionData remains untouched.
The answer is either A) use the iteration as you originally noted or B) change the data type to a class, rather than a struct.
because each 'c' is by default let, and this is a new instance of CalendarDate and the value of array at index copied to this for each step of for, and 'c' isn't pointer to the index of the array and it is just a copy of index, so if you set a new value to this, the new value does not apply in array.
but 'i' is used as index of array and can directly manipulate the values of array.
If you are using structs they are copies in the array. So even changing them only changes the copy, not an actual object in the array.
You have to make them a variable in the loop to be editable copy, and reassign them into the array right back.
If they are classes and not structs, than you don't have to reassign part, just do the var thing.
for (index, var c) in collectionData.enumerated() {
c.selected = false
collectionData[index] = c
}

Resources