Android官方架构组件DataBinding-Ex: 双向绑定篇

前言

本文是 Android官方架构组件 系列的番外篇,由于目前国内关于DataBinding双向绑定的博客,讲的实在是五花八门,不少文章看完以后仍然一头雾水,特此专门写一篇文章进行总结。java

此外,前几天在CSDN上看到 貌似掉线 老师发布了一篇文章《我为何放弃在项目中使用Data Binding》,里面针对性指出了目前DataBinding的使用中一些痛点,不少地方我感同身受,但鉴于 事物的存在必然存在两面性 ,特此也在 本文的末尾 写了一些我我的的理解, 阐述了为何我我的 还在坚持使用DataBinding , 但愿对读者能有所裨益。android

本文默认读者对DataBinding的使用有了初步的了解。git

什么是双向绑定?

DataBinding的自己是对View层状态的一种观察者模式的实现,经过让ViewViewModel层可观察的对象(好比LiveData)进行绑定,当ViewModel层数据发生变化,View层也会自动进行UI的更新。github

上述我讲的是DataBinding最基础的用法,即 单向绑定 ,其优点在于,将View层抽象为一个纯Java的可观察者——这意味着ViewModel层相关代码是彻底可直接用于进行 单元测试编程

但实际的开发中,单向绑定并不是是足够的,在一些特定的场景,咱们也须要用到 双向绑定网络

好比说,对于一个TextView的内容展现,通常状况下,咱们只是用来经过将一个String类型的数据对其进行渲染:架构

显而易见,数据的流向是单向的,换句话说,咱们认为TextViewDataSource只进行了 操做——若是此时进行了网络请求,咱们须要用到DataSource某个属性做为参数,咱们依然能够毫无顾忌从DataSource取值。app

可是换一个场景,若是咱们把TextView换成一个EditText,接下来咱们须要面对的则大相径庭,好比登陆界面: 框架

这彷佛没有什么问题,咱们依然经过一个LiveDataEditText进行了单向绑定:源码分析

问题发生了,当咱们对 输入框 进行编辑,EditText的UI发生了变动,可是LiveData内的数据却没有更新,当咱们想要在ViewModel层请求登陆的API接口时,咱们就必需要去经过editText.getText()才能获取用户输入的密码。

因而咱们但愿,即便是EditText的内容发生了变动,可是LiveData内的数据也能和EditText保持内容的同步——这样咱们就不须要让ViewModel层持有View层的引用,在请求接口时,直接从LiveData中取值便可:

这就是双向绑定的意义。

使用场景是什么

什么适合使用 双向绑定 呢,还记得上文中的一句话吗:

对于单向绑定来讲,数据的流向是单向的,换句话说,咱们认为TextViewDataSource只进行了 操做。

如今咱们定义,当 不肯定的操做发生时 ——一般,这种操做表明着用户对UI控件的交互,这时UI的变化须要影响到ViewModel层的数据状态(除了 数据驱动视图 以外,视图也在驱动数据,以方便做为参数未来进行网络请求等等操做),这时 双向绑定 就能够大展身手了。

显然上文中的EditText的是 双向绑定 经典的使用场景之一,此外,双向绑定的使用场景很是常见,好比CheckBox

当用户选中了CheckBox,咱们固然但愿ViewModel层的LiveData<Boolean>状态进行对应的更新,以便未来咱们直接从LiveData中取值做为参数进行网络请求。

而若是没有双向绑定,用户操做了UI,咱们就须要 手动添加代码保证状态的同步——好比checkBox.setOnCheckChangedListener(),不然,就会在接下来的操做中获得与预期不一样的结果。

听起来好像很麻烦,那么究竟如何使用呢?

幸运的是,Android原生控件中,绝大多数的双向绑定使用场景,DataBinding都已经帮咱们实现好了:

这意味着咱们并不须要去手动实现复杂的双向绑定,以上文的EditText为例,咱们只须要经过@={表达式}进行双向的绑定:

<EditText android:id="@+id/etPassword" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@={ fragment.viewModel.password }" />
复制代码

相比单向绑定,只须要多一个=符号,就能保证View层和ViewModel层的 状态同步 了。

难点在哪?

