Android basado en el paquete DataBinding RecyclerView para lograr un rápido desarrollo de listas

1. Introducción

En el desarrollo de aplicaciones móviles, el componente de lista es un componente de interfaz de usuario muy común. El componente de lista se usa en la mayoría de los desarrollos de aplicaciones para el desarrollo de interfaz. En el desarrollo de Android, el componente de lista generalmente usa RecyclerView proporcionado oficialmente, mientras que el desarrollo regular de RecyclerView es necesario crear manualmente el código correspondiente del Adaptador y ViewHolder, y cada uso de RecyclerView necesita escribir este tipo de código repetitivo, hay códigos duplicados, lo que reduce la eficiencia de desarrollo, por lo que para mejorar la eficiencia de desarrollo de la lista (perezoso) , hay varios pares El marco encapsulado por RecyclerView para simplificar su proceso de desarrollo, este artículo es la implementación de uno de los marcos.

Primero nombremos el marco, emmm... Es realmente difícil nombrar algo, es mucho más difícil que escribir código, y finalmente me devané los sesos para pensar en uno ardf, la abreviatura de "marco de desarrollo rápido de Android" en inglés, que es " Marco de desarrollo rápido de Android", genial, el 50% del trabajo realizado en este marco.

2. Ideas de implementación

Después de pensar en el nombre, el siguiente paso es pensar en cómo implementarlo. El propósito central del marco es simplificar el proceso de desarrollo. Para el desarrollo de RecyclerView, la creación de RecyclerView y el diseño de los elementos son definitivamente esenciales. por lo tanto, solo se puede hacer desde Adapter y ViewHolder Para simplificar, las funciones de Adapter y ViewHolder son principalmente para cargar el diseño del elemento y procesar los datos de visualización y los eventos del elemento. Si esta pieza se puede hacer general, no hay necesita crear Adapter y ViewHolder cada vez.

Finalmente, pensé en usar DataBinding para la encapsulación. A través de la extensión de DataBinding, el diseño del elemento, los datos de la lista y los eventos se configuran en el Adaptador a través de xml. En el Adaptador, el archivo de diseño del elemento se carga a través de DataBinding. Finalmente, un ViewHolder es creado y enlazado a datos, para reducir el código de desarrollo de Adapter y ViewHolder.

DataBinding es un marco de vinculación de datos oficial de Google. Con esta biblioteca, puede vincular declarativamente la fuente de datos en la aplicación a los componentes de la interfaz en el diseño y realizar la actualización de la interfaz basada en datos, reduciendo así el acoplamiento entre el diseño y la lógica para hacer que la lógica del código sea más clara. Para obtener más información sobre DataBinding, consulte la documentación oficial de Google: DataBinding

Comparación del proceso de desarrollo después del envasado y antes del envasado:

Se puede encontrar que no hay necesidad de crear Adapter y ViewHolder ardfdespués de , y la forma de configurar los datos se cambia a la forma de usar el enlace DataBinding, lo que reduce el acoplamiento entre la interfaz y la lógica, lo que reduce en gran medida la escritura y mejora del código repetitivo. eficiencia del desarrollo.

3. Uso

Dado que es un marco para mejorar la eficiencia del desarrollo y simplificar el proceso de desarrollo, primero veamos cómo es el efecto de uso real, si es tan bueno como se dice, y muéstrame el código.

3.1 Introducción a los atributos extendidos

ardfExtiende una serie de propiedades de RecycleView a través de BindingAdapter de DataBinding, que se utiliza para configurar rápidamente RecyclerView en diseño xml. Toda la configuración de RecyclerView se puede completar sin escribir código java/kotlin, incluidos datos de lista, diseño de elementos, eventos, etc. Las propiedades de configuración son como sigue:

