Android官方架构组件DataBinding双向绑定篇: 观察者模式的殊途同归

栏目: Android · 发布时间: 5年前

内容简介:本文是Android官方架构组件 系列的番外篇,因为目前国内关于此外,前几天在CSDN上看到貌似掉线 老师发布了一篇文章本文默认读者对

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

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

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

什么是双向绑定?

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

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

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

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

Android官方架构组件DataBinding双向绑定篇: 观察者模式的殊途同归

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

但是换一个场景,如果我们把 TextView 换成一个 EditText ,接下来我们需要面对的则截然不同,比如登录界面:

Android官方架构组件DataBinding双向绑定篇: 观察者模式的殊途同归

这似乎没有什么问题,我们依然通过一个 LiveDataEditText 进行了单向绑定:

Android官方架构组件DataBinding双向绑定篇: 观察者模式的殊途同归

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

于是我们希望,即使是 EditText 的内容发生了变更,但是 LiveData 内的数据也能和 EditText 保持内容的同步——这样我们就不需要让 ViewModel 层持有 View 层的引用,在请求接口时,直接从 LiveData 中取值即可:

Android官方架构组件DataBinding双向绑定篇: 观察者模式的殊途同归

这就是双向绑定的意义。

使用场景是什么

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

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

现在我们定义,当 不确定的操作发生时 ——通常,这种操作代表着用户对UI控件的交互,这时UI的变化需要影响到 ViewModel 层的数据状态(除了 数据驱动视图 之外,视图也在驱动数据,以方便作为参数将来进行网络请求等等操作),这时 双向绑定 就可以大展身手了。

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

Android官方架构组件DataBinding双向绑定篇: 观察者模式的殊途同归

当用户选中了 CheckBox ,我们当然希望 ViewModel 层的 LiveData<Boolean> 状态进行对应的更新,以便将来我们直接从 LiveData 中取值作为参数进行网络请求。

而如果没有双向绑定,用户操作了UI,我们就需要 手动添加代码保证状态的同步 ——比如 checkBox.setOnCheckChangedListener() ,否则,就会在接下来的操作中得到与预期不同的结果。

Android官方架构组件DataBinding双向绑定篇: 观察者模式的殊途同归

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

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

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的状态抽象为一个流,但最终,两者在思想上殊途同归。

关于我

Hello,我是 却把清梅嗅 ,如果您觉得文章对您有价值,欢迎 :heart:,也欢迎关注我的博客或者 Github

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


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

软件测试

软件测试

乔根森 / 韩柯 / 机械工业出版社 / 2003-12-1 / 35.00元

《软件测试》(原书第2版)全面地介绍了软件测试的基础知识和方法。通过问题、图表和案例研究,对软件测试数学问题和技术进行了深入的研究,并在例子中以更加通用的伪代码取代了过时的Pascal代码,从而使内容独立于具体的程序设计语言。《软件测试》(原书第2版)还介绍了面向对象测试的内容,并完善了GUI测试内容。一起来看看 《软件测试》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具