双向绑定定义好以后,使用起来很简单,但定义却稍微比单向绑定麻烦一些,即便原生的控件DataBinding已经帮助咱们实现好了,对于三方的控件或者自定义控件,还须要咱们本身实现

本文以SwipeRefreshLayout为例,让咱们来看看其 双向绑定 实现的方式:

object SwipeRefreshLayoutBinding {

    @JvmStatic
    @BindingAdapter("app:bind_swipeRefreshLayout_refreshing")
    fun setSwipeRefreshLayoutRefreshing( swipeRefreshLayout: SwipeRefreshLayout, newValue: Boolean ) {
        if (swipeRefreshLayout.isRefreshing != newValue)
            swipeRefreshLayout.isRefreshing = newValue
    }

    @JvmStatic
    @InverseBindingAdapter( attribute = "app:bind_swipeRefreshLayout_refreshing", event = "app:bind_swipeRefreshLayout_refreshingAttrChanged" )
    fun isSwipeRefreshLayoutRefreshing(swipeRefreshLayout: SwipeRefreshLayout): Boolean =
            swipeRefreshLayout.isRefreshing

    @JvmStatic
    @BindingAdapter( "app:bind_swipeRefreshLayout_refreshingAttrChanged", requireAll = false )
    fun setOnRefreshListener( swipeRefreshLayout: SwipeRefreshLayout, bindingListener: InverseBindingListener? ) {
        if (bindingListener != null)
            swipeRefreshLayout.setOnRefreshListener {
                bindingListener.onChange()
            }
    }
}
复制代码

有点晦涩,是否是?咱们先不要纠结于细节的实现,先来看看代码中是如何使用的吧:

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout android:layout_width="match_parent" android:layout_height="match_parent" app:bind_swipeRefreshLayout_refreshing="@={ fragment.viewModel.refreshing }">

            <androidx.recyclerview.widget.RecyclerView/>

</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
复制代码

refreshing实际就只是一个LiveData

val refreshing: MutableLiveData<Boolean> = MutableLiveData()
复制代码

这里的双向绑定,意义在于,当咱们为LiveData手动设置值时,SwipeRefreshLayout的UI也会发生对应的变动;同理,当用户手动下拉执行刷新操做时,LiveData的值也会对应的变成为true(表明刷新中的状态)。

相比于其它的方式,双向绑定将SwipeRefreshLayout的刷新状态抽象成为了一个LiveData<Boolean> ——咱们只须要在xml中定义好,以后就能够在ViewModel中围绕这个状态进行代码的编写,不一样于view.setOnRefreshListener()的方式,这种代码是纯Java的,咱们能够针对每一行代码进行纯JVM的单元测试。

本小节的全部代码你均可以在 这里 获取。

整理思路,循序渐进实现双向绑定

说了这么多,可是咱们一行代码都尚未实现,不着急,由于编码只是其中的一个步骤,最重要的是 整理一个流畅的思路,这样,在接下来的编码阶段,你会若有神助。

1.实现单向绑定

咱们知道,双向绑定的前提是单向绑定,所以,咱们先配置好对应单向绑定的接口:

@JvmStatic
@BindingAdapter("app:bind_swipeRefreshLayout_refreshing")
fun setSwipeRefreshLayoutRefreshing( swipeRefreshLayout: SwipeRefreshLayout, newValue: Boolean ) {
        swipeRefreshLayout.isRefreshing = newValue
}
复制代码

咱们经过将LiveData的值和DataBinding绑定在一块儿,每当LiveData的状态发生了变动,SwipeRefreshLayout的刷新状态也会发生对应的更新。

咱们实现了数据驱动视图的效果,接下来咱们须要思考的是,咱们如何才能知道用户会执行下拉操做呢?

2.观察View层的状态变动

只有观察到View层的状态变动,咱们才能驱动LiveData进行对应的更新,其实很简单,经过swipeRefreshlayout.setOnRefreshListener()便可:

@JvmStatic
@BindingAdapter( "app:bind_swipeRefreshLayout_refreshingAttrChanged", requireAll = false )
fun setOnRefreshListener( swipeRefreshLayout: SwipeRefreshLayout, bindingListener: InverseBindingListener? ) {
    if (bindingListener != null)
        swipeRefreshLayout.setOnRefreshListener {
            bindingListener.onChange()   // 1
        }
}
复制代码

