Skip to content

xuexiangjys/DataBindingSample

Repository files navigation

DataBindingSample

DataBinding的使用集合

关于我

公众号 掘金 知乎 CSDN 简书 思否 哔哩哔哩 今日头条
我的Android开源之旅 点我 点我 点我 点我 点我 点我 点我

准备工作

启用

1.DataBinding启用

android {
    dataBinding {
        enabled = true
    }
}

2.ViewBinding启用

android {
    buildFeatures {
        viewBinding true
    } 
}

快捷方式

在你的布局中找到最外层的布局,将光标放在如图位置。

  • Windows请按快捷键 Alt + 回车
  • Mac请按快捷键 option + 回车


DataBinding绑定

1.数据类型

通常我们在DataBinding中绑定的数据类型是ViewModel或者是AndroidViewModel,它俩都是生命周期可感知的,唯一的区别是AndroidViewModel可以获取到应用的上下文Application

2.数据创建

ViewModel的创建通常是通过ViewModelProvider进行创建和获取。

ViewModelProvider(this).get(Xxx::class.java)

而在ViewModel中,通常使用MutableLiveData作为可变UI响应数据类型。相比较LiveData而言,它开放了修改值的接口,下面是一个ViewModel的简单例子:

class RecyclerViewRefreshState(application: Application) : AndroidViewModel(application) {

    val title = MutableLiveData("RecyclerView的刷新和加载更多演示")
    val isLoading = MutableLiveData(false)
    val sampleData = MutableLiveData<List<SimpleItem>>(arrayListOf())
    val loadState = MutableLiveData(LoadState.DEFAULT)
    val layoutStatus = MutableLiveData(Status.DEFAULT)
}

当然了,如果你有一个LiveData会随着一个或多个LiveData的变化而变化,这个时候你可能就需要使用MediatorLiveData,即合并LiveData。

这里我简单利用MediatorLiveData实现一个组合的LiveData--CombinedLiveData

open class CombinedLiveData<T>(vararg liveData: LiveData<*>, block: () -> T) :
    MediatorLiveData<T>() {
    init {
        value = block()
        liveData.forEach {
            addSource(it) {
                val newValue = block()
                if (value != newValue) {
                    value = newValue
                }
            }
        }
    }
}

fun <R, T1, T2> combineLiveData(
    liveData1: LiveData<T1>,
    liveData2: LiveData<T2>,
    block: (T1?, T2?) -> R
) = CombinedLiveData(liveData1, liveData2) { block(liveData1.value, liveData2.value) }

这个时候,我们就可以通过combineLiveData方法将两个LiveData组合起来,形成一个新的LiveData。下面我简单给出一个示例代码:

class CombineLiveDataState : DataBindingState() {
    val userName = MutableLiveData("小明")
    val userAge = MutableLiveData(20)
    val userInfo = combineLiveData(userName, userAge) { name, age ->
        "${name}今年${age}岁了!"
    }

    fun onAgeChanged() {
        userAge.value = userAge.value?.plus(1)
    }
}

这里变化了userAge的值后,userInfo也会随着一起变化。

3.布局绑定

一般我们使用DataBindingUtil进行布局绑定操作。绑定操作我们可分为:绑定Activity、绑定Fragment和绑定View。

  1. 绑定Activity

使用DataBindingUtil.setContentView方法进行绑定。

fun <DataBinding : ViewDataBinding> bindActivity(
    activity: ComponentActivity,
    layoutId: Int
): DataBinding = DataBindingUtil.setContentView<DataBinding>(activity, layoutId).apply {
    lifecycleOwner = activity
}
  1. 绑定Fragment

使用DataBindingUtil.inflate方法进行绑定。

fun <DataBinding : ViewDataBinding> bindFragment(
    fragment: Fragment,
    inflater: LayoutInflater,
    layoutId: Int,
    parent: ViewGroup? = null,
    attachToParent: Boolean = false
): DataBinding = DataBindingUtil.inflate<DataBinding>(inflater, layoutId, parent, attachToParent).apply {
    lifecycleOwner = fragment.viewLifecycleOwner
}
  1. 绑定View

使用DataBindingUtil.bind方法进行绑定。

fun <DataBinding : ViewDataBinding> bindView(
    view: View,
    viewLifecycleOwner: LifecycleOwner,
): DataBinding = DataBindingUtil.bind<DataBinding>(view).apply {
    lifecycleOwner = viewLifecycleOwner
}