nombre de la propiedad escribe describir
datos Lista Recopilación de datos mostrada por RecycleView
diseño del elemento En t El ID de recurso del diseño del elemento.
itemViewType ItemViewTypeCreator 多 type item 的生成器,用于获取 item 类型和对应类型的 layout 资源 id
itemClick OnItemClickListener item 点击事件,将item点击事件在布局里直接代理到 ViewModel 里
itemEventHandler Any item 内部事件处理器,用于代理 item 内部事件的处理

具体使用方法可参考 3.3、3.4、3.5、3.6 的使用介绍。

3.2 项目配置

在项目 Module 的 build.gradle 文件中添加封装好的依赖库,已经上传 mavenCentral,如下:

dependencies {
    implementation 'com.loongwind.ardf:recyclerview-ext:1.0.0'
}
复制代码

ardf基于 DataBinding 实现,所以需要使用该库的 Module 的 build.gradle 里的 android 配置下启用 DataBinding,启用方式如下:

android {
    ...
    buildFeatures {
        dataBinding true
    }
}
复制代码

同时在插件中添加 kotlin-kapt的插件,如下:

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt'
}
复制代码

ardf 的使用配置就完成了,点击 Sync Now同步 build.gradle 生效后即可进行代码开发。

3.3 简单使用

先看一下结合 MVVM 架构如何快速实现简单的列表数据显示以及列表数据更新功能。

3.3.1 准备列表数据

先创建一个 ViewModel 用于存放列表的数据,这里主要演示列表的开发就直接用一个普通的类而不是 Jetpack 的 ViewModel 库,代码如下:

class RecycleViewModel(){
    val data = ArrayList<String>()

    init {
        for (i in 0..5){
            data.add("Item $i")
        }
    }
}
复制代码

代码很简单,有一个 List 类型的 data 变量,里面存放的是 String 类型的数据,在初始化的时候向里面添加了 5 条测试数据。

3.3.2 创建 item 布局

创建列表的 item 布局文件 layout_item.xml, 简单添加一个 TextView 进行演示,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <!--通过 DataBinding 接收 item 数据-->
        <variable
            name="item"
            type="String" />

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:paddingVertical="2dp">

        <TextView
            android:id="@+id/text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:padding="15dp"
            android:text="@{item}" // 使用 DataBinding 进行数据绑定
            android:background="#7AEDEBEB"/>

    </LinearLayout>
</layout>
复制代码

布局里通过 DataBinding 传入了一个 String 类型的 item 变量,并将这个变量绑定到了 TextView 的 text 属性上,即对 TextView 设置显示的字符串值,这里需要注意以下两点:

  • 变量名必须为 item,因为这是框架里封装好的,名称不对无法自动接收传递过来的数据
  • item 的数据类型需跟前面 ViewModel 中定义的列表中的数据类型一致,也就是与上面定义的 data 里子元素类型一致

3.3.3 创建 RecyclerView

数据和 item 布局都准备好了,下面就是在页面的 activity_recycleview_simple.xml 布局里创建 RecyclerView 了,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <!--  通过 DataBinding 接收 ViewModel 实例  -->
        <variable
            name="viewModel"
            type="com.loongwind.ardf.demo.RecycleViewModel" />

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.recyclerview.widget.RecyclerView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:data="@{viewModel.data}"  // 绑定列表数据
            app:itemLayout="@{@layout/layout_item}"/>  // 绑定 item 布局


    </LinearLayout>
</layout>
复制代码

布局里通过 DataBinding 接收一个 RecycleViewModel 类型的 viewModel 变量,也就是第 1 步准备数据的 RecycleViewModel 类的实例。

xml 里 RecyclerView 设置主要分为三步:

  • 设置 layoutManger
  • 通过 data属性绑定列表数据
  • 通过 itemLayout 属性绑定 item 布局

一定不要忘了设置 layoutManger,在实际开发中经常有小伙伴忘记设置这个属性导致列表不显示而排查半天原因浪费大量的时间

3.3.4 Activity 中使用

接下来就是在 Activity 中使用了,即加载第 3 步创建的 layout 布局,并将 RecycleViewModel 的实例通过 DataBinding 传到布局里去。代码如下:

