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
Related
I have a method 'activity' which hosts a 'screen' and since state hoisting is cool, I did just that but with a problem.
While the code works perfectly (incrementing the value) using Modifier.clickable, the same code does not properly work using detectTapGestures::onTap as can be observed through Log.d("ExampleScreen", "onClick Multi Count is $multiCount")
#Composable
fun ExampleActivity() {
var multiCount by remember {
mutableStateOf(0)
}
Log.d("ExampleActivity", "Multi Count is $multiCount") //this works either way
ExampleScreen(
multiCount = multiCount,
incrementMultiCount = {
multiCount = ++multiCount
}
)
}
#Composable
fun ExampleScreen(
modifier: Modifier = Modifier,
multiCount: Int,
incrementMultiCount: () -> Unit
) {
Log.d("ExampleScreen", "Example Multi Count is $multiCount") //this works either way
Column(
modifier = modifier
.fillMaxSize()
.padding()
) {
Text(
modifier = Modifier
.size(100.dp, 50.dp)
.background(MaterialTheme.colorScheme.primaryContainer)
.clickable {//this works
incrementMultiCount()
Log.d("ExampleScreen", "onClick Multi Count is $multiCount")
}
/* .pointerInput(Unit) { //this does not work
detectTapGestures(
onTap = {
incrementMultiCount()
Log.d("ExampleScreen", "onClick Multi Count is $multiCount") //this stays 0
},
onLongPress = {
}
)
}*/, text = "Click to confirm"
)
}
}
Partially, it works the same (incrementing works but not reading the
value) as shown in logs defined in ExampleActivity and function body of
ExampleScreen but doesn't in the onTapGesture.
If the remember value is directly in the ExampleScreen composable, onTap and clickable works perfectly but not what I wanted.
Finally, before suggesting I use what works, I wanted to use detectTapGestures because I really need the LongPress method for a secondary work.
Plus I will really appreciate an explanation since I thought both works the same.
You have to wrap your callback in rememberUpdatedState, something like this:
val onTap: () -> Unit = {
incrementMultiCount()
Log.d("ExampleScreen", "onClick Multi Count is $multiCount")
}
val updatedOnTap = rememberUpdatedState(onTap)
detectTapGestures(onTap = { updatedOnTap.value.invoke() })
This is what .clickable modifier does with its onClick argument as well, you can see that in its source code. Explanation can be found in rememberUpdatedState documentation:
rememberUpdatedState should be used when parameters or values computed during composition are referenced by a long-lived lambda or object expression. Recomposition will update the resulting State without recreating the long-lived lambda or object, allowing that object to persist without cancelling and resubscribing, or relaunching a long-lived operation that may be expensive or prohibitive to recreate and restart.
By the way, there is also Modifier.combinedClickable that can be used for long click detection.
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.
I have a ExerciseDetailFragment that contains an ExerciseFragmentStatePagerAdapter to show a bunch of SupportUsFragmentCard. These cards can be swiped horizontally by the user.
In my ExerciseFragmentStatePagerAdapter I have the following code:
fun createFragment(position: Int): Fragment {
val exercise = exercises[position]
val card = SupportUsFragmentCard()
card.setExercise(exercise) <---- this is my question/problem
return card
}
As you can see the pager adapter just instantiates the SupportUsFragmentCard and for this the exercise needs to be passed along (the card displays some exercise information) via the setExercise.
The SupportUsFragmentCard looks like this:
open class SupportUsFragmentCard : RootFragment() {
...
val viewModel: SupportUsViewModel by viewModels()
...
fun setExercise(exercise: Exercise?) {
viewModel.setExercise(exercise) <---- passes on the exercise to the viewModel
}
It’s implementation passes along the exercise to the underlying viewModel. Which in its turn encapsulates this into a LiveData (on which the SupportUsFragmentPlain has observers but I omitted this code as its not the issue):
class SupportUsViewModel() : ViewModel() {
//interface to the view
fun getExercise(): LiveData<Exercise?> = exercise
fun setExercise(execise: Exercise?) {
exercise.value = execise. <--- updates the Livedata object
}
//underlying live data mutable values
private val exercise = MutableLiveData<Exercise?>(null)
}
When this code is executed, it crashes with exception:
IllegalStateException: Can't access ViewModels from detached fragment
I think because the createFragment ends up updating the viewModel without actually already being on screen. I also feel that this way of working does not respect the MVVM architecture.
What is the correct way in this scenario, using MVVM and LiveData to initialise the SupportUsViewModel with an Exercise in the ExerciseFragmentStatePagerAdapter's createFragment function?
I know state is the enemy of Reactive programming but I'm dealing with it in my process of learning RxSwift.
My app is very simple, the first screen is a list and a search of books and the second a detail of the book in which you can add/remove a book to your shelf and mark it as read/unread.
To show the detail of the book I create a BookViewModel passing a BooksService to perform network operations and the current Book to show.
The problem is that I have to keep track of the changes in the book in order to change the UI: for example, after removing the book the button that previously says "Remove" now it has to say "Add".
I achieve this behavior using a Variable<Book> exposed to the observers as a Driver<Book>, but I'm messing a lot with it when the network operation returns and I have to update the value of the Variable<Book> in order to trigger the update of the UI.
This is the initializer of the view model:
init(book: Book, booksService: BooksService) {
self._book = Variable(book)
self.booksService = booksService
}
This is the observable I expose
var book: Driver<Book> {
return _book.asDriver()
}
And here it is my function to add/remove the book:
func set(toggleInShelfTrigger: Observable<Void>) {
toggleInShelfTrigger // An observable from a UIBarButtonItem tap
.map({ self._book.value }) // I have to map the variable's value to the actual book
.flatMap({ [booksService] book -> Observable<Book> in
return (book.isInShelf ?
booksService.delete(book: book) :
booksService.add(book: book))
}) // Here I have to know if the books is in the shelf or not in order to perform one operation or another.
.subscribe(onNext: { self._book.value = $0 }) // I have to update the variable's value in order to trigger the UI update
.disposed(by: disposeBag)
}
I am very unhappy with this code and the whole view model. It works but it is clunky, and essentially wrong because if the network operation fails the subscription will be disposed and my button will became unresponsive.
If I get rid of the Variable<Book> and return a Driver<Book> from the method set(toggleInShelfTrigger: Observable<Void>) I won't have this mess but I will not be able to know if I have to add or to remove the book.
So, what is the real world way to keep track of the state of an object in this kind of app? How can I achieve this by only using Rx operators?
EDIT
I've managed to clean that crappy code but I'm still trying to achieve state without Variable and using scan operator.
This is my new BookViewModel initializer:
init(book: Book, booksService: BooksService) {
self.bookVariable = Variable(book)
let addResult = addBook
.mapBookFrom(bookVariable)
.flatMapLatest({ booksService.add(book: $0) })
.updateBookVariable(bookVariable)
let removeResult = ... // Similar to addResult, changing service call
let markReadResult = ... // Similar to addResult, changing service call
let markUnreadResult = ... // Similar to addResult, changing service call
self.book = Observable.of(addResult, removeResult, markReadResult, markUnreadResult).merge()
.startWith(.success(book))
}
I made a couple of custom operators to help me manage the Variable<Book>, one to get the real Book:
private extension ObservableType where E == Void {
func mapBookFrom(_ variable: Variable<Book>) -> Observable<Book> {
return map({ _ in return variable.value })
}
}
And another to update the Variable after the service returns:
private extension ObservableType where E == BookResult<Book> {
func updateBookVariable(_ variable: Variable<Book>) -> Observable<BookResult<Book>> {
return self.do(onNext: { result in
if case let .success(book) = result {
variable.value = book
}
})
}
}
Now I have a very clean view model, but not "perfect".
I would place the responsibility of the observing changes to the model object (Book) with the View.
Also, Variable is deprecated, best to use PublishRelay instead.
Of course, it depends how far you want to engineer this architecture, but something not too far from your example would be:
class BookDetailViewController: UIViewController {
let viewModel = BookViewModel(book: Book, booksService: BooksService)
func loadView() {
view = BookDetailView(viewModel: viewModel)
}
// ...
}
class BookDetailViewModel {
let book: PublishRelay<Book>
func addBook() {
book
.flatMap(booksService.add)
.bind(to: book)
.subscribe()
}
// ...
}
class BookDetailView: UIView {
let button: UIButton
init(viewModel: BookDetailViewModel) {
viewModel.book
.asObservable()
.subscribe(onNext: { book [button] in
button.setText(book.isSaved ? "Remove" : "Add")
})
button.rx.tap
.map { _ in viewModel.book.isSaved }
.subscribe(onNext: {
$0 ? viewModel.removeBook() : viewModel.addBook()
})
}
}
You could also implement a func toggle() in the view model instead, and just forward the button tap to call that method. It might be more accurate, semantically, depending on your interpretation of business logic and the extent to which you want to gather all of it in the view model.
Also note the example code is missing dispose bags, but that's another topic.
Colin Eberhardt has a great article on how to do some bindings in reactive-cocoa 3. There was however, one solution I didn't really like, and it had to do with his text field. He had created a property in his ViewModel for "searchtext" which he was observing in his view model, and he bound it to the text field's text. I'm not a fan of this because the view model shouldn't, in my mind, be holding onto this text, nor should anyone else be able to observe that text other than the view model. To me this should be accomplished by either a Signal or Signal Producer.
So my question is what the recommended way is to pass this data from, let's say a UITextView to the view model to act on it. I have 2 ways so far to accomplish it:
ViewModel
var signalProducer: SignalProducer<String, NSError>? {
didSet {
if let signalProducer = signalProducer {
signalProducer
... do stuff
}
}
}
ViewController
viewModel.signalProducer = textView.rac_textSignal().toSignalProducer()
|> map { text in text as! String }
(should my view model have direct access to the signal producer?)
Or
ViewModel
let (textViewTextSignal, textViewTextSink) = Signal<String, NoError>.pipe()
init() {
textViewTextSignal
... do stuff with it
}
ViewController
textView.rac_textSignal().toSignalProducer()
|> map { text in text as! String }
|> start(next: { [unowned self] text in
sendNext(self.viewModel.textViewTextSink, text)
})
(should any object be able to trigger this signal?)
I may be missing some fundamental concepts between Signal and Signal Producer here also, I'm just wondering how other people have been accomplishing this interaction.