Home > Mobile >  RecyclerView is not updated when an update occurs in Room
RecyclerView is not updated when an update occurs in Room

Time:12-01

I have a RecyclerView where an item can be edited via a DialogFragment, so when an item is clicked a Dialog is shown, then I can change some properties of that item, the issue is that RecyclerView is not updated with the updated properties and I have to force a notifyItemChanged when the Dialog is closed.

When an item in RecyclerView is clicked I set a MutableLiveData in my ViewModel so then it can be manipulated in the Dialog.

My ViewModel looks like this:

@HiltViewModel
class DocumentProductsViewModel @Inject constructor(private val repository: DocumentProductsRepository) :
    ViewModel() {
    val barcode = MutableLiveData<String>()

    private val _selectedProduct = MutableLiveData<DocumentProduct>()
    val selectedProduct: LiveData<DocumentProduct> = _selectedProduct

    private val _selectedDocumentId = MutableLiveData<Long>()
    val selectedDocumentId: LiveData<Long> = _selectedDocumentId

    val products: LiveData<List<DocumentProduct>> = _selectedDocumentId.switchMap { documentId ->
        repository.getDocumentProducts(documentId).asLiveData()
    }

    fun insert(documentProduct: DocumentProduct) = viewModelScope.launch {
        repository.insert(documentProduct)
    }

    fun setProductQuantity(quantity: Float) {
        _selectedProduct.value = _selectedProduct.value.also {
            it?.timestamp = System.currentTimeMillis()
            it?.quantity = quantity
        }
        update()
    }

    fun start(documentId: Long?) = viewModelScope.launch{
        if (documentId == null) {
            _selectedDocumentId.value = repository.getHeaderByType("Etichette")?.id
        }

        documentId?.let { documentId ->
            _selectedDocumentId.value = documentId
        }
    }

    fun select(product: DocumentProduct) {
        _selectedProduct.value = product
    }

    fun delete() = viewModelScope.launch {
        _selectedProduct.value?.let { repository.delete(it) }
    }

    private fun update() = viewModelScope.launch {
        _selectedProduct.value?.let { repository.update(it) }
    }
}

And in my fragment I'm subscribed to products as this:

private fun initRecyclerView() {
    binding.rvProducts.adapter = adapter

    viewModel.products.observe(viewLifecycleOwner) { products ->
        val productsCount = products.count()
        binding.tvProductsCount.text =
            resources.getQuantityString(R.plurals.articoli, productsCount, productsCount)

        // TODO: create amount string and set it with resources
        binding.tvProductsAmount.text = productsCount.toEuro()

        adapter.submitList(products)
        binding.rvProducts.smoothScrollToPosition(adapter.itemCount - 1)
    }

    initSwipe(adapter)
}

When setProductQuantity is called the RecyclerView remains unchanged until notify is called while delete works fine without the necessity of calling any notify on RecyclerView.

UPDATE:

The item position is actually changed in RecyclerView as it's sorted by it's last changed timestamp BUT not the quantity field.

Here is my Adapter:

class DocumentProductsListAdapter : ListAdapter<DocumentProduct, DocumentProductsListAdapter.ViewHolder>(ProductDiffCallback) {

    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
        val view: View = LayoutInflater.from(viewGroup.context)
            .inflate(R.layout.layout_item, viewGroup, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val product = getItem(position)
        holder.bind(product)
    }

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val barcode: TextView = itemView.findViewById(R.id.barcode)
        val quantity: TextView = itemView.findViewById(R.id.quantity)
        val description: TextView = itemView.findViewById(R.id.description)
        val unitOfMeasure: TextView = itemView.findViewById(R.id.unitOfMeasure)

        fun bind(product: DocumentProduct) {
            barcode.text = product.barcode
            quantity.text = product.quantity.formatForQta().replace(".", ",")

            if (product.labelType != null && product.labelType != "") {
                unitOfMeasure.text = product.labelType
            } else {
                unitOfMeasure.text = product.unitOfMeasure?.lowercase(Locale.ITALIAN)
            }

            description.text = product.description ?: "-"
        }
    }
}

object ProductDiffCallback : DiffUtil.ItemCallback<DocumentProduct>() {
    override fun areItemsTheSame(oldItem: DocumentProduct, newItem: DocumentProduct): Boolean {
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: DocumentProduct, newItem: DocumentProduct): Boolean {
        return oldItem == newItem
    }
}
data class DocumentProduct(
    @PrimaryKey(autoGenerate = true)
    var id: Long,
    var barcode: String,
    @Json(name = "desc")
    var description: String?,
    @ColumnInfo(defaultValue = "PZ")
    @Json(name = "um")
    var unitOfMeasure: String?,
    @Json(name = "qta")
    var quantity: Float,
    @Json(name = "id_testata")
    var documentId: Long,
    @Json(name = "tipo_frontalino")
    var labelType: String?,
    var timestamp: Long?
) {
    constructor(barcode: String, documentId: Long, labelType: String?) : this(
        0,
        barcode,
        null,
        "PZ",
        1f,
        documentId,
        labelType,
        null
    )
 
    override fun equals(other: Any?): Boolean {
        return super.equals(other)
    }
 
    override fun hashCode(): Int {
        return super.hashCode()
    }
}

CodePudding user response:

You have the implementations of areContentsTheSame() and areItemsTheSame() swapped.

areContentsTheSame() is asking if everything in the two items being compared is the same. Therefore, if the class has a proper equals()/hashcode() for all properties used by the ViewHolder, you can use oldItem == newItem. If you use a data class with all relevant properties in the primary constructor, then you don't need to manually override equals()/hashcode().

areItemsTheSame() is asking if the two items represent the same conceptual row item, with possible differences in their details. So it should be oldItem.id == newItem.id.

The problem with your data class is that you are overriding equals()/hashcode() without providing any implementation at all. This is effectively disabling the proper implementations that are provided by the data modifier by calling through to the super implementation in the Any class. You should not override them at all when you use data class.

  • Related