如何用DiffUtil提高应用性能

206 阅读5分钟

利用DiffUtil提高应用性能

移动应用程序的性能是决定用户体验的最重要方面之一。性能指的是应用程序在各种事件中加载和刷新数据的能力。

当你想在RecyclerView中更新数据时,你可能会使用notifyOnDataSetChanged()notifyOnItemPositionChanged() 方法。虽然这些方法在处理少量数据时效果很好,但对于你的应用程序来说,更新和完全重新加载大量的数据会变得很麻烦。

这很容易导致ANR(App Not Responding)异常,使你的应用程序的用户感到烦恼。一个恼火的用户总是会给出负面的反馈

通过使用DiffUtil ,我们可以轻松地处理数据集的变化,并即时反映在RecyclerView上。

DiffUtil是一个实用类,它计算两组数据之间的变化,并返回一个更新操作的列表,将旧列表改为新列表。它使用Eugene W. Myers的算法来计算将一个数据集转换为另一个数据集需要多少次更新。

由于Myers的技术不能处理已经被移动的对象,DiffUtil在输出上做了第二次尝试,以识别已经被重新定位的元素。这就节省了时间和设备的资源,同时为用户提供了更好的体验。

前提条件

本文假设你有以下知识。

  1. 使用[Android Studio]创建Android应用程序。
  2. [Kotlin编程语言]的基础知识。
  3. 在Android中使用命令式范式--使用XML 和视图组,如[Constraint Layout]。
  4. 视图绑定和/或[数据绑定]。
  5. [安卓回收器视图]的基础知识。

建立一个Android项目

打开Android Studio,用你选择的模板创建一个项目。确保你选择的API版本不低于16,以便使你的应用与DiffUtil兼容。

启用viewBinding

ViewBinding允许我们使用各自的绑定类从XML 布局文件访问视图和视图组。

// in the build.gradle file (module level)
android{
    buildFeatures{
        viewBinding true
    }
}

设置RecyclerView

在这一点上,我们将创建一个RecyclerView,并将其与核心组件设置在一起。

用户界面

打开你的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">

    <!--NOTE: This file is named activity_main.xml-->

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="8dp"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

    <Button
        android:id="@+id/btnUpdate"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        android:text="@string/update_data"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="@id/recyclerview"
        app:layout_constraintStart_toStartOf="@id/recyclerview"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.96" />
</androidx.constraintlayout.widget.ConstraintLayout>

这包括一个按钮,当点击时,将对Recyclerview进行更新操作。

行项目

这是决定Recyclerview中数据表示格式的单元构建块。出于兼容性和必要性的考虑,它也会跟随应用程序中的数据模型。

<?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="wrap_content"
    android:paddingBottom="8dp">

    <!--NOTE: This file is named student_card.xml-->

    <androidx.cardview.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:padding="8dp">

            <TextView
                android:id="@+id/studentId"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintHorizontal_bias="0.25"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                tools:text="00" />

            <TextView
                android:id="@+id/studentName"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintHorizontal_bias="0.25"
                app:layout_constraintStart_toEndOf="@id/studentId"
                app:layout_constraintTop_toTopOf="parent"
                tools:text="Example Name" />

        </androidx.constraintlayout.widget.ConstraintLayout>
    </androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>

在这里,我们创建了一个学生项目卡,它持有两个textViews。

应用程序的数据模型

一个数据集定义了适配器工作的数据对象的类型。创建一个名为Student 的Koltin数据类,并粘贴以下内容。

data class Student(
    val id: String,
    val name: String
)

回收器视图适配器

适配器完成了将数据集连接到Recyclerview的艰巨任务。创建一个名为StudentAdapter 的Kotlin类并粘贴以下内容。

class StudentAdapter : RecyclerView.Adapter<StudentAdapter.CardViewHolder>() {
    private var oldList = emptyList<Student>()

    inner class CardViewHolder(val card: StudentCardBinding) : RecyclerView.ViewHolder(card.root)

    override fun getItemCount(): Int = oldList.size

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CardViewHolder {
        return CardViewHolder(
            StudentCardBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        )
    }

    override fun onBindViewHolder(holder: CardViewHolder, position: Int) {
        holder.card.apply {
            studentId.text = oldList[position].id
            studentName.text = oldList[position].name
        }
    }
}

我们以后会用这个适配器来实现DiffUtil回调。

创建DiffUtil回调类

现在,继续创建一个名为MyDiffUtil 的Kotlin类并粘贴以下内容。

class MyDiffUtil(
    private val oldList: List<Student>,
    private val newList: List<Student>

) : DiffUtil.Callback() { // extending the Callback class
    override fun getOldListSize(): Int {
        return oldList.size
    }

    override fun getNewListSize(): Int {
        return newList.size
    }

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition].id == newList[newItemPosition].id
    }

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return when {
            oldList[oldItemPosition].id == newList[newItemPosition].id -> true
            oldList[oldItemPosition].name == newList[newItemPosition].name -> true
            else -> false
        }
    }
}

了解DiffUtil的回调

  • getOldListSize 返回在类的构造函数中传递的旧列表的大小。
  • getNewListSize 返回构造函数中传递的新或更新列表的大小。
  • areItemsTheSame 返回一个布尔值,即相应位置的项目是否相同。
  • areContentsTheSame 检查相应位置上的项目的内容是否相同。

注意: oldListnewList都应该持有相同类型的元素。

将DiffUtil附加到一个适配器上

导航到StudentAdapter.kt 文件,并创建一个公共函数,我们将用它来向适配器分配更新。

fun setData(newList: List<Student>) {
    val diffUtil = MyDiffUtil(oldList, newList)
    val diffUtilResults = DiffUtil.calculateDiff(diffUtil)

    oldList = newList
    diffUtilResults.dispatchUpdatesTo(this)
}

使用适配器

适配器已经准备好被使用了。如下图所示,把它附加到Recyclerview上。

class MainActivity : AppCompatActivity() {
    private var binding: ActivityMainBinding? = null
    private val studentAdapter: StudentAdapter = StudentAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding!!.root)

        updateDataWhenClicked()
        binding!!.recyclerview.apply {
            val newList: List<Student> = listOf(
                Student("1", "Milan Gans"),
                Student("2", "Renda Slaugh"),
                Student("3", "Basil Levison"),
                Student("4", "Tina Travers"),
            )
            adapter = studentAdapter.also { adapter ->
                adapter.setData(newList)
            }
        }
    }

    private fun updateDataWhenClicked() {
        binding!!.btnUpdate.setOnClickListener {
            val newList: List<Student> = listOf(
                Student("1", "Milan Gans"),
                Student("2", "Renda Slaugh"),
                Student("3", "Basil Levison"),
                Student("4", "Tina Travers"),
                Student("5", "Lil Mosey"),
                Student("6", "Maurine Alexa"),
            )
            studentAdapter.setData(newList)
        }
    }
}

在这里,我们用一个假的列表初始化了适配器。当更新按钮被点击时,RecyclerView就会被更新为一个新的假列表。

提示:当在一个生产项目上工作时,你应该从一个数据库中获取数据,最好是[房间数据库]或[远程数据库]。

测试应用程序

运行应用程序后,你应该看到与此类似的东西。

App testing image-gif

注意,只有新的项目被更新。这样一来,设备的资源,如电池和内存,就被节省了。因此,用户有一个快速和无缝的体验。

结论

就这样吧!你现在可以使用DiffUtil提高你的应用程序性能。