注意我注释了 //1的地方,每当swipeRefreshLayout刷新状态被用户的操做改变,咱们都可以在这里监听到,并交给InverseBindingListener这个 信使 去通知DataBinding

嗨!View层的状态发生了变动,你快去通知LiveData也进行对应数据的更新呀!

新的问题来了,如今DataBinding已经知道须要去通知LiveData进行对应数据的更新了,关键是——

3. 我要把什么数据交给LiveData?

是的,即便LiveData须要进行更新,可是它并不知道要新的状态是什么。

LiveData: 老哥,你却是把数据给我啊!

咱们急需将SwipeRefreshLayout最新状态告诉LiveData,所以咱们经过InverseBindingAdapter注解和 步骤二 中去进行对接:

@JvmStatic
@InverseBindingAdapter( attribute = "app:bind_swipeRefreshLayout_refreshing", event = "app:bind_swipeRefreshLayout_refreshingAttrChanged" // 2 【注意!】 )
fun isSwipeRefreshLayoutRefreshing(swipeRefreshLayout: SwipeRefreshLayout): Boolean =
        swipeRefreshLayout.isRefreshing
复制代码

注意到 //2 注释的那行代码没有,咱们经过相同的tag(即app:bind_swipeRefreshLayout_refreshingAttrChanged这个字符串,步骤二中咱们也声明了相同的字符串),和 步骤二 中的代码块造成了绑定对接。

如今,LiveData知道如何进行反向的数据更新了:

每当用户下拉刷新,InverseBindingListener通知DataBinding,LiveData就会从swipeRefreshLayout.isRefreshing得知最新的状态,并进行数据的同步更新。

4.不要忘了防止死循环!

细心的你多少已经感受到了不对劲的地方,如今的双向绑定有一个致命的问题,那就是无限循环会致使的ANR异常。

View层UI状态被改变,ViewModel对应发生更新,同时,这个更新又回通知View层去刷新UI,这个刷新UI的操做又会通知ViewModel去更新.......

所以,为了保证不会无限的死循环致使App的ANR异常的发生,咱们须要在最初的代码块中加一个判断,保证,只有View状态发生了变动,才会去更新UI:

@JvmStatic
@BindingAdapter("app:bind_swipeRefreshLayout_refreshing")
fun setSwipeRefreshLayoutRefreshing( swipeRefreshLayout: SwipeRefreshLayout, newValue: Boolean ) {
    if (swipeRefreshLayout.isRefreshing != newValue)   // 只有新老状态不一样才更新UI
        swipeRefreshLayout.isRefreshing = newValue
}
复制代码

小结:我为何还在坚守DataBinding

本文的初始计划中,还有一个模块是关于 双向绑定的源码分析,写到后来又以为没有必要了,由于即便是 源码,也只是将上文中实现的思路啰嗦复述了一遍而已。

双向绑定自己是一个极具争议的功能;事实上,DataBinding自己也极具争议——DataBinding的好用与否,用或者不用都不重要,重要的是咱们须要去正视它展示出来的思想:即如何将一个 难以测试,状态多变 的View, 经过代码抽象为 易于维护和测试 的纯Java的状态?

DataBinding将烦不胜烦的View层代码抽象为了易于维护的数据状态,同时极大减小了View层向ViewModel层抽象的 胶水代码,这就是最大的优点。

固然,DataBinding并不必定就是正解,事实上,RxBinding就是另一个优秀的解决方案,一样以SwipeRefreshLayout为例,我依然能够将其抽象为一个可观察的Observable<Boolean>——前者经过在xml中对数据进行绑定和观察,后者经过RxJava对View的状态抽象为一个流,但最终,二者在思想上异曲同工。

系列文章

争取打造 Android Jetpack 讲解的最好的博客系列

Android Jetpack 实战篇


关于我

Hello,我是却把清梅嗅,若是您以为文章对您有价值,欢迎 ❤️,也欢迎关注个人我的博客或者Github

若是您以为文章还差了那么点东西,也请经过关注督促我写出更好的文章——万一哪天我进步了呢?

相关文章
相关标签/搜索