class RecycleViewSimpleActivity : AppCompatActivity(){


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 加载页面显示布局,通过 DataBindingUtil.setContentView 方式加载
        // ActivityRecycleviewSimpleBinding 是 DataBinding 插件根据布局文件自动生成
        val binding = DataBindingUtil.setContentView<ActivityRecycleviewSimpleBinding>(
            this,
            R.layout.activity_recycleview_simple
        )

        // 绑定数据
        binding.viewModel = RecycleViewModel(this)
    }

}
复制代码

通过 DataBinding 加载界面布局,然后绑定界面数据源。代码实现就完成了,运行一下看看效果:

可以发现整个实现过程中没有涉及 Adapter 和 ViewHolder,是不是比较省时省力。

3.3.5 数据更新

列表数据已经展示出来了,但却是静态数据,那么如何实现列表数据的动态更新呢,这就需要用到 DataBinding 提供的可观察者对象 Observable ,它是一个数据容器,里面存放的是我们需要的实际数据,当 Observable 中的数据发生变化时就会通知订阅它的观察者,Observable 提供了一个 List 的观察者容器 ObservableArrayList ,这里我们只需要将原来定义的 List 类型的 data 修改为 ObservableArrayList 即可,代码如下:

val data = ObservableArrayList<String>()
复制代码

当我们对 data 中的数据进行更新的时候,就会自动刷新界面更新界面上显示的数据,下面为了演示在页面布局里添加两个按钮分别进行添加数据和删除数据的操作,如下:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="viewModel"
            type="com.loongwind.ardf.demo.RecycleViewModel" />

    </data>

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

        <androidx.recyclerview.widget.RecyclerView
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:layout_constraintVertical_weight="1"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toTopOf="@+id/add_item"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:data="@{viewModel.data}"
            app:itemLayout="@{@layout/layout_item}"/>

        <Button
            android:id="@+id/add_item"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            android:layout_marginStart="20dp"
            android:layout_marginBottom="20dp"
            app:layout_constraintRight_toLeftOf="@id/del_item"
            android:text="添加item"
            android:onClick="@{()->viewModel.addItem()}"/>


        <Button
            android:id="@+id/del_item"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toTopOf="@id/add_item"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintLeft_toRightOf="@id/add_item"
            android:layout_marginEnd="20dp"
            android:text="删除item"
            android:onClick="@{()->viewModel.deleteItem()}"/>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
复制代码

按钮的点击事件也是通过 DataBinding 绑定到 ViewModel 的对应方法,也就是这里的 addItem()deleteItem(),ViewModel 中代码如下:

class RecycleViewModel(var view: IView){
  
    ...
      
    fun addItem(){
        data.add("Item ${data.size}")
    }

    fun deleteItem(){
        data.removeAt(data.size - 1)
    }
}
复制代码

演示代码简单实现了添加 item 和删除 item 的方法。运行一下看一下效果:

3.4 item 点击事件

item 的点击事件处理是列表开发中常见的事件处理,如点击列表 item 跳转到对应的详情页,ardf也对 item 的点击事件进行了封装,只需要在 xml 中通过 itemClick 为 RecyclerView 绑定点击事件即可,代码如下:

<androidx.recyclerview.widget.RecyclerView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:data="@{viewModel.data}"
            app:itemLayout="@{@layout/layout_item}"
            app:itemClick="@{(item,position)-> viewModel.onItemClick(item)}"/>
复制代码

通过 DataBinding 将 item 的点击事件代理到 ViewModel 的 onItemClick 方法,onItemClick 方法是我们在 ViewModel 中自定义创建的,如下:

class RecycleViewModel(var view: IView){

    ...
    
    fun onItemClick(item:Any?){
        if(item is String){
            view.toast(item)
        }
    }
}
复制代码

onItemClick 的参数是一个 Any? 类型,在布局 xml 中传入的是 item 的数据,所以需要判断数据类型与 item 的数据类型是否一致,再进行业务处理。

此处为了方便展示测试效果,通过自定义 IView 接口实现了 Toast 弹窗提示