⚠️特别注意事项⚠️️】

DataBinding绑定的时候,一定要给ViewDataBinding赋值LifecycleOwner, 否则ViewModel中的LiveData发生数据改变后,则不会通知UI组件进行页面更新。

4.ViewModel绑定

ViewModel的绑定有两种写法。

  • 直接使用ViewDataBinding.variableId = xxx直接赋值。
val mainState = ViewModelProvider(this).get(MainState::class.java)
activityMainbinding.state = mainState
  • 使用ViewDataBinding.setVariable(int variableId, @Nullable Object value)进行赋值。
val mainState = ViewModelProvider(this).get(MainState::class.java)
binding.setVariable(BR.state, mainState)

这两者的唯一区别在于,第一种需要知道ViewDataBinding的具体类型,而第二种是ViewDataBinding自身的方法,无需知道ViewDataBinding的具体类型。

一般来说在框架中使用到泛型未知ViewDataBinding具体类型的时候,都会使用第二种方式进行绑定,可以说第二种方式更通用一些。


基础使用

1.点击事件绑定

1.无参响应函数:

fun onIncrement() {
    // 方法体
}
android:onClick="@{() -> state.onIncrement()}"

2.接口变量响应函数

注意,这里变量的类型应该是View.OnClickListener接口。

val onClickDecrement = View.OnClickListener {
    // 方法体
}
android:onClick="@{state.onClickDecrement}"

3.有参响应函数

fun onReset(view: View) {
    // 方法体
}
// 第一种写法
android:onClick="@{(view) -> state.onReset(view)}" 

// 第二种写法
android:onClick="@{state::onReset}"

2.@BindingAdapter自定义属性

所有注解的功能都是基于XML属性值为DataBinding表达式才生效(即@{})

使用@BindingAdapter进行控件自定义属性绑定的时候,一定要使用 "@{}" 进行赋值,这一点非常重要!!!

  1. 顶级函数实现
// Kotlin拓展函数式写法, 推荐使用
@BindingAdapter("customTitle")
fun TextView.setCustomTitle(title: String) {
    text = "标题1: $title"
}

// 第一个参数必须是view的子类
@BindingAdapter("customTitle1")
fun setCustomTitle1(view: TextView, title: String) {
    view.text = "标题2: $title"
}

// 多个参数进行绑定,requireAll=true,代表两个参数都设置了才生效,默认是true.
// 如果requireAll为false, 你没有填写的属性值将为null. 所以需要做非空判断.
@BindingAdapter(value = ["customTitle", "customSize"], requireAll = true)
fun TextView.setTextContent(title: String, size: Int) {
    text = "标题3: $title"
    textSize = size.toFloat()
}

【特别注意事项⚠️

很多时候,很多新手在写DataBinding的时候,经常会漏掉"@{}",尤其是用数字和Boolean类型的值时。就比如我上面设置的customSize属性,类型值是Int型,正确的写法应该是下面这样:

  • 正确的写法
<TextView
    style="@style/TextStyle.Title"
    android:layout_marginTop="16dp"
    app:customSize="@{25}"
    app:customTitle="@{state.title}" />
  • 常见错误的写法
<TextView
    style="@style/TextStyle.Title"
    android:layout_marginTop="16dp"
    app:customSize="25"
    app:customTitle="@{state.title}" />

上述错误的写法,运行后编译器会报错AAPT: error: attribute customSize (aka com.xuexiang.databindingsample:customSize) not found.

所以当我们写DataBinding的时候,如果出现AAPT: error: attribute xxx (aka com.aa.bb:xxx) not found.,十有八九是你赋值漏掉了"@{}"

  1. 单例类+@JvmStatic注解
object TitleAdapter {
    @JvmStatic
    @BindingAdapter("customTitle2")
    fun setCustomTitle2(view: TextView, title: String) {
        view.text = "标题4: $title"
    }
}

3.@BindingConversion自定义类型转换

作用:在使用DataBinding的时候,对属性值进行转换,以匹配对应的属性。 定义:方法必须为公共静态(public static)方法,且有且只能有1个参数。

下面我给一个简单的例子:

1.对于User类,age的类型是Int。

data class User(
    val name: String,
    val gender: String? = "",
    val age: Int = 10,
    val phone: String? = "13124765438",
    val address: String? = null
)

