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

前言

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

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

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

什么是双向绑定?

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

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

但实际的开发中,单向绑定并非是足够的,在一些特定的场景,我们也需要用到 双向绑定

比如说,对于一个TextView的内容展示,一般情况下,我们只是用来通过将一个String类型的数据对其进行渲染:

显而易见,数据的流向是单向的,换句话说,我们认为TextViewDataSource只进行了 操作——如果此时进行了网络请求,我们需要用到DataSource某个属性作为参数,我们依然可以毫无顾忌从DataSource取值。

但是换一个场景,如果我们把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

如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章——万一哪天我进步了呢?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335