DiffUtil + RecyclerView 在 Kotlin中的使用

很惭愧, 做了多年的Android开发还没有使用过DiffUtil这样解放双手的工具。

1 DiffUtil 用来解决什么问题?

List发生变化, 我们使用 RecyclerView.Adapter.notifyDataChanged很熟练了

  1. 如果List仅仅是一个item变化了,其他item没有变化怎么办? notifyItemChanged
  2. 如果List仅仅是一个item移除了,其他item没有移除怎么办? notifyItemRemoved
  3. 如果List部分item发生变化,其他的item都没有变化怎么办?
  4. 如果List部分item移除了,其他item没有移除怎么办?

有如下解决思路:

  1. 你可以无脑继续使用notifyDataChanged 但失去了性能
  2. 自己判断发生变化的item,自己调用notifyItemxxx
  3. DiffUtil帮你判断哪里发生了变化,并自动帮你调用 notifyItemxxx

2 DiffUtil 是什么?

DiffUtil is a utility class that calculates the difference between two lists and outputs a list of update operations that converts the first list into the second one.

It can be used to calculate updates for a RecyclerView Adapter. See ListAdapter and AsyncListDiffer which can simplify the use of DiffUtil on a background thread.

DiffUtil uses Eugene W. Myers’s difference algorithm to calculate the minimal number of updates to convert one list into another. Myers’s algorithm does not handle items that are moved so DiffUtil runs a second pass on the result to detect items that were moved.

DiffUtil 是一个实用程序类,它计算两个列表之间的差异并输出将第一个列表转换为第二个列表的更新操作列表。

它可用于计算 RecyclerView 适配器的更新。请参阅 ListAdapter 和 AsyncListDiffer,它们可以简化后台线程上 DiffUtil 的使用。

DiffUtil 使用 Eugene W. Myers 的差分算法来计算将一个列表转换为另一列表所需的最小更新次数。 Myers 的算法不处理已移动的项目,因此 DiffUtil 对结果运行第二遍以检测已移动的项目。

3 DiffUtil的使用

item_song_info.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:padding="16dp">

    <!-- Title TextView -->
    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Title"
        android:textSize="20sp"
        android:textStyle="bold" />

    <!-- Spacer View to add space between title and subtitle -->
    <View
        android:layout_width="8dp"
        android:layout_height="match_parent" />

    <!-- Subtitle TextView -->
    <TextView
        android:id="@+id/tv_sub_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Subtitle"
        android:textSize="16sp" />
</LinearLayout>

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

MusicBean.kt

data class MusicBean(var type: Int, var title: String, val subTitle: String)

MainActivity.kt

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val recyclerView: RecyclerView = findViewById(R.id.recyclerView)
        recyclerView.layoutManager = LinearLayoutManager(this)
        val adapter = MyAdapter()
        recyclerView.adapter = adapter

        adapter.data = getSampleDataA()

        Handler(Looper.getMainLooper()).postDelayed({
            adapter.data = getSampleDataB()
        }, 2000)
    }


    // 用于生成初始数据
    private fun getSampleDataA(): List<MusicBean> {
        val data = mutableListOf<MusicBean>()
        for (i in 1..10) {
            MusicBean(type = i, title = "ItemA $i", subTitle = "subTitle $i").let {
                data.add(it)
            }
        }
        return data
    }

    // 用于生成变化后的数据
    private fun getSampleDataB(): List<MusicBean> {
        val data = mutableListOf<MusicBean>()
        for (i in 1..10) {
            val tag = if (i <= 5) {
                "B"
            } else "A"
            MusicBean(type = i, title = "Item$tag $i", subTitle = "subTitle $i").let {
                data.add(it)
            }
        }
        return data
    }


    class MyAdapter : RecyclerView.Adapter<MyAdapter.MyViewHolder>() {

        var data: List<MusicBean> = emptyList()
            set(value) {
                // 如果比较的集合较多(比如超过1000个), 建议使用子线程去比较
                val diffResult = DiffUtil.calculateDiff(MyDiffCallback(field, value))
                // 旧值赋新值
                field = value
                // 这里一定要保证在主线程调用
                diffResult.dispatchUpdatesTo(this)
            }

        class MyDiffCallback(
            private val oldList: List<MusicBean>, private val newList: List<MusicBean>
        ) : DiffUtil.Callback() {

            override fun getOldListSize(): Int = oldList.size
            override fun getNewListSize(): Int = newList.size

            override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
                // 把这里想成是比较holder的类型, 比如纯文本的holder和纯图片的holder的type肯定不同
                return oldList[oldItemPosition].type == newList[newItemPosition].type
            }

            override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
                // 把这里想成是同一种holder的比较,比如都是纯文本holder,但是title不一致
                return oldList[oldItemPosition].title == newList[newItemPosition].title
            }
        }

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
            val view =
                LayoutInflater.from(parent.context).inflate(R.layout.item_song_info, parent, false)
            return MyViewHolder(view)
        }

        override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
            val item = data[position]
            holder.bind(item)
        }

        override fun getItemCount(): Int {
            return data.size
        }

        class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

            fun bind(item: MusicBean) {
                val tvTitle: TextView = itemView.findViewById(R.id.tv_title)
                val tvSubTitle: TextView = itemView.findViewById(R.id.tv_sub_title)
                tvTitle.text = item.title
                tvSubTitle.text = item.subTitle
            }
        }
    }
}