2.使用@BindingAdapter定义了age的类型却是String。

@BindingAdapter(value = ["name", "age"], requireAll = true)
fun TextView.setUserInfo(name: String, age: String) {
    text = "${name}今年${age}"
}

3.这时候使用DataBinding的时候,👇的app:age="@{state.user.age}"会编译报错,提示类型不匹配。

<TextView
    style="@style/TextStyle.Title"
    android:layout_marginTop="16dp"
    app:name="@{state.user.name}"
    app:age="@{state.user.age}"/>

4.这个时候,我们就可以使用@BindingConversion自定义类型转换: Int -> String, 这样👆的代码就不会编译出错了。

@BindingConversion
fun int2string(integer: Int) = integer.toString()

4.@{}中表达式使用

  1. 常用运算符
  • 算术 + - / * %
  • 字符串合并 +
  • 逻辑 && ||
  • 二元 & | ^
  • 一元 + - ! ~
  • 移位 >> >>> <<
  • 比较 == > < >= <=
  • 三元 ?:
  • Array 访问 []
<TextView
    android:text="@{@string/app_name +  @string/app_name}"/>
<TextView 
    android:visibility="@{!state.user.phone.empty ? View.VISIBLE : View.GONE}"/>
  1. 常用转义字符
  • 空格: &nbsp;
  • <小于号: &lt;
  • >大于号: &gt;
  • &与号: &amp;
<TextView 
    android:visibility="@{!state.user.phone.empty &amp;&amp; state.user.age > 5 ? View.VISIBLE : View.GONE}"/>
  1. 资源使用

@string @color @drawable @dimen @array

<TextView
    style="@style/TextStyle.Content"
    android:text="@{@string/user_format(state.user.name, state.user.gender)}"
    android:textColor="@{@color/toast_error_color}"
    android:textSize="@{@dimen/xui_config_size_content_text_phone}" />
  1. 集合

集合不属于java.lang*下, 需要导入全路径。集合使用[]进行访问。

<data>
    <import type="java.util.List"/>
    <import type="android.util.SparseArray"/>
    <import type="java.util.Map"/>
    <variable name="list" type="List&lt;String>"/>
    <variable name="sparse" type="SparseArray&lt;String>"/>
    <variable name="map" type="Map&lt;String, String>"/>
</data>
<TextView
    android:text="@{`key: key1, value:` + map[`key1`]}" />
  1. 引用类的静态方法

kotlin中定义静态方法,一定要在方法上加上@JvmStatic,否则将无法成功引用。

(1) 定义方法

object AppUtils {

    @JvmStatic
    fun getAppInfo(context: Context?) =
        context?.let {
            "packageName: ${it.packageName}, \nversionName: ${
                it.packageManager.getPackageInfo(
                    it.packageName,
                    0
                ).versionName
            }"
        }
}

(2) 导入方法所在类路径

<import type="com.xuexiang.databindingsample.utils.AppUtils"/>

(3) 引用方法

<TextView
    android:text="@{AppUtils.getAppInfo(context)}"/>
  1. 空值合并运算符

空值合并运算符 ?? 会取第一个不为 null 的值作为返回值。

<TextView
    android:text="@{`地址:` + (state.user.address ?? `默认地址`)}"/>

等价于

<TextView
    android:text="@{state.user.address != null ?  state.user.address : `默认地址`)}"/>

5.include 和 ViewStub

在主布局文件中将相应的变量传递给 include 布局,需使用自定义的 bind 命名空间将变量传递给 (include/ViewStub), 从而使两个布局文件之间共享同一个变量。

例如,在include中定义的变量id是:, 那么就使用 app:user="@{state.user}" 来绑定数据,与variable定义的name保持一致。

  1. include
<include
    layout="@layout/include_user_info"
    app:user="@{state.user}" />
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <variable
            name="user"
            type="com.xuexiang.databindingsample.fragment.basic.model.User" />

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginVertical="16dp"
        android:orientation="vertical">

        <TextView
            style="@style/TextStyle.Content"
            android:userInfo="@{user}" />

    </LinearLayout>
</layout>

如果你想在页面中获取include引用布局的某个控件时,你需要给include设置资源id,然后通过它去访问引用布局中的控件,就以👆的例子为例,如果我想访问布局中的TextView,我们可以这样写:

binding?.includeLayout?.tvTitle?.text = "用户信息"

⚠️特别注意事项⚠️️】

