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.
The TextView should be displaying here already
But it only shows after updating it back and the list is no longer empty at this point
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.
But the adapter.currentList
is behaving like a previousList
parameter
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