在这里插入图片描述

4 参考文章

DiffUtil 官方介绍
将 DiffUtil 和数据绑定与 RecyclerView 结合使用
DiffUtil和它的差量算法
DiffUtils 遇到 Kotlin,榨干视图局部刷新的最后一滴性能

  • 23
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个使用 Kotlin 和 Paging3+RecyclerView 实现数据分页的示例: 1. 首先,我们需要定义一个包含数据的列表和当前页码的类,使用 PagingData 类型: ``` data class Data( val id: Int, val name: String, val description: String ) ``` 2. 接下来,我们需要定义一个 PagingSource,它负责从数据源获取数据: ``` class DataPagingSource(private val api: DataApi) : PagingSource<Int, Data>() { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Data> { try { val page = params.key ?: 1 val pageSize = params.loadSize val response = api.getData(page, pageSize) val data = response.data val prevKey = if (page == 1) null else page - 1 val nextKey = if (data.isEmpty()) null else page + 1 return LoadResult.Page( data = data, prevKey = prevKey, nextKey = nextKey ) } catch (e: Exception) { return LoadResult.Error(e) } } override fun getRefreshKey(state: PagingState<Int, Data>): Int? { return state.anchorPosition?.let { anchorPosition -> val anchorPage = state.closestPageToPosition(anchorPosition) anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) } } } ``` 3. 然后,我们需要定义一个 PagingData 类型的数据流,并使用它创建一个 PagingDataAdapter: ``` class DataAdapter : PagingDataAdapter<Data, DataViewHolder>(DataDiffCallback) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.item_data, parent, false) return DataViewHolder(view) } override fun onBindViewHolder(holder: DataViewHolder, position: Int) { val item = getItem(position) holder.bindData(item) } object DataDiffCallback : DiffUtil.ItemCallback<Data>() { override fun areItemsTheSame(oldItem: Data, newItem: Data): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: Data, newItem: Data): Boolean { return oldItem == newItem } } } ``` 4. 最后,在 Activity 或 Fragment 使用 RecyclerView 和 DataAdapter: ``` class MainActivity : AppCompatActivity() { private lateinit var recyclerView: RecyclerView private lateinit var adapter: DataAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) recyclerView = findViewById(R.id.recycler_view) recyclerView.layoutManager = LinearLayoutManager(this) adapter = DataAdapter() recyclerView.adapter = adapter val api = DataApi() val pagingSourceFactory = { DataPagingSource(api) } val pagingConfig = PagingConfig(pageSize = 10) val dataFlow = Pager( config = pagingConfig, pagingSourceFactory = pagingSourceFactory ).flow dataFlow.cachedIn(lifecycleScope).collectLatest { pagingData -> adapter.submitData(pagingData) } } } ``` 这是一个简单的示例,你可以根据自己的需求进行修改和扩展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值