如何避免在可过滤适配器(Filterable Adapter)中使用 notifyDataSetChanged()?

7

我正在努力提高我的应用程序的稳定性和性能,但目前在Android Studio中卡在了一个警告上。请看下面的Adapter类:

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相关。 如果可以,始终使用更具体的更改事件效率会更高。只有在万不得已时才依赖notifyDataSetChanged。 然而,我不知道如何在这种情况下(使用过滤器)使用notifyDataSetChanged。什么是正确的方法,如何在这种情况下使用它?
2个回答

6
据我所知,在RecyclerView.Adapter中使用Filterable接口是没有意义的。Filterable旨在用于AdapterView适配器中,因为有一些小部件会检查Adapter是否为Filterable并自动提供一些过滤功能。然而,RecyclerView.Adapter与AdapterView的Adapter没有任何关系。
如果您喜欢,仍然可以使用Filter接口来组织您的代码,但对我来说,这似乎是不必要的额外样板文件。我曾经在StackOverflow上看到其他旧答案说要在RecyclerView.Adapter中实现Filterable,但我认为他们是出于与旧的Adapter类一起工作的习惯而这么做。
至于在过滤时改善适配器的性能,有几个选项。
  1. 使用 SortedList 和 SortedList.Callback 来管理您的列表。回调让您实现一堆函数来通知特定项目或项目范围的更改,而不是一次性更改整个列表。我没有使用过这个,因为有太多的回调函数需要实现,很容易出错。同时也需要大量的样板代码。 这里的最佳答案描述了如何做到这一点,但它已经几年过去了,所以我不知道是否有更新的方法。

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

由于您正在筛选项目,您可能希望保留额外的属性来存储原始未过滤的列表,并在应用新的过滤器时使用该属性。因此,在此示例中,我创建了一个额外的列表属性。请注意,只有在传递给submitList()时才使用它,并始终在onBindViewHolder()中使用currentList,因为currentList是适配器实际用于显示的内容。

并且我删除了可过滤函数,并使外部类可以简单地设置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)
}

我刚刚实现了你的解决方案,它非常有效。非常感谢你详细的解释和解决方案! - Ravers
@Tenfour04 我有一个类似的使用案例,涉及到ListAdapter,但是我对Kotlin没有经验。你能否用Java/Android代码行来展示你上面的例子? - AJW
老实说,我觉得你花时间阅读一下 Kotlin 官网上关于 Kotlin 语法的前几页,你就能理解它了,比我把它翻译成 Java 还要快。 - Tenfour04

2
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)等方法来更新视图,因为这是更新视图最有效的方式。更多信息请参见此处

1
当您滚动视图时,整个视图会被多次重绘。notifyDataSetChanged()很慢,因为它不仅会重新绘制整个布局,而且还会重新测量整个布局。此外,它还会导致闪烁,因为它不执行动画来显示内容的变化。 - Tenfour04

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接