androidfilterandroid-recyclerviewpayloadandroid-asynclistdiffer

RecyclerView Adapter with filtering and payloads


The question

Hello! Has anyone implemented a RecyclerView Adapter, which implements filtering via Filter and in which additional items can be updated via payloads? How to synchronize two lists, the original and the filtered one, when you want to update an element, but you already used a filtering? Does anyone know any good examples?

PS: on top of that, there are a lot of elements in the adapter and updating occurs via AsyncListDiffer.

Thank you all in advance for your answers!

The problem

The main problem is that when I enter a query into the search, I get filtered items, add one of the items to favorites (marked with an asterisk in the item), but when I clear the search, the list item is not a favorite (the asterisk is not displayed). Everything is also complicated by how to correctly update list elements during AsyncListDiffer, so as not to accidentally update them while the list is being asynchronously updated.

Code

I suspect the problem is in the submitList and updateItem method:

class Adapter(
    private val listener: AdapterListener
): RecyclerView.Adapter<RecyclerView.ViewHolder>(), Filterable {

    private val diffCallback = object : DiffUtil.ItemCallback<UiModel>() {

        override fun areItemsTheSame(oldItem: UiModel, newItem: UiModel): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: UiModel, newItem: UiModel): Boolean {
            return (
                oldItem.name == newItem.name &&
                oldItem.id == newItem.id &&
                oldItem.isFavorite == newItem.isFavorite
            )
        }
    }

    private var listDiffer = AsyncListDiffer(this, diffCallback)
    private val initialDataItems = mutableListOf<UiModel>()
    private val dataFilter = object : Filter() {

        override fun performFiltering(constraint: CharSequence?): FilterResults {
            val queryRawName = constraint?.toString()
            if (queryRawName.isNullOrEmpty()) return FilterResults().apply { values = initialDataItems }
            val queryName = queryRawName.lowercase().trim()
            val filteredItems = initialDataItems.filter { it.name?.lowercase()?.contains(queryName).isTrue() }
            return FilterResults().apply { values = filteredItems }
        }

        override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
            val values = results?.values as? List<*> ?: return
            val filteredValues = values.filterIsInstance(UiModel::class.java)
            if (values.size != filteredValues.size) return
            listDiffer.submitList(filteredValues)
        }
    }

    override fun getItemCount() = listDiffer.currentList.size

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val view = parent.inflate(R.layout.item_layout)
        return CustomViewHolder(
            view = view,
            listener = { listener.onClickFavorite(uiModel) }
        )
    }

    override fun onBindViewHolder(
        holder: RecyclerView.ViewHolder,
        position: Int,
        payloads: MutableList<Any>
    ) {
        val payloadValid = payloads.isNotEmpty() && payloads[0] is UiModel
        if (payloadValid && holder is CustomViewHolder) {
            holder.updatePayload(payloads[0] as UpdateUiModel)
        } else {
            super.onBindViewHolder(holder, position, payloads)
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val item = getItem(position)
        (holder as? CustomViewHolder)?.bind(item)
    }

    override fun getFilter() = dataFilter

    fun submitList(data: List<UiModel>, updateInitialItems: Boolean = true) {
        listDiffer.submitList(data)
        if (updateInitialItems) {
            initialDataItems.clear()
            initialDataItems.addAll(data)
        }
    }

    fun updateItem(payload: UpdateUiModel) {
        if (payload.id == null) return
        listDiffer.currentList.forEachIndexed { index, dataItem ->
            if (dataItem.id == payload.id) {
                updateModel(index, payload)
                notifyItemChanged(index, payload)
            }
        }
    }

    private fun isCorrectPosition(position: Int) = position in 0 until itemCount

    private fun updateModel(index: Int, payload: UiModel) {
        val currentList = listDiffer.currentList.toMutableList()
        if (isCorrectPosition(index)) currentList[index] = currentList[index].updateModel(payload)
        submitList(currentList)
    }

    private fun getItem(position: Int) = listDiffer.currentList[position]
}

Solution

  • Yes, it really need to update the initial list. What I changed:

    submitList method:

    fun submitList(data: List<UiModel>) {
        listDiffer.submitList(data)
        initialDataItems.clear()
        initialDataItems.addAll(data)
    }
    

    updateItem method (the code can be improved, I wrote it quickly):

    fun updateItem(payload: UpdateUiModel) {
        if (payload.id == null) return
        listDiffer.currentList.forEachIndexed { index, dataItem ->
            if (dataItem.id == payload.id) {
                updateModel(index, payload)
            }
        }
        val newInitialList = initialDataItems.toMutableList()
        initialDataItems.forEachIndexed { index, dataItem ->
            if (dataItem.id == payload.id) {
                if (isCorrectPosition(index)) newInitialList[index] = newInitialList[index].updateModel(payload)
            }
        }
        initialDataItems = newInitialList
    }
    

    Note that at the end I do initialDataItems = newInitialList in order to completely update the initial list.

    And the last method:

    private fun updateModel(index: Int, payload: UiModel) {
        val currentList = listDiffer.currentList.toMutableList()
        if (isCorrectPosition(index)) currentList[index] = currentList[index].updateModel(payload)
        listDiffer.submitList(currentList)
    }