androidandroid-recyclerviewlistadapterandroid-diffutilsandroid-listadapter

ListAdapter currentList and itemCount not returning updates after filter or submitList


I am implementing filterable list for RecyclerView using ListAdapter with AsyncDifferConfig.Builder that implements Filterable. When searching and no result match, a TextView will be shown.

 adapter.filter.filter(filterConstraint)


 // Searched asset may not match any of the available item
 if (adapter.itemCount <= 0 && adapter.currentList.isEmpty() && filterConstraint.isNotBlank())
     logTxtV.setText(R.string.no_data)
 else
     logTxtV.text = null

Unfortunately the update of filter did not propagate immediately on adapter's count and list. The adapter count and list is one step behind.

enter image description here

The TextView should be displaying here already enter image description here

But it only shows after updating it back and the list is no longer empty at this point enter image description here

I am not sure if this is because I am using AsyncDifferConfig.Builder instead of regular DiffCallback

ListAdapter class
abstract class FilterableListAdapter<T, VH : RecyclerView.ViewHolder>(
    diffCallback: DiffUtil.ItemCallback<T>
) : ListAdapter<T, VH>(AsyncDifferConfig.Builder(diffCallback).build()), Filterable {

    /**
     * True when the RecyclerView stop observing
     * */
    protected var isDetached: Boolean = false

    private var originalList: List<T> = currentList.toList()

    /**
     * Abstract method for implementing filter based on a given predicate
     * */
    abstract fun onFilter(list: List<T>, constraint: String): List<T>

    override fun getFilter(): Filter {
        return object : Filter() {
            override fun performFiltering(constraint: CharSequence?): FilterResults {
                return FilterResults().apply {
                    values = if (constraint.isNullOrEmpty())
                        originalList
                    else
                        onFilter(originalList, constraint.toString())
                }
            }

            @Suppress("UNCHECKED_CAST")
            override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
                submitList(results?.values as? List<T>, true)
            }
        }
    }

    override fun submitList(list: List<T>?) {
        submitList(list, false)
    }

    /**
     * This function is responsible for maintaining the
     * actual contents for the list for filtering
     * The submitList for parent class delegates false
     * so that a new contents can be set
     * While a filter pass true which make sure original list
     * is maintained
     *
     * @param filtered True if the list was updated using filter interface
     * */
    private fun submitList(list: List<T>?, filtered: Boolean) {
        if (!filtered)
            originalList = list ?: listOf()

        super.submitList(list)
    }

    override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
        super.onDetachedFromRecyclerView(recyclerView)
        isDetached = true
    }

}

RecyclerView Adapter

class AssetAdapter(private val glide: RequestManager, private val itemListener: ItemListener) :
    FilterableListAdapter<AssetDataDomain, AssetAdapter.ItemView>(DiffUtilAsset()) {

    inner class ItemView(itemView: AssetCardBinding) : RecyclerView.ViewHolder(itemView.root) {
        private val assetName = itemView.assetName
        private val assetPrice = itemView.assetPrice
        private val assetMarketCap = itemView.assetMarketCap
        private val assetPercentChange = itemView.assetPercentChange
        private val assetIcon = itemView.assetIcon
        private val assetShare = itemView.assetShare

        // Full update/binding
        fun bind(domain: AssetDataDomain) {

            with(itemView.context) {

                assetName.text = domain.symbol ?: domain.name

                bindNumericData(
                    domain.metricsDomain.marketDataDomain.priceUsd,
                    domain.metricsDomain.marketDomain.currentMarketcapUsd,
                    domain.metricsDomain.marketDataDomain.percentChangeUsdLast24Hours
                )

                if (!isDetached)
                    glide
                        .load(
                            getString(
                                R.string.icon_url,
                                AppConfigs.ICON_BASE_URL,
                                domain.id
                            )
                        )
                        .into(assetIcon)

                assetShare.setOnClickListener {
                    itemListener.onRequestScreenShot(
                        itemView,
                        getString(
                            R.string.asset_info,
                            domain.name,
                            assetPercentChange.text.toString(),
                            assetPrice.text.toString()
                        )
                    )
                }

                itemView.setOnClickListener {
                    itemListener.onItemSelected(domain)
                }

            }


        }

        // Partial update/binding
        fun bindNumericData(priceUsd: Double?, mCap: Double?, percent: Double?) {

            with(itemView.context) {
                assetPrice.text = getString(
                    R.string.us_dollars,
                    NumbersUtil.formatFractional(priceUsd)
                )
                assetMarketCap.text = getString(
                    R.string.mcap,
                    NumbersUtil.formatWithUnit(mCap)
                )
                assetPercentChange.text = getString(
                    R.string.percent,
                    NumbersUtil.formatFractional(percent)
                )

                AppUtil.displayPercentChange(assetPercentChange, percent)

                if (NumbersUtil.isNegative(percent))
                    assetPrice.setTextColor(Color.RED)
                else
                    assetPrice.setTextColor(Color.GREEN)
            }

        }

    }

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

    override fun onBindViewHolder(holder: ItemView, position: Int) {
        onBindViewHolder(holder, holder.absoluteAdapterPosition, emptyList())
    }

    override fun onBindViewHolder(holder: ItemView, position: Int, payloads: List<Any>) {

        if (payloads.isEmpty() || payloads[0] !is Bundle)
            holder.bind(getItem(position)) // Full update/binding
        else {

            val bundle = payloads[0] as Bundle

            if (bundle.containsKey(DiffUtilAsset.ARG_PRICE) ||
                bundle.containsKey(DiffUtilAsset.ARG_MARKET_CAP) ||
                bundle.containsKey(DiffUtilAsset.ARG_PERCENTAGE))
                holder.bindNumericData(
                    bundle.getDouble(DiffUtilAsset.ARG_PRICE),
                    bundle.getDouble(DiffUtilAsset.ARG_MARKET_CAP),
                    bundle.getDouble(DiffUtilAsset.ARG_PERCENTAGE)
                ) // Partial update/binding

        }

    }

    // Required when setHasStableIds is set to true
    override fun getItemId(position: Int): Long {
        return currentList[position].id.hashCode().toLong()
    }

    override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
        super.onDetachedFromRecyclerView(recyclerView)
        isDetached = true
    }

    override fun onFilter(list: List<AssetDataDomain>, constraint: String): List<AssetDataDomain> {

        return list.filter {
            it.name.lowercase().contains(constraint.lowercase()) ||
                    it.symbol?.lowercase()?.contains(constraint.lowercase()) == true
        }

    }

    interface ItemListener {

        fun onRequestScreenShot(view: View, description: String)
        fun onItemSelected(domain: AssetDataDomain)

    }

}

UPDATE:

I can confirm that using DiffCallback instead of AsyncDifferConfig.Builder does not change the behavior and issue. It also seems that currentList is in async thus update on list does not reflect immediately after calling submitList.

I do not know if this is intended behavior but upon overriding onCurrentListChanged, the currentList parameter is what I am looking for.

enter image description here

But the adapter.currentList is behaving like a previousList parameter


Solution

  • When you submit a list to recyclerView, it takes some time to compare items of current list and the previous one (to see if an item is removed, moved or added). so the result is not immediately ready.

    you can use a RecyclerView.AdapterDataObserver to be notified of changes in recyclerView (it will tell what happened to items overall, like 5 were added etc)

    P.S. if you look at recyclerView source code you will see that the DiffCallBack passed in the constructor, is wrapped in AsyncDifferConfig