运行效果如下:

3.5 Item 内部事件

对于复杂的业务可能需要在 item 内部进行事件处理,比如 item 上有可操作按钮、选择框等,ardf也对 item 内部事件的处理进行了封装,只需要在 xml 中通过 itemEventHandler 属性为 RecyclerView 绑定Item内部点击事件即可,如下:

<androidx.recyclerview.widget.RecyclerView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:data="@{viewModel.data}"
            app:itemLayout="@{@layout/layout_item}"
            app:itemEventHandler="@{viewModel}"/>
复制代码

通过 itemEventHandler 将 ViewModel 传递到了 item 布局,在 item 布局里将 item 的内部事件代理到 ViewModel 内进行处理,item 布局代码如下:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="item"
            type="String" />

        <variable
            name="handler"
            type="com.loongwind.ardf.demo.RecycleViewModel" />

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            android:gravity="center"
            android:padding="15dp"
            android:text="@{item}"/>

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            android:text="删除"
            android:layout_marginRight="10dp"
            android:onClick="@{()->handler.eventDeleteItem(item)}"/>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
复制代码

item 布局里通过 handler接收传进来的 itemEventHandler对象,类型需跟 itemEventHandler 传递的类型一致,这里演示在 item 布局里添加一个删除按钮,再将删除按钮的点击事件代理到 ViewModel 的 eventDeleteItem方法,该方法也是在 ViewModel 中自定义创建的,如下:

class RecycleViewModel(var view: IView){

    ...
    
    fun eventDeleteItem(item:String){
        data.remove(item)
    }
}
复制代码

该方法接收了一个 String 类型的 item 数据,实现从列表中移除该 item 数据,效果如下所示:

3.6 不同类型的 item 布局

RecyclerView 是支持不同类型的 item 布局的,ardf也通过提供 itemViewType属性的配置来实现不同类型 item 布局的展示。

itemViewType 属性需传入一个 ItemViewTypeCreator类型的对象,ItemViewTypeCreator是一个接口类型,定义如下:

interface ItemViewTypeCreator{
    /**
     * 通过 item 下标和数据返回布局类型
     * @param position item 下标
     * @param item item 数据
     * @return item 布局类型
     */
    fun getItemViewType(position: Int, item: Any?) : Int
    
    /**
     * 通过 item 布局类型返回布局资源 id
     * @param viewType item 数据类型
     * @return item 布局资源 id
     */
    fun getItemLayout(viewType: Int) : Int
}
复制代码

在 ViewModel 创建一个 ItemViewTypeCreator 的对象实例,如下:

class MultiItemViewModel(var view: IView){
    // List 的 item 数据类型改为 Any
    val data = ObservableArrayList<Any>()
    
    // 定义多 item 布局类型的创建器
    val itemViewTypes = object : BaseBindingAdapter.ItemViewTypeCreator{
        override fun getItemViewType(position: Int, item: Any?): Int {
            // 通过 item 数据类型返回不同的布局类型
            return if(item is String){
                0
            }else{
                1
            }
        }

        override fun getItemLayout(viewType: Int): Int {
            // 根据不同的布局类型返回不同的布局资源 id
            return if(viewType == 0){
                R.layout.layout_item
            }else{
                R.layout.layout_item2
            }
        }

    }
    init {
        // 添加测试数据
        for (i in 0..10){
            // 双数添加字符串数据,单数添加 User 数据
            if(i % 2 == 0){
                data.add("Item $i")
            }else{
                data.add(User(name = "Name $i", img = "https://picsum.photos/200"))
            }

            println(data)
        }
    }
}
复制代码

创建了一个 MultiItemViewModel 类用于演示实现不同类型 item 布局的处理,实例化一个 ItemViewTypeCreator 类的对象实现 item 类型和布局的返回。

将 data 类型修改为 ObservableArrayList<Any>用于存放不同类型的 item 数据。

