Android Jetpack——DataBinding:从排斥到真香
好像确实如此
刚学Android Jetpack
时,前辈们都不怎么推荐使用DataBinding
。从中了解到DataBinding
是这样的:
- 消除
findViewById
(我选择kt) - 在xml中写(逻辑)代码 (黑人问号面???,反感)
- 无需手动设置一些监听 (不就几个监听吗)
soDataBinding
在我眼里作用不大,甚至有点反感(主要xml那块),很长一段时间都排斥DataBinding
,项目中只使用ViewModel
和LiveData
等其他Jetpack
组件。
渐渐入坑
借助kt的插件,我们在任何地方都不需要写findViewById
(感谢大佬指出)。但由于以前不太懂在RecyclerView.ViewHolder
中使用kt插件,还是老老实实的findViewById
。这也让我想起DataBinding
的好处:消除findViewById
。而且对一个组件学都没学,在不了解的情况下,就判处"死刑",好像也不妥。
于是我决定尝试学习一下DataBinding
,但秉着不在xml中写逻辑代码的原则,在学习DataBinding
时,有关运算符的介绍都是跳过不看的。例如这些:
[图片上传失败...(image-66944c-1587105433325)]
<figcaption></figcaption>
减少胶水代码
原本使用kotlin
搬砖的我,减少胶水代码才是databinding
为我带来最直接的便利。比起修改LiveData
的值,然后设置Observer
感知LiveData
的变化,才对View
的数据或状态进行调整。直接使用DataBinding
,修改数据的同时,View
的数据或状态同步修改,更有一气呵成的感觉。无论是数据绑定,双向绑定,还是设置监听,都是在减少胶水代码。
正如@却把清梅嗅大佬在Android官方架构组件DataBinding-Ex: 双向绑定篇中总结到的:
DataBinding将烦不胜烦的View层代码抽象为了易于维护的数据状态,同时极大减少了View层向ViewModel层抽象的 胶水代码,这就是最大的优势。
“数据级联”
很多时候,我们需要对数据进行转换,以便在前端给用户展示正确的信息,而这种转换应该是自发的。众所周知,在LiveData
中存在map
扩展方法(内部调用Transformations#map
),我们可以利用该函数对数据进行自发的转换。
//性别
val sex = MutableLiveData<Int>()
val sexStr = sex.map {
when(it){
1 -> "男"
2 -> "女"
else -> ""
}
}
我可以照常对sex
进行赋值,让数据转换自发进行。而数据绑定时,只需要对sexStr
进行绑定或订阅。set
原始数据,视图呈现我需要展示的信息,好像这样更符合数据驱动的思想。
但由于当时不知道LiveData
也能用于dataBinding
的数据绑定(流下了没有技术的眼泪),于是我陷入了使用LiveData
无法进行数据绑定,使用Observable*类
无法进行自发转换的“困境”。
我在想:Observable*对象
也具转换的能力,该多好呀。于是我打算利用Observable*类#addOnPropertyChangedCallback
,对Observable*类
定义一系列map
扩展方法。但定义完一个后发现,Observable*类
数目较多,对其定义map
扩展无疑是 n * n 的排列组合(考虑到避免基础类型的装箱与拆箱)。懒人的我当然是选择放弃啦,只能另寻它法。
直到在官网看到这段代码:
[图片上传失败...(image-470c79-1587105433323)]
<figcaption>https://developer.android.com/reference/android/databinding/ObservableField</figcaption>
第三个
ObservableField
的构造方法中,传入了前两个ObservableField
对象:first
和last
。并且重载了内部的get
方法,get
方法的值依据first
和last
生成。这好像是我想要的东西!!
查看了一下源码:
#ObservableField.java
/**
* Creates an ObservableField that depends on {@code dependencies}. Typically,
* ObservableFields are passed as dependencies. When any dependency
* notifies changes, this ObservableField also notifies a change.
*
* @param dependencies The Observables that this ObservableField depends on.
*/
public ObservableField(Observable... dependencies) {
super(dependencies);
}
#BaseObservableField.java
public BaseObservableField(Observable... dependencies) {
if (dependencies != null && dependencies.length != 0) {
DependencyCallback callback = new DependencyCallback();
for (int i = 0; i < dependencies.length; i++) {
dependencies[i].addOnPropertyChangedCallback(callback);
}
}
}
class DependencyCallback extends Observable.OnPropertyChangedCallback {
@Override
public void onPropertyChanged(Observable sender, int propertyId) {
notifyChange();
}
}
简单说就是对构造方法中多个Observable*类
对象(如 first
和 last
)添加一个属性改变回调Observable.OnPropertyChangedCallback
。让这些Observable*类
对象在值改变时(例如first
的变化值 或 last
的值变化时),通过Observable.OnPropertyChangedCallback
回调通知一声(告诉 display
:我改变数据了 )。
然后又通过notifyChange()
告知其他人,他自身属性也改变了,让其他人重新获取他的值。重写内部的get
方法就能够让其他地方重新获取该值时(你都告诉我,你的值改变了,那我当然要重新获取一遍啦),计算出新值。
对于这样的现象:
一个对象拥有感知一个或多个对象的值变化的能力。当其感知对象的值改变时,自动调整自身的值。
暂且叫它“数据级联”吧
解决xml中的逻辑代码
回头一看,那些xml里编写的简单逻辑表达式,不正可以使用“数据级联”进行完成吗?以官网的代码为例:
android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"
复制代码
无非就是一个`visibility`值,需要依赖`age`的值,进行一些运算得出结果。而且每次`age`的值更新,需要通知`visibility`重新进行计算:
val age = ObservableInt()
val visibility = object :ObservableInt(age){
override fun get(): Int {
return if (age.get() > 13)
View.GONE
else
View.VISIBLE
}
}
#xml
android:visibility="@{viewModel.visibility}"
而且以前在xml中无法实现的较为复杂的逻辑代码,也可以尝试通过“数据级联”来实现。这样一来,xml层就只剩下数据绑定和设置监听的代码,我个人觉得还是可以接受的。(问了下前端的同学,Vue好像也是类似DataBinding这样绑定的)
实践
模仿b站的登录页面:
只有手机号码输入框和验证码输入框都存在输入值时,登录按钮才可点击,且透明度跟随改变。
//手机号码
val phone = ObservableField<String>()
//验证码
val smsCode = ObservableField<String>()
//登录按钮可点击状态
val loginEnable = object :ObservableBoolean(phone,smsCode) {
override fun get(): Boolean {
//获取手机输入框内容
val phoneStr = phone.get()
//获取验证码输入框内容
val smsCodeStr = smsCode.get()
//手机框和密码框都存在输入值时,才允许点击登录按钮
return if (phoneStr.isNullOrEmpty() || smsCodeStr.isNullOrEmpty())
false
else
true
}
}
//登录按钮的透明度
val loginAlpha = object:ObservableFloat(loginEnable){
override fun get(): Float {
//获取按钮是否可点击的布尔值
val enable = loginEnable.get()
return if (enable)
1.0f
else
0.5f
}
}
此时,我们需要做得就是通过DataBinding
将这4个Observable*类
对象绑定到xml就可以了(phone
和smsCode
使用双向绑定)。而这一切,都是自发的,岂不美哉?当然,LiveData
也可以使用MediatorLiveData
来实现对多个数据源监听。在这些数据源发生改变时,对其进行通知:
//手机号码
val phone = MutableLiveData<String>().apply { value = "" }
//验证码
val smsCode = MutableLiveData<String>().apply { value = "" }
//获取登录按钮可点击状态
val loginEnable = MediatorLiveData <Boolean>().apply {
val observer = Observer<String> {
refreshEnable()
}
addSource(smsCode,observer)
addSource(phone,observer)
}
//登录按钮的透明度
val loginAlpha = loginEnable.map { enable ->
if (enable)
1.0f
else
0.5f
}
fun refreshEnable(){
//获取验证码
val smsCodeStr = smsCode.value
//获取手机号码
val phoneStr = phone.value
//手机号码和验证码不为空,才可以点击登录按钮
if (phoneStr.isNullOrEmpty() || smsCodeStr.isNullOrEmpty())
loginEnable.value = false
else
loginEnable.value = true
}
自定义BindingAdapter优化
DataBinding
具有自动优化View
更新的能力,这与官方优化的BindingAdapter
有不少关系:
#TextViewBindingAdapter.java
@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
final CharSequence oldText = view.getText();
//会比较新旧值,一样则不重新赋值。
if (text == oldText || (text == null && oldText.length() == 0)) {
return;
}
if (text instanceof Spanned) {
if (text.equals(oldText)) {
return; // No change in the spans, so don't set anything.
}
} else if (!haveContentsChanged(text, oldText)) {
return; // No content changes, so don't set anything.
}
view.setText(text);
}
很多时候,我们需要将一些数据绑定的重复逻辑抽离到BindingAdapter
中(例如ImageView
依据url
使用Glide
加载图片),但其质量也会存在参差不齐的情况。这需要我们在自定义BindingAdapter
时,尽量对值进行一些必要的判断,以减少View的重新测量与重绘。
在BindingAdapter
中拦截没用的数据来优化View
更新,宛如是“末段拦截”。比起 “末段拦截” 更有用的是 “中程拦截”。具有防抖功能的Observable*类
比 LiveData
更具备 “中程拦截” 的能力,因为它在每次赋值前都会判断是否和旧值相等:
public void set(int value) {
if (value != mValue) {
mValue = value;
notifyChange();
}
}
回传旧值
DataBinding
允许在自定义的BindingAdapter
中,回传旧值。但是需要先声明旧值,再声明新值。详见:DataBinding介绍
。但对于能通过公开属性获取到的数据,要求DataBinding
回传旧值的作用并不大。反而像监听、回调这些,可以借助DataBinding
回传旧值的功能来进行移除。(当然直接替换的监听/回调也就不需要回传旧值)
@BindingAdapter("android:onLayoutChange")
fun setOnLayoutChangeListener(
view: View,
oldValue: View.OnLayoutChangeListener?,
newValue: View.OnLayoutChangeListener?
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
if (oldValue != null) {
view.removeOnLayoutChangeListener(oldValue)
}
if (newValue != null) {
view.addOnLayoutChangeListener(newValue)
}
}
}
使用DataBinding的一点小建议
- 不在xml中写任何逻辑代码,databinding在xml中只负责数据绑定和设置监听。
- xml标签中使用
databinding
的属性统一移到标签底部。 - 尽量使用官方定义的
BindingAdapter
进行数据绑定与设置监听。
小结
DataBinding加速MVVM的构建,减少大部分胶水代码。没有DataBinding
也可以借助LiveData
和ViewModel
构建MVVM
。如果实在无法容忍在xml中写入额外的东西,可以放弃DataBinding
。
xml中的代码应该全部抽离到Java/Kt
中,使用"数据级联"将其进行转换,再将其绑定到View
中。
数据绑定优先选择Observable*类
而非 LiveData
。如果想对Observable*类
的值进行监听,可以使用Observable*类#addOnPropertyChangedCallback
添加回调。
DataBinding
总体来说还是很香的^ ^
参考资料
《是让人耳目一新的 Jetpack MVVM 精讲啊》评论区
《重学安卓:从 被反对 到 真香 的 Jetpack DataBinding!》及其 评论区
(ps:我个人比较喜欢看评论区,哈哈)
[图片上传失败...(image-37f1f7-1587105433319)]
<figcaption></figcaption>
</article>