Android Jetpack——DataBinding:从排斥到真香

好像确实如此

刚学Android Jetpack时,前辈们都不怎么推荐使用DataBinding。从中了解到DataBinding是这样的:

  • 消除findViewById (我选择kt)
  • 在xml中写(逻辑)代码 (黑人问号面???,反感)
  • 无需手动设置一些监听 (不就几个监听吗)

soDataBinding在我眼里作用不大,甚至有点反感(主要xml那块),很长一段时间都排斥DataBinding,项目中只使用ViewModelLiveData等其他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就可以了(phonesmsCode使用双向绑定)。而这一切,都是自发的,岂不美哉?当然,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也可以借助LiveDataViewModel构建MVVM。如果实在无法容忍在xml中写入额外的东西,可以放弃DataBinding

xml中的代码应该全部抽离到Java/Kt中,使用"数据级联"将其进行转换,再将其绑定到View中。

数据绑定优先选择Observable*类而非 LiveData。如果想对Observable*类的值进行监听,可以使用Observable*类#addOnPropertyChangedCallback添加回调。

DataBinding总体来说还是很香的^ ^

参考资料

Data Binding — Lessons Learnt

《是让人耳目一新的 Jetpack MVVM 精讲啊》评论区

《重学安卓:从 被反对 到 真香 的 Jetpack DataBinding!》及其 评论区

Android官网

(ps:我个人比较喜欢看评论区,哈哈)

[图片上传失败...(image-37f1f7-1587105433319)]

<figcaption></figcaption>

</article>

作者:大棋
链接:https://juejin.im/post/5dfa26c9f265da33d7442cdb

posted @ 2020-04-17 16:17  AndroidAlvin  阅读(987)  评论(0编辑  收藏  举报