User 的 item 布局《代码如下:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <!-- DataBinding 接收 item 数据,数据类型为 User -->
        <variable
            name="item"
            type="com.loongwind.ardf.demo.User" />

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#3703A9F4">
        
        <!-- 用户头像,并绑定点击事件 -->
        <ImageView
            android:layout_width="32dp"
            android:layout_height="32dp"
            android:src="@mipmap/ic_launcher"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            android:layout_marginLeft="30dp"
            android:onClick="@{()->handler.omImgClick(item)}"/>

        <!-- 用户名称 -->
        <TextView
            android:id="@+id/text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            android:gravity="center"
            android:padding="15dp"
            android:text="@{item.name}"/>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
复制代码

将接收的 item 数据类型换成 User。

最后在页面布局中的 RecyclerView 上配置 itemViewType 属性,如下:

<androidx.recyclerview.widget.RecyclerView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:data="@{viewModel.data}"
            app:itemViewType="@{viewModel.itemViewTypes}"/>
复制代码

运行一下看一下效果:

4. 源码解析

上面介绍了 ardf 的使用方法,ardf的核心实现是封装了通用的 Adapter 和 ViewHolder,然后通过 DataBinding 的 @BindingAdapter扩展支持将 RecyclerView 的常用设置在 xml 里进行配置。

整体结构关系图如下:

从图上可以发现,ardf核心为以下三个模块:

  • ViewHolder 的封装:BindingViewHolder,实现 item 数据和内部事件的绑定
  • Adapter 的封装: BaseBindingAdapterDefaultBindingAdapter,实现列表数据变化的监听、根据 item 布局创建 ViewHolder 并绑定事件
  • @BindingAdapter 扩展:setData方法,关联 RecyclerView 与 Adapter

接下来将从源码层面向大家介绍该封装的详细实现。

4.1 ViewHolder

创建一个 BindingViewHolder 类继承自 RecyclerView.ViewHolder :

class BindingViewHolder<T, BINDING : ViewDataBinding>(val binding: BINDING) 
    : RecyclerView.ViewHolder(binding.root){

    fun bind(t: T?) {
        binding.setVariable(BR.item, t)
    }

    fun setItemEventHandler(handler:Any?){
        binding.setVariable(BR.handler, handler)
    }
}
复制代码

该类有两个泛型,T为 item 的数据类型,BINDING为 item 布局生成的 ViewDataBinding 类。传入的参数 binding 即为 BINDING 类型,然后通过 binding.root获取布局的实际 View 将其传给 RecyclerView.ViewHolder。

BindingViewHolder 还对外提供了两个方法,bindsetItemEventHandler方法。

bind 是用于绑定数据,即将 item 的数据和布局绑定起来,这里是通过 binding.setVariable(BR.item, t)将数据传递到布局里的 item 变量;

setItemEventHandler 是设置 item 内部事件处理的对象,绑定到布局的 handler 变量。

这里的 BR.itemBR.handler是 DataBinding 根据布局里使用的变量自动生成的,所以为了生成这两个变量,建了一个空的布局文件,定义了这两个变量,如下:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <variable
            name="item"
            type="Object" />
        <variable
            name="handler"
            type="Object" />

    </data>

</layout>
复制代码

4.2 Adapter

创建好通用的 ViewHolder 以后,接下来就是封装通用的 Adapter,为了便于扩展先创建一个抽象的 BaseBindingAdapter定义如下:

abstract class BaseBindingAdapter<T:Any, BINDING : ViewDataBinding> :
    RecyclerView.Adapter<BindingViewHolder<T, BINDING>>() {
        ...
}
复制代码

跟 BindingViewHolder 一样有两个泛型,Adapter 的 ViewHolder 泛型类型就是上面创建的 BindingViewHolder。

4.2.1 数据处理

类定义好后,接下来就是具体的实现,因为需要向 Adapter 中设置数据,所以需要定义一个 data 变量用于接收列表的数据源,并重写其 set 方法,代码如下:

/**
 * 列表数据
 */