这里需要注意的是,include标签,如果设置了layout_widthlayout_height这两个属性,那么布局就是由include外层设置的layout属性生效,内层属性不生效。

如果include标签没有设置layout_widthlayout_height这两个属性,那么就是由include引用的布局内层设置的layout属性生效。

举个例子,如果把👆设置的include改成下面这样:

<include
    layout="@layout/include_user_info"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="24dp"
    app:user="@{state.user}" />

那么@layout/include_user_info加载的布局,距离上部的距离就是24dp,而不是16dp。

  1. ViewStub
<ViewStub
    android:id="@+id/user_info"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="16dp"
    android:layout="@layout/viewstub_user_info"
    app:info="@{state.user}" />
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <variable
            name="info"
            type="com.xuexiang.databindingsample.fragment.basic.model.User" />

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginVertical="16dp"
        android:orientation="vertical">

        <TextView
            style="@style/TextStyle.Content"
            android:userInfo="@{info}" />

    </LinearLayout>
</layout>

因为ViewStub功能是延迟加载引用的布局,当我们需要让其进行加载的时候,我们需要通过ViewStub的资源id获取到ViewStub,然后进行inflate,示例代码如下:

binding?.userInfo?.viewStub?.inflate()

进阶使用

简化RecycleView的使用

1.定义一个供绑定的ViewHolder

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

    private val mLifecycle = LifecycleRegistry(this)

    fun bindingData(data: T?, variableId: Int = BR.item) {
        binding.setVariable(variableId, data)
    }

    init {
        mLifecycle.currentState = Lifecycle.State.INITIALIZED
    }

    fun onAttached() {
        mLifecycle.currentState = Lifecycle.State.STARTED
    }

    fun onDetached() {
        mLifecycle.currentState = Lifecycle.State.RESUMED
    }

    override fun getLifecycle(): Lifecycle = mLifecycle

}

2.定义一个供绑定的RecyclerView.Adapter

class BindingRecyclerViewAdapter<T>(
    @LayoutRes val layoutId: Int,
    var dataSource: MutableList<T>
) : RecyclerView.Adapter<BindingViewHolder<T>>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder<T> {
        val layoutInflater = LayoutInflater.from(parent.context)
        val binding = DataBindingUtil.inflate<ViewDataBinding>(
            layoutInflater,
            layoutId,
            parent,
            false
        )
        val holder = BindingViewHolder<T>(binding)
        binding.lifecycleOwner = holder
        return holder
    }


    override fun onBindViewHolder(holder: BindingViewHolder<T>, position: Int) {
        holder.setDataBindingVariables(dataSource.getOrNull(position))
        holder.itemView.tag = position
        if (holder.binding.hasPendingBindings()) holder.binding.executePendingBindings()
    }

    override fun getItemCount() = dataSource.size

    @SuppressLint("NotifyDataSetChanged")
    fun refresh(data: List<T>) {
        if (data.isNotEmpty()) {
            dataSource = data.toMutableList()
            notifyDataSetChanged()
        }
    }

    @SuppressLint("NotifyDataSetChanged")
    fun loadMore(data: List<T>) {
        if (data.isNotEmpty()) {
            dataSource.addAll(data)
            notifyDataSetChanged()
        }
    }

    override fun onViewAttachedToWindow(holder: BindingViewHolder<T>) {
        holder.onAttached()
    }

    override fun onViewDetachedFromWindow(holder: BindingViewHolder<T>) {
        holder.onDetached()
    }

    override fun onViewRecycled(holder: BindingViewHolder<T>) {
        holder.binding.lifecycleOwner = null
        super.onViewRecycled(holder)
    }
}

3.使用@BindingAdapter自定义绑定方法

@BindingAdapter(
    value = ["data", "itemLayout", "loadState"],
    requireAll = false
)
fun <T> RecyclerView.setBindingRecyclerViewAdapter(
    data: List<T>?,
    @LayoutRes layoutId: Int?,
    loadState: LoadState? = LoadState.DEFAULT,
) {
    requireNotNull(data) { "app:data argument cannot be null!" }
    requireNotNull(layoutId) { "app:itemLayout argument cannot be null!" }

    if (adapter == null) {
        adapter = BindingRecyclerViewAdapter(layoutId, data.toMutableList())
        layoutManager = XLinearLayoutManager(context)
    } else {
        @Suppress("UNCHECKED_CAST")
        (adapter as? BindingRecyclerViewAdapter<T>)?.run {
            when (loadState) {
                LoadState.REFRESH -> refresh(data, selectedPosition)
                LoadState.LOAD_MORE -> loadMore(data)
                else -> {}
            }
        }
    }
}

