Picture a list of StackOverflow questions that can individually be upvoted and downvoted. Each one of them has a count that displays the number of its votes. Clicking on upvote increases that count by one, clicking on downvote decreases that count by one. That's the general idea of what I'm trying to achieve. The layout, rv_item.xml
(condensed for brevity) for a single item in the RecyclerView:
data>
<variable
name="item"
type="com.mycodez.Item"/>
<variable
name="vm"
type="com.mycodez.ListViewModel" />
</data>
<Button
android:id="@+id/decrease_quantity"
android:onClick="@{() -> vm.decrementItemQuantity(item)}"/>
<TextView
android:id="@+id/quantity"
android:text="@{String.valueOf(item.quantity)}"
tools:text="1" />
<Button
android:id="@+id/increase_quantity"
android:onClick="@{() -> vm.incrementItemQuantity(item)}"/>
Where @+id/decrease_quantity
is similar to a downvote, @+id/quantity
similar to a vote count, and @+id/increase_quantity
similar to an upvote. The adapter:
class ListRvAdapter extends RecyclerView.Adapter<ListRvAdapter.ViewHolder> {
private List<Item> items;
private ListViewModel viewModel;
private LifecycleOwner lifecycleOwner;
ListRvAdapter(List<Item> items, ListViewModel viewModel, LifecycleOwner lifecycleOwner) {
this.items = items;
this.viewModel = viewModel;
this.lifecycleOwner = lifecycleOwner;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
RvItemBinding binding = RvItemBinding.inflate(layoutInflater, parent, false);
return new ViewHolder(binding, viewModel, lifecycleOwner);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.bind(items.get(position));
}
@Override
public int getItemCount() {
return items.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
private final RvItemBinding binding;
private ListViewModel viewModel;
private LifecycleOwner lifecycleOwner;
ViewHolder(RvItemBinding binding, ListViewModel viewModel, LifecycleOwner lifecycleOwner) {
super(binding.getRoot());
this.binding = binding;
this.viewModel = viewModel;
this.lifecycleOwner = lifecycleOwner;
}
public void bind(Item item) {
binding.setItem(item);
binding.setVm(viewModel);
binding.executePendingBindings();
binding.setLifecycleOwner(lifecycleOwner);
}
}
}
I've omitted the ViewModel, ListViewModel
because what vm.decrementItemQuantity(item)
and vm.incrementItemQuantity(item)
essentially do is call the model to update the quantity in the database:
@Query("UPDATE items SET quantity=quantity + 1 WHERE id=:itemId")
Completable incrementQuantity(int itemId);
@Query("UPDATE items SET quantity=quantity - 1 WHERE quantity > 1 AND id=:itemId")
Completable decrementQuantity(int itemId);
So while the quantity is being updated in the model, it's not clear to me how I should update the UI for that particular RecyclerView item to reflect that change. Normally, when using LiveData and DataBinding, I'd do something like itemQuantityLiveData.setValue(item.quantity)
and that would immediately be reflected in the XML. But in this case, I'm using the bind(Item item)
method in my ViewHolder, and the item
object is being passed to the DataBinding layout as a variable, not as an expression.
How do I solve this?
No need to overthink things. Calling notifyItemChanged(position)
on the adapter instance is the missing piece. So it becomes a matter of how to pass the index of the RecyclerView item back to the adapter.
Edit rv_item.xml
to take an additional variable, position
:
<data>
<variable
name="item"
type="com.mycodez.Item"/>
<!-- add this variable -->
<variable
name="position"
type="int"/>
<variable
name="vm"
type="com.mycodez.ListViewModel" />
</data>
Also, in that same XML file, pass position
to the vm
methods triggered by onClick
:
<Button
android:id="@+id/decrease_quantity"
android:onClick="@{() -> vm.decrementItemQuantity(item, position)}"/>
<Button
android:id="@+id/increase_quantity"
android:onClick="@{() -> vm.incrementItemQuantity(item, position)}"/>
Then the bind
method in the ListRvAdapter
ViewHolder
becomes:
public void bind(Item item, int position) { // new argument
binding.setItem(item);
binding.setPosition(position); // pass position to the layout
binding.setVm(viewModel);
binding.executePendingBindings();
binding.setLifecycleOwner(lifecycleOwner);
}
Pass the new argument in the adapter's onBindViewHolder
like so:
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.bind(items.get(position), position);
}
Finally, add two methods to the ListRvAdapter
for respectively increasing and decreasing quantity:
public void increaseQuantity(int position) {
Item item = items.get(position);
item.quantity += 1;
notifyItemChanged(position);
}
public void decreaseQuantity(int position) {
Item item = items.get(position);
item.quantity -= 1;
notifyItemChanged(position);
}
With all that in place, when the Button @+id/increase_quantity
is clicked, vm.incrementItemQuantity(item, position)
is triggered. In a MVVM setup, the ViewModel will have a LiveData Event object for position
:
public MutableLiveData<Event<Integer>> itemQuantityIncreased = new MutableLiveData<>();
And the call to vm.incrementItemQuantity(item, position)
will set that object's value:
itemQuantityIncreased.setValue(new Event<>(position))
;
Finally, the View will have an Observer on the ViewModel's itemQuantityIncreased
LiveData object. Because the Event sends the RecyclerView item's position with it, the View can get a hold of that and call adapter.increaseQuantity(position)
where it's appropriate. adapter.decreaseQuantity(position)
works just the same.