var data: List<T>? = null
    @SuppressLint("NotifyDataSetChanged")
    set(data) {
        field = data
        // 判断如果是 ObservableList 类型,则为其添加 changeCallback 回调
        if (data is ObservableList<*>) {
            // 如果 listener 为空则创建 ObserverListChangeListener 对象,传入当前 Adapter
            if (listener == null) {
                listener = ObserverListChangeListener(this)
            }
            // 将已添加的 listener 移除,防止添加多个导致重复回调
            (data as ObservableList<T>).removeOnListChangedCallback(listener)

            // 设置 List 数据改变回调
            data.addOnListChangedCallback(listener)
        }
        // 刷新界面数据
        notifyDataSetChanged()
    }
复制代码

data 用于接收设置的列表数据,重写了 set 方法,如果设置的数据类型是 ObservableList 则为其添加数据改变的回调。回调ObserverListChangeListener的代码如下:

class ObserverListChangeListener<T>(private val adapter:  RecyclerView.Adapter<*>) : ObservableList.OnListChangedCallback<ObservableList<T>>() {
    @SuppressLint("NotifyDataSetChanged")
    override fun onChanged(sender: ObservableList<T>) {
        adapter.notifyDataSetChanged()
    }

    override fun onItemRangeRemoved(sender: ObservableList<T>, positionStart: Int, itemCount: Int) {
        adapter.notifyItemRangeRemoved(positionStart, itemCount)
    }

    override fun onItemRangeMoved(sender: ObservableList<T>, fromPosition: Int, toPosition: Int, itemCount: Int) {
        adapter.notifyItemMoved(fromPosition, toPosition)
    }

    override fun onItemRangeInserted(sender: ObservableList<T>, positionStart: Int, itemCount: Int) {
        adapter.notifyItemRangeInserted(positionStart, itemCount)
    }

    override fun onItemRangeChanged(sender: ObservableList<T>, positionStart: Int, itemCount: Int) {
        adapter.notifyItemRangeChanged(positionStart, itemCount)
    }
}
复制代码

构造参数传入了 RecyclerView.Adapter ,在每个数据变化的回调中调用 Adapter 的对应刷新数据的方法,实现数据变化自动刷新界面。

数据有了,getItemCount方法的实现就有了,同时为了方便根据 position 获取 item 的数据,这里也提取了一个 getItem方法,实现如下:

    fun getItem(position: Int): T? {
        return data?.getOrNull(position)
    }
    
        override fun getItemCount(): Int {
        return data?.size ?: 0
    }
复制代码

4.2.2 创建布局

定义一个 layoutRes用于接收 item 布局的资源 id,如下:

    @get:LayoutRes
    abstract val layoutRes: Int
复制代码

这里定义的是一个抽象 get 方法,需要子类去实现返回具体的 item 布局的资源 id。

定义 itemViewTypeCreator用于接收有多种 item 布局类型时的布局数据:

var itemViewTypeCreator: ItemViewTypeCreator? = null
复制代码

实现 getItemViewType处理 item 布局类型:

   override fun getItemViewType(position: Int): Int {
        return itemViewTypeCreator?.getItemViewType(position, getItem(position))
            ?: super.getItemViewType(position)
    }
复制代码

代码很好理解,如果 ItemViewTypeCreator 不为空则调用 getItemViewType 方法返回布局类型,如果为空则调用 super 方法,即默认的 item 布局类型。

然后实现 onCreateViewHolder方法,源码如下:

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder<T, BINDING> {
    val layout = itemViewTypeCreator?.getItemLayout(viewType) ?: layoutRes
    val binding = DataBindingUtil.inflate<BINDING>(LayoutInflater.from(parent.context), layout, parent, false)
    val holder = BindingViewHolder<T, BINDING>(binding)
    bindClick(holder, binding)
    return holder
}
复制代码

先判断 itemViewTypeCreator是否为空,不为空就调用 getItemLayout方法获取布局 id,为空则直接使用 layoutRes;获取到 item 布局的资源 id 后就可以通过 DataBindingUtil.inflate方法创建布局的 ViewDataBinding,再通过 binding 创建 ViewHolder 并返回。

