Search code examples
androidandroid-studiokotlinandroid-recyclerviewandroid-diffutils

diffutil callback in recyclerView doesn't work perfectly


I'm trying to add some search on the RecyclerView list without using

notifyDataSetChanged()

instead to it using

diffutil.callback()

but the issue is that it change the list correctly but it doesn't change the UI correctly

Here is my code and I will explain it

class RecordsAdapter : RecyclerView.Adapter<RecordsAdapter.ViewHolder>() {

    var adapterList = listOf<CustomerModel>()

    var modelList = listOf<CustomerModel>()
        set(value) {
            adapterList = value
            field = value
        }

    private var modelListFiltered = listOf<CustomerModel>()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
        ViewHolder(CustomerCellBinding.inflate(LayoutInflater.from(parent.context), parent, false))

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(adapterList[position])
    }

    override fun getItemCount(): Int = adapterList.size

    fun filter(isFiltered: Boolean, filterSearch: String) {
        if (isFiltered) {
            val filter = modelList
                .filter {
                    it.name.contains(filterSearch) || it.id.contains(filterSearch)
                }

            modelListFiltered = filter
        }

        adapterList = if (isFiltered) modelListFiltered else modelList

        val diff = CartDiffUtil(
            if (isFiltered) modelList else modelListFiltered,
            if (isFiltered) modelListFiltered else modelList
        )

        DiffUtil.calculateDiff(diff).dispatchUpdatesTo(this)
    }

    inner class ViewHolder(private var binding: CustomerCellBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(model: CustomerModel) {
            binding.let {
                it.model = model
                it.executePendingBindings()
            }
        }
    }
}

class CartDiffUtil(private val oldList: List<CustomerModel>, private val newList: List<CustomerModel>) : DiffUtil.Callback() {

    override fun getOldListSize() = oldList.size

    override fun getNewListSize() = newList.size

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
    oldList[oldItemPosition].id == newList[newItemPosition].id

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
    oldList[oldItemPosition] == newList[newItemPosition]
}

So I'm calling filter function to filter and I'm sending two parameters on if there is any filter and the second is the search.

Now the issue appears in this scenario

0. searching ""

1. searching "testing 2"

2. searching "testing 4"

3. searching "testing 2"

4. searching ""

init page

enter image description here enter image description here enter image description here enter image description here

As you can see in the images, when I search for "testing 2" after "testing 4" it keeps showing "testing 4" and even if I clear the search it gives me two cells of "testing 4" instead of one "testing 2" and one "testing 4"

Hope my question is clear.

Thanks.


Solution

  • I'm guessing your juggling of three list properties is leading to some situations where there can be the same list instance in the before and after of the DiffUtil so it cannot successfully compare them.

    Also, it's much easier to use ListAdapter instead of RecyclerView.Adapter when you want to use DiffUtil. Note that when you use ListAdapter, you use ItemCallback instead of Callback. ItemCallback is simpler.

    Try doing it this way, where there is only the modelList and when it or the filter changes, you determine what the new list is and submit it to the ListAdapter and let it handle the changes.

    class RecordsAdapter : ListAdapter<CustomerModel, RecordsAdapter.ViewHolder>(CustomerModelCallback) {
    
        var modelList = listOf<CustomerModel>()
            set(value) {
                field = value
                resync()
            }
    
        private var filterText: String = ""
        private var isFiltered: Boolean = false
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
            ViewHolder(CustomerCellBinding.inflate(LayoutInflater.from(parent.context), parent, false))
    
        override fun onBindViewHolder(holder: ViewHolder, position: Int) {
            holder.bind(adapterList[position])
        }
    
        fun filter(isFiltered: Boolean, filterText: String = "") {
            this.isFiltered = isFiltered
            this.filterText = filterText
            resync()
        }
    
        private fun resync() {
            val newList = when {
                isFiltered && filterText.isNotEmpty() -> 
                    modelList.filter {
                        it.name.contains(filterSearch) || it.id.contains(filterSearch)
                    }
                else -> modelList
            }
    
            submitList(newList)
        }
    
        // view holder...
    }
    
    object CustomerModelCallback : DiffUtil.ItemCallback<CustomerModel>() {
        override fun areItemsTheSame(oldItem: CustomerModel, newItem: CustomerModel): Boolean =
            oldItem.id == newItem.id
    
        override fun areContentsTheSame(oldItem: CustomerModel, newItem: CustomerModel): Boolean =
            oldItem == newItem
    }