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

减少胶水代码

      原本使用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

      第三个ObservableField的构造方法中,传入了前两个ObservableField对象:firstlast。并且重载了内部的 get 方法, get 方法的值依据firstlast生成。这好像是我想要的东西!!

查看了一下源码:

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