如何避免可过滤适配器上的 notifyDataSetChanged?

How to avoid notifyDataSetChanged on a Filterable Adapter?

我正在改进我的应用稳定性和性能,但现在我被 Android Studio 发出的警告卡住了。请考虑以下适配器 class:

private class CoinsAdapter(private val fragment: CoinFragment, private val coins: List<Coin>): RecyclerView.Adapter<CoinsAdapter.ViewHolder>(), Filterable {

    private val filter = ArrayList(coins)

    override fun onCreateViewHolder(parent: ViewGroup, position: Int): ViewHolder {
        val binding = ItemCoinBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val coin = filter[position]
        
        holder.binding.coinImage.setImageResource(coin.image)
        holder.binding.coinText.text = builder.toString()
    }

    override fun getItemCount() = filter.size

    override fun getFilter() = object : Filter() {

        override fun performFiltering(constraint: CharSequence): FilterResults {
            if (constraint.length < 2) return fetchResults(coins)
            val pattern = constraint.toString().lowercase().trim()

            val filter = arrayListOf<Coin>()
            for (coin in coins) if (coin.name.lowercase().contains(pattern)) filter.add(coin)

            return fetchResults(filter)
        }

        private fun fetchResults(coins: List<Coin>): FilterResults {
            val results = FilterResults()
            results.values = coins

            return results
        }

        override fun publishResults(constraint: CharSequence, results: FilterResults) {
            filter.clear()
            filter.addAll(results.values as List<Coin>)

            notifyDataSetChanged()
        }
    }

    private inner class ViewHolder(val binding: ItemCoinBinding) : RecyclerView.ViewHolder(binding.root)
}

适配器和过滤器工作完美,但请注意 publishResults 功能。 Android Studio 就 notifyDataSetChanged.

发出警告

It will always be more efficient to use more specific change events if you can. Rely on notifyDataSetChanged as a last resort.

但是,我不知道如何在这种情况下使用 notifyDataSetChanged(使用过滤器)。在这种情况下,正确的方法是什么以及如何使用它?

据我所知,在 RecyclerView.Adapter 中使用 Filterable 接口毫无意义。 Filterable 旨在用于 AdapterView Adapters,因为有一些小部件可以检查 Adapter 是否是 Filterable 并且可以自动提供一些过滤功能。但是,RecyclerView.Adapter 与 AdapterView 的 Adapter 没有任何关系。

如果愿意,您仍然可以使用 Filter 接口来组织代码,但对我来说,这似乎是不必要的额外样板。我在 Whosebug 上看到其他旧答案说要在 RecyclerView.Adapter 中实现 Filterable,但我认为他们这样做是出于使用旧适配器 class.

的习惯

至于在过滤时提高适配器的性能,有几个选项。

  1. 使用 SortedList 和 SortedList.Callback 来管理您的列表。回调让您实现一堆函数来通知特定项目或项目范围的更改,而不是一次通知整个列表。我没有用过这个,而且似乎有很多出错的空间,因为要实现的回调函数太多了。这也是一大堆样板。 描述了如何做到这一点,但它已经有几年历史了,所以我不知道是否有更新的方法。

  2. 从 ListAdapter 扩展您的适配器。 ListAdapter 的构造函数采用 DiffUtil.ItemCallback 参数。回调告诉它如何比较两个项目。只要您的模型项具有唯一的 ID 属性,这就很容易实现。使用 ListAdapter 时,您不会在 class 中创建自己的列表 属性,而是让 superclass 处理它。然后,不用设置新的过滤列表并调用 notifyDataSetChanged(),而是使用过滤列表调用 adapter.submitList(),它使用 DiffUtil 自动仅更改必要的视图,并且它也使用漂亮的动画来完成.请注意,您也不需要覆盖 getItemCount(),因为 superclass 拥有该列表。

由于您要过滤项目,您可能希望保留一个额外的 属性 来存储原始的未过滤列表,并在应用新过滤器时使用它。所以我在这个例子中确实创建了一个额外的列表 属性 。您需要注意仅使用它传递给 submitList() 并始终在 onBindViewHolder() 中使用 currentList 因为 currentList 是适配器实际用于显示的内容。

并且我删除了 Filterable 函数并使其成为外部 class 可以简单地设置 filter 属性.

class CoinsAdapter : ListAdapter<Coin, CoinsAdapter.ViewHolder>(CoinItemCallback) {
    
    object CoinItemCallback : DiffUtil.ItemCallback<Coin>() {
        override fun areItemsTheSame(oldItem: Coin, newItem: Coin): Boolean = oldItem.id == newItem.id
        override fun areContentsTheSame(oldItem: Coin, newItem: Coin): Boolean = oldItem == newItem
    }
    
    var coins: List<Coin> = emptyList()
        set(value) {
            field = value
            onListOrFilterChange()
        }

    var filter: CharSequence = ""
        set(value) {
            field = value
            onListOrFilterChange()
        }

    override fun onCreateViewHolder(parent: ViewGroup, position: Int): ViewHolder {
        val binding = ItemCoinBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val coin = currentList[position]

        holder.binding.coinImage.setImageResource(coin.image)
        holder.binding.coinText.text = builder.toString()
    }

    private fun onListOrFilterChange() {
        if (filter.length < 2) {
            submitList(coins)
            return
        }
        val pattern = filter.toString().lowercase().trim()
        val filteredList = coins.filter { pattern in it.name.lowercase() }
        submitList(filteredList)
    }

    inner class ViewHolder(val binding: ItemCoinBinding) : RecyclerView.ViewHolder(binding.root)
}

notifyDataSetChanged 重绘整个视图,这就是 Android Studio 显示警告的原因。

要解决这个问题,您可以使用 DiffUtil

private class CoinsAdapter(private val fragment: CoinFragment, private val coins: List<Coin>): RecyclerView.Adapter<CoinsAdapter.ViewHolder>(FilterDiffCallBack()), Filterable {
 ....
 ....
  //This check runs on background thread
class FilterDiffCallBack: DiffUtil.ItemCallback<Post>() {
    override fun areItemsTheSame(oldItem: Coin, newItem: Coin): Boolean {
      
        return oldItem.someUniqueId == newItem.someUniqueId
    }

    override fun areContentsTheSame(oldItem: Coin, newItem: Coin): Boolean {
        
        return oldItem == newItem
    }
}
...
...
override fun publishResults(constraint: CharSequence, results: FilterResults) {

        submitList(results)// call the DiffUtil internally
    }
}

如果列表中的数据主要随着用户交互而变化,那么您可以使用 notifyItemChanged(int)notifyItemInserted(int) notifyItemRemoved(int) 等方法,因为这是更新您的数据的最有效方式看法。更多信息 here