I am using an MVVM architechture to build a simple ordering app. I am using a RecyclerView in my ProductsFragment to list all the products that can be ordered. I am also using LiveData in my ViewModel and observable in my Fragment to check for any changes to the List of products.
In my list item I have 3 buttons: 1 button to add a product to the basket, another to increment the quantity that customer wants to add to the basket and a third to decrement the quantity the customer wants to add to the basket.
In my product data class, whenever the customer clicks on increment or decrement button, the quantity is updated in the data model.
I am also using databinding to bind the product to the recyclerview list item layout and the click listeners. I am using the ListAdapter to get access to DiffUtil out of the box.
The problem I am having is that when the observable is notified I want to use the submitList method of the ListAdapter so only the item that has changed is updated in the RecyclerView. However, I have noticed that the DiffUtil method areContentsTheSame() always returns true. So the list item isn't updated. I don't want to use notifyDatasetChanged as this blocks the UI thread.
Another problem I am having is when I add a product to the basket, the reference to the product is being kept, so when I add a product to the basket, the MutableLiveData is also updated, when I just want MutableLiveData to be updated. How can I stop a reference to the LiveData being created when I add a product to the basket?
ProductsViewModel
class ProductsViewModel : ViewModel() {
// LIVE DATA
private val _basket = MutableLiveData<Basket>()
val basket: LiveData<Basket>
get() = _basket
private val _products = MutableLiveData<List<Product>>()
val products: LiveData<List<Product>>
get() = _products
init {
_basket.value = Basket()
_products.value = dummyData
}
fun onIncrementProductQuantityButtonPressed(product: Product) {
//product.quantity += 1
//val newList = _products.value
//_products.value = newList
val newProduct = product.copy(quantity = product.quantity.plus(1))
_basket.value!!.updateProductInBasket(newProduct)
_basket.value = _basket.value
}
fun onDecrementProductQuantityButtonPressed(product: Product) {
if (product.quantity>1) {
//product.quantity = product.quantity.minus(1)
val newProduct = product.copy(quantity = product.quantity.minus(1))
_basket.value!!.updateProductInBasket(newProduct)
_basket.value = _basket.value
}
}
}
ProductsFragment
class ProductsFragment : Fragment() {
private lateinit var viewModel: ProductsViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
val binding: FragmentProductsBinding = DataBindingUtil.inflate(
inflater, R.layout.fragment_products, container, false)
viewModel = ViewModelProviders.of(this).get(ProductsViewModel::class.java)
val adapter = ProductsAdapter(ProductListener { product, onClickType ->
when(onClickType) {
OnClickType.INCREMENT -> {
viewModel.onIncrementProductQuantityButtonPressed(product)
}
OnClickType.DECREMENT -> {
viewModel.onDecrementProductQuantityButtonPressed(product)
}
OnClickType.BASKET -> {
viewModel.addToBasketButtonPressed(product)
}
}
})
viewModel.products.observe(this, Observer { list ->
adapter.submitList(list)
//adapter.notifyDataSetChanged() // TODO: check why I have to do notifyDataSetChanged()
})
viewModel.basket.observe(this, Observer {
activity?.invalidateOptionsMenu()
})
binding.viewModel = viewModel
binding.lifecycleOwner = this
binding.productsRecyclerView.adapter = adapter
setHasOptionsMenu(true)
return binding.root
}
ProductsAdapter
class ProductsAdapter(private val clickListener: ProductListener) : ListAdapter<Product, ProductsAdapter.ProductViewHolder>(ProductDiffUtil()) {
class ProductDiffUtil: DiffUtil.ItemCallback<Product>() {
override fun areItemsTheSame(oldItem: Product, newItem: Product): Boolean {
Log.d("Products", "Are items the same")
return oldItem.name == newItem.name && oldItem.size == newItem.size
}
override fun areContentsTheSame(oldItem: Product, newItem: Product): Boolean {
Log.d("Products", "Are contents the same ${oldItem == newItem}")
Timber.d("Are contents the same ${oldItem == newItem}")
Timber.d("OLD ITEM: $oldItem")
Timber.d("NEW ITEM: $newItem")
return oldItem == newItem //need to check this
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
return ProductViewHolder.from(parent)
}
override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
val item = getItem(position)
//use holder to access all the views in the card item
holder.bind(clickListener, item)
}
class ProductViewHolder private constructor(private val binding: LayoutProductCardBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(clickListener: ProductListener, item: Product) {
binding.product = item
binding.clickListener = clickListener
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): ProductViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = LayoutProductCardBinding.inflate(layoutInflater, parent, false)
return ProductViewHolder(binding)
}
}
}
}
class ProductListener(val clickListener: (product: Product, clickType: OnClickType) -> Unit) {
fun onAddToBasket(product: Product) = clickListener(product, OnClickType.BASKET)
fun onDecrementProductQuantity(product: Product) = clickListener(product, OnClickType.DECREMENT)
fun onIncrementProductQuantity(product: Product) = clickListener(product, OnClickType.INCREMENT)
}
enum class OnClickType { BASKET, DECREMENT, INCREMENT }
Your issue seems to have a simple solution :). What you're doing is update only the value of the product that is passed to VM from view. What you should do is : create a copy of the product using .copy(quantity = ...) method, overwriting the quantity. Then replace your previous item in list and pass your new list to LiveData. This might also be the case why you need to actively call notifyDataSetChanged(as per your comment in code).
Hope this helps. Cheers!