4.2.3 绑定数据&事件

onCreateViewHolder 中创建完 holder 后还调用了一个 bindClick方法,用于绑定 item 的事件,bindClick的实现如下:

    protected fun bindClick(holder: BindingViewHolder<*, *>, binding: BINDING) {
        binding.root.setOnClickListener {
            val position = holder.layoutPosition
            itemClickListener?.onItemClick(getItem(position), position)
        }
    }
复制代码

通过 binding.root获取 item 的 View 对象,然后对其设置点击事件,在事件的处理里调用 itemClickListener?.onItemClick,即布局里传入的 item 点击事件, itemClickListener的定义如下:

var itemClickListener: OnItemClickListener<T>? = null
  
interface OnItemClickListener<T> {
    fun onItemClick(t: T?, position: Int)
}
复制代码

最后实现 onBindViewHolder方法进行数据绑定:

override fun onBindViewHolder(holder: BindingViewHolder<T, BINDING>, position: Int) {
    holder.bind(getItem(position))
    holder.setItemEventHandler(itemEventHandler)
}
复制代码

先调用 holder.bind绑定数据,然后调用 holder.setItemEventHandler设置 item 内部事件的处理对象。

完整的 BaseBindingAdapter源码如下:

abstract class BaseBindingAdapter<T:Any, BINDING : ViewDataBinding> :
    RecyclerView.Adapter<BindingViewHolder<T, BINDING>>() {

    var itemClickListener: OnItemClickListener<T>? = null
    private var listener: ObserverListChangeListener<T>? = null
    var itemViewTypeCreator: ItemViewTypeCreator? = null
    var itemEventHandler : Any? = null

    var data: List<T>? = null
        @SuppressLint("NotifyDataSetChanged")
        set(data) {
            field = data
            //如果是ObservableList则为其添加changeCallback
            if (data is ObservableList<*>) {
                if (listener == null) {
                    listener = ObserverListChangeListener(this)
                }
                (data as ObservableList<T>).removeOnListChangedCallback(listener)
                data.addOnListChangedCallback(listener)
            }
            notifyDataSetChanged()
        }


    @get:LayoutRes
    abstract val layoutRes: Int

    fun getItem(position: Int): T? {
        return data?.getOrNull(position)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder<T, BINDING> {
        val layout = itemViewTypeCreator?.getItemLayout(viewType) ?: layoutRes
        val binding = DataBindingUtil.inflate<BINDING>(LayoutInflater.from(parent.context), layout, parent, false)
        val holder = BindingViewHolder<T, BINDING>(binding)
        bindClick(holder, binding)
        return holder
    }

    override fun getItemViewType(position: Int): Int {
        return itemViewTypeCreator?.getItemViewType(position, getItem(position))
            ?: super.getItemViewType(position)
    }

    override fun onBindViewHolder(holder: BindingViewHolder<T, BINDING>, position: Int) {
        holder.bind(getItem(position))
        holder.setItemEventHandler(itemEventHandler)
    }

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

    interface OnItemClickListener<T> {
        fun onItemClick(t: T?, position: Int)
    }


    protected fun bindClick(holder: BindingViewHolder<*, *>, binding: BINDING) {
        binding.root.setOnClickListener {
            val position = holder.layoutPosition
            itemClickListener?.onItemClick(getItem(position), position)
        }
    }

    interface ItemViewTypeCreator{
        fun getItemViewType(position: Int, item: Any?) : Int
        fun getItemLayout(viewType: Int) : Int
    }

}
复制代码

4.2.4 通用 Adapter

BaseBindingAdapter类有一个 get 的 layoutRes 是抽象方法,需要子类传入一个 item 布局资源 id ,这里定义了一个通用也是默认的 DefaultBindingAdapter类:

class DefaultBindingAdapter(@param:LayoutRes @field:LayoutRes override val layoutRes: Int)
    : BaseBindingAdapter<Any, ViewDataBinding>()
复制代码

只传入了一个参数,即 item 布局 id,将其作为 layoutRes 的 get 返回值。

4.3 @BindingAdapter

Adapter 准备好后,就可以通过 @BindingAdapter 将其与 RecyclerView 进行关联,实现在 xml 中配置数据源、布局和相关事件等数据。

DataBinding 实现在 xml 里绑定数据的本质是通过调用 View 对应属性的 set 方法来实现,如果 View 没有对应的 set 方法,就需要通过 @BindingAdapter 来扩展一个 set 方法来实现。这里为 RecyclerView 扩展了一个 setData 的方法,源码如下:

@BindingAdapter(value = ["data", "itemLayout", "itemClick","itemViewType", "itemEventHandler"], requireAll = false)
fun setData(
    recyclerView: RecyclerView,
    data: List<Any>?,
    @LayoutRes itemLayout: Int,
    listener: BaseBindingAdapter.OnItemClickListener<Any>?,
    itemViewTypeCreator: BaseBindingAdapter.ItemViewTypeCreator?,
    itemEventHandler: Any?
) {
    val adapter = recyclerView.adapter
    if (adapter == null) {
        val defaultBindingAdapter = DefaultBindingAdapter(itemLayout)
        defaultBindingAdapter.data = data
        defaultBindingAdapter.itemClickListener = listener
        defaultBindingAdapter.itemViewTypeCreator = itemViewTypeCreator
        defaultBindingAdapter.itemEventHandler = itemEventHandler
        recyclerView.adapter = defaultBindingAdapter
    } else if (adapter is BaseBindingAdapter<*, *>) {
        (adapter as BaseBindingAdapter<Any, ViewDataBinding>).data = data
        adapter.itemViewTypeCreator = itemViewTypeCreator
        adapter.itemClickListener = listener
        adapter.itemEventHandler = itemEventHandler
    }
}
复制代码

要让 DataBinding 识别这个 set 方法需要在方法上加 @BindingAdapter 的注解,同时在注解中声明其在 xml 可配置的对应属性的名称,其传入的数据与该方法的参数除第一个参数以外一一对应,第一个参数则应用的 View 本身;注解上还有一个 requireAll参数,表示是否需要所有属性都在 xml 里配置了才能匹配使用该方法,这里设置的 false,即表示不用全都配置也能匹配到该方法。

具体实现首先获取 RecyclerView 当前的 adapter,如果当前 adapter 为空则创建一个 DefaultBindingAdapter ,然后设置列表数据、item 点击事件、多 item 布局类型的创建器、item 内部事件处理器,最后把 adapter 设置给 RecyclerView;如果 adapter 不为空,且类型为 BaseBindingAdapter则重新设置一遍 adapter 的对应数据 。

Aquí se presenta la lógica de implementación y el código fuente de todo el paquete. Se encuentra que no hay mucho código y que la implementación del paquete no es complicada, pero el efecto de uso real es muy bueno.

5. Finalmente

Después de encapsular RecyclerView en función de DataBinding, no hay necesidad de repetir la escritura de código repetitivo de Adapter y ViewHolder al desarrollar la función de lista, de modo que los desarrolladores puedan concentrarse más en la restauración del diseño de la interfaz de usuario del elemento de la función de lista en sí misma y el procesamiento de la lógica de datos, para mejorar la eficiencia del desarrollo, reducir en gran medida el acoplamiento entre el diseño y la lógica, y también facilitar las pruebas unitarias correspondientes durante el desarrollo para mejorar mejor la calidad del desarrollo.

Dirección de origen: ardf

mavenCentral:com.loongwind.ardf:recyclerview-ext:1.0.0

Estoy participando en el reclutamiento del programa de firma de creadores de la comunidad tecnológica de Nuggets, haga clic en el enlace para registrarse y enviar

Supongo que te gusta

Origin juejin.im/post/7119129384727871496
Recomendado
Clasificación