4.在xml中进行数据绑定

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="state"
            type="com.xuexiang.databindingsample.fragment.advanced.model.RecyclerViewBasicState" />
    </data>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/white"
        android:overScrollMode="never"
        app:data="@{state.sampleData}"
        app:itemLayout="@{@layout/databinding_item_simple_list_2}" />

</layout>

5.在ViewModel中设置数据

class RecyclerViewBasicState(application: Application) : DataBindingPageState(application) {

    override fun initTitle() = "RecycleView的基础使用演示"

    val sampleData = MutableLiveData(getDemoData(application))

    fun getDemoData(context: Context, from: Int = 1, to: Int = 40): List<SimpleItem> {
        // 模拟数据加载
        val list = mutableListOf<SimpleItem>()
        for (index in from..to) {
            list.add(
                SimpleItem(
                    context.getString(R.string.item_example_number_title, index),
                    context.getString(R.string.item_example_number_subtitle, index)
                )
            )
        }
        return list
    }
}

这样,有了这样一套绑定体系后,后面我们再需要使用到RecyclerView的时候,就只需要4和5步就行了,1-3步都是可重复使用的。

RecycleView的进阶使用

我们除了可以简单地使用DataBinding去加载RecyclerView的数据,我们还可以拓展其他一些操作来增强对RecyclerView的使用。

1.分割线的颜色和高度

@BindingAdapter(
    value = ["data", "itemLayout", "loadState", "dividerHeight", "dividerColor"],
    requireAll = false
)
fun <T> RecyclerView.setBindingRecyclerViewAdapter(
    data: List<T>?,
    @LayoutRes layoutId: Int?,
    loadState: LoadState? = LoadState.DEFAULT,
    dividerHeight: Float? = null,
    @ColorInt dividerColor: Int? = null,
) {
    requireNotNull(data) { "app:data argument cannot be null!" }
    requireNotNull(layoutId) { "app:itemLayout argument cannot be null!" }

    if (adapter == null) {
        adapter = BindingRecyclerViewAdapter(layoutId, data.toMutableList())
        layoutManager = XLinearLayoutManager(context)
        setDividerStyle(dividerHeight, dividerColor)
    } else {
        @Suppress("UNCHECKED_CAST")
        (adapter as? BindingRecyclerViewAdapter<T>)?.run {
            when (loadState) {
                LoadState.REFRESH -> refresh(data, selectedPosition)
                LoadState.LOAD_MORE -> loadMore(data)
                else -> {}
            }
        }
    }
}

fun RecyclerView.setDividerStyle(
    dividerHeight: Float? = null,
    @ColorInt dividerColor: Int? = null
) {
    val divider = DividerItemDecoration(context, LinearLayoutManager.VERTICAL)
    dividerHeight?.let {
        divider.dividerHeight = it.roundToInt()
    }
    dividerColor?.let {
        divider.dividerColor = it
    }
    addItemDecoration(divider)
}

2.事件监听

(1) 定义监听接口

/**
 * 列表条目点击监听
 */
interface OnItemClickListener<T> {
    /**
     * 条目点击
     *
     * @param itemView 条目
     * @param item     数据
     * @param position 索引
     */
    fun onItemClick(itemView: View?, item: T?, position: Int)
}

/**
 * 列表条目长按监听
 */
interface OnItemLongClickListener<T> {
    /**
     * 条目长按
     *
     * @param itemView 条目
     * @param item     数据
     * @param position 索引
     */
    fun onItemLongClick(itemView: View?, item: T?, position: Int) : Boolean = true
}

(2) Adapter设置监听

class BindingRecyclerViewAdapter<T>(
    @LayoutRes val layoutId: Int,
    var dataSource: MutableList<T>,
    var onItemClickListener: OnItemClickListener<T>?,
    var onItemLongClickListener: OnItemLongClickListener<T>?,
) : RecyclerView.Adapter<BindingViewHolder<T>>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder<T> {
        val layoutInflater = LayoutInflater.from(parent.context)
        val holder = createViewHolder(layoutInflater, parent, viewType)
        initViewHolder(holder)
        return holder
    }

    private fun initViewHolder(holder: BindingViewHolder<T>) {
        onItemClickListener?.run {
            holder.itemView.setOnClickListener {
                val position = holder.itemView.tag as Int
                onItemClick(it, dataSource.getOrNull(position), position)
            }
        }
        onItemLongClickListener?.run {
            holder.itemView.setOnLongClickListener {
                val position = holder.itemView.tag as Int
                onItemLongClick(it, dataSource.getOrNull(position), position)
            }
        }
    }

    override fun onBindViewHolder(holder: BindingViewHolder<T>, position: Int) {
        holder.bindingData(dataSource.getOrNull(position))
        holder.itemView.tag = position
        if (holder.binding.hasPendingBindings()) holder.binding.executePendingBindings()
    }
}

(3) 使用@BindingAdapter自定义绑定方法

@BindingAdapter(
    value = ["data", "itemLayout", "loadState", "dividerHeight", "dividerColor", "itemClick", "itemLongClick"],
    requireAll = false
)
fun <T> RecyclerView.setBindingRecyclerViewAdapter(
    data: List<T>?,
    @LayoutRes layoutId: Int?,
    loadState: LoadState? = LoadState.DEFAULT,
    dividerHeight: Float? = null,
    @ColorInt dividerColor: Int? = null,
    onItemClickListener: OnItemClickListener<T>? = null,
    onItemLongClickListener: OnItemLongClickListener<T>? = null,
) {
    requireNotNull(data) { "app:data argument cannot be null!" }
    require(layoutId != null || itemViewParser != null) { "app:itemLayout and app:itemViewParser argument need a parameter that is not null!" }

    if (adapter == null) {
        adapter = BindingRecyclerViewAdapter(
            layoutId,
            data.toMutableList(),
            onItemClickListener,
            onItemLongClickListener
        )
        layoutManager = XLinearLayoutManager(context)
        setDividerStyle(dividerHeight, dividerColor)
    } else {
        @Suppress("UNCHECKED_CAST")
        (adapter as? BindingRecyclerViewAdapter<T>)?.run {
            when (loadState) {
                LoadState.REFRESH -> refresh(data, selectedPosition)
                LoadState.LOAD_MORE -> loadMore(data)
                else -> {}
            }
        }
    }
}

3.多布局类型加载

(1) 定义布局解析器接口

interface ItemViewParser {

    fun getItemViewType(position: Int): Int

    fun getItemLayoutId(viewType: Int): Int
}

(2) 增加布局解析器默认实现

class DefaultItemViewParser(@LayoutRes val layoutId: Int): ItemViewParser {

    override fun getItemViewType(position: Int) = 0

    override fun getItemLayoutId(viewType: Int) = layoutId

}

(3) 重写Adapter的onCreateViewHolder方法和getItemViewType方法

class BindingRecyclerViewAdapter<T>(
    private val itemViewParser: ItemViewParser,
    var dataSource: MutableList<T>,
    var onItemClickListener: OnItemClickListener<T>?,
    var onItemLongClickListener: OnItemLongClickListener<T>?,
) : RecyclerView.Adapter<BindingViewHolder<T>>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder<T> {
        val layoutInflater = LayoutInflater.from(parent.context)
        val holder = createViewHolder(layoutInflater, parent, viewType)
        initViewHolder(holder)
        return holder
    }

    private fun createViewHolder(
        layoutInflater: LayoutInflater,
        parent: ViewGroup,
        viewType: Int
    ): BindingViewHolder<T> {
        val binding = DataBindingUtil.inflate<ViewDataBinding>(
            layoutInflater,
            itemViewParser.getItemLayoutId(viewType),
            parent,
            false
        )
        val holder = BindingViewHolder<T>(binding)
        binding.lifecycleOwner = holder
        return holder
    }

    override fun getItemViewType(position: Int): Int {
        return itemViewParser.getItemViewType(position)
    }
}

(4) 使用@BindingAdapter自定义绑定方法

@BindingAdapter(
    value = ["data", "itemLayout", "itemViewParser", "loadState", "dividerHeight", "dividerColor", "itemClick", "itemLongClick"],
    requireAll = false
)
fun <T> RecyclerView.setBindingRecyclerViewAdapter(
    data: List<T>?,
    @LayoutRes layoutId: Int?,
    itemViewParser: ItemViewParser?,
    loadState: LoadState? = LoadState.DEFAULT,
    dividerHeight: Float? = null,
    @ColorInt dividerColor: Int? = null,
    onItemClickListener: OnItemClickListener<T>? = null,
    onItemLongClickListener: OnItemLongClickListener<T>? = null,
) {
    requireNotNull(data) { "app:data argument cannot be null!" }
    require(layoutId != null || itemViewParser != null) { "app:itemLayout and app:itemViewParser argument need a parameter that is not null!" }

    if (adapter == null) {
        adapter = BindingRecyclerViewAdapter(
            itemViewParser ?: DefaultItemViewParser(layoutId!!),
            data.toMutableList(),
            onItemClickListener,
            onItemLongClickListener
        )
        layoutManager = XLinearLayoutManager(context)
        setDividerStyle(dividerHeight, dividerColor)
    } else {
        @Suppress("UNCHECKED_CAST")
        (adapter as? BindingRecyclerViewAdapter<T>)?.run {
            when (loadState) {
                LoadState.REFRESH -> refresh(data, selectedPosition)
                LoadState.LOAD_MORE -> loadMore(data)
                else -> {}
            }
        }
    }
}

4.刷新和加载更多

这里为了简单,我使用了开源的SmartRefreshLayout组件实现上拉刷新,下拉加载。

(1)使用@BindingAdapter自定义绑定方法

@BindingAdapter(
    value = ["refreshListener", "loadMoreListener"],
    requireAll = false
)
fun SmartRefreshLayout.setRefreshLayoutListener(
    refreshListener: OnRefreshListener?,
    loadMoreListener: OnLoadMoreListener?
) {
    setOnRefreshListener(refreshListener)
    setOnLoadMoreListener(loadMoreListener)
}

(2)在xml中进行数据绑定

<layout 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">

    <data>

        <variable
            name="state"
            type="com.xuexiang.databindingsample.fragment.advanced.model.RecyclerViewRefreshState" />
    </data>

    <com.scwang.smart.refresh.layout.SmartRefreshLayout
        android:id="@+id/refreshLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:loadMoreListener="@{state.loadMoreListener}"
        app:refreshListener="@{state.refreshListener}">

        <com.scwang.smart.refresh.header.ClassicsHeader
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@android:color/white"
            android:overScrollMode="never"
            app:data="@{state.sampleData}"
            app:itemLayout="@{@layout/databinding_item_simple_list_2}"
            app:loadState="@{state.loadState}"
            tools:listitem="@layout/databinding_item_simple_list_2" />

        <com.scwang.smart.refresh.footer.ClassicsFooter
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </com.scwang.smart.refresh.layout.SmartRefreshLayout>

</layout>

(3)在ViewModel中设置数据

class RecyclerViewRefreshState(application: Application) : DataBindingPageState(application) {

    val sampleData = MutableLiveData<List<SimpleItem>>(arrayListOf())

    val loadState = MutableLiveData(LoadState.DEFAULT)

    var pageIndex = 0

    val refreshListener = OnRefreshListener { refreshLayout ->
        // 延迟1000ms模拟网络请求延时
        refreshLayout.layout.postDelayed({
            pageIndex = 0
            loadState.value = LoadState.REFRESH
            sampleData.value = sampleGetData(application)
            refreshLayout.finishRefresh()
            refreshLayout.resetNoMoreData()
        }, 1000)
    }

    val loadMoreListener = OnLoadMoreListener { refreshLayout ->
        refreshLayout.layout.postDelayed({
            pageIndex += 1
            loadState.value = LoadState.LOAD_MORE
            sampleData.value = sampleGetData(application)
            if (pageIndex >= 3) { // 模拟只能加载更多3页,即总共4页的数据
                refreshLayout.finishLoadMoreWithNoMoreData()
            } else {
                refreshLayout.finishLoadMore()
            }
        }, 1000)
    }

    /**
     * 模拟获取数据
     */
    private fun sampleGetData(context: Context) =
        getDemoData(context, pageIndex * PAGE_SIZE + 1, PAGE_SIZE * (pageIndex + 1))
}

特别感谢

如果觉得项目还不错,可以考虑打赏一波

你的打赏是我维护的动力,我将会列出所有打赏人员的清单在下方作为凭证,打赏前请留下打赏项目的备注!

pay.png

联系方式

更多资讯内容,欢迎扫描关注我的个人微信公众号:【我的Android开源之旅】