Databinding使用笔记

Author Avatar
xjunz 3月 08, 2021
  • 在其它设备中阅读本文章

Databinding使用中的一些细节

可能存在错误或疏漏,欢迎批评斧正…

  • 实体类的字段要想实现可观察,其字段需要使用ObservableXxx。如果不想使用ObservableXxx,该实体类可继承自BaseObservable,然后在对应的字段的getter上添加@Bindable注解,在对应的setter里调用notifyPropertyChange,这样该字段仍然是可观察的,如果你既不想使用Observable,而且该类已经继承自其他类,无法再继承BaseObservable。那么可以参照BaseObservable的写法引入PropertyChangeRegistry以及相关方法。这样做看似不优雅,但是其实是官方操作,因为Google官方的例子中就是这样做的,参见databinding-samples

  • @BindingAdapter注解,该注解可以实现自定义XML属性。比如,你想实现一个View根据XML中定义的width设置宽度,那么,我们可以定义一个android:width属性:

    <layout xmlns:android="http://schemas.android.com/apk/res/android">
        <data>
            <variable
                name="width"
                type="int" />
        </data>
            <View
                  ...
                android:width="@{width}"/>
    </layout>
    

    但是此时,Databinding框架是无法识别android:width的,因为View没有setWidth方法,此时我们要定义一个方法来告诉框架如何使用此属性:

      @BindingAdapter("android:width")
       public static void setWidth(@NotNull View view, int width) {
              ViewGroup.LayoutParams lp = view.getLayoutParams();
              lp.width = width;
              view.setLayoutParams(lp);
       }
    

    注意,该方法可以放在任何一个可视性为public的类中,并且其修饰符须为public static(除非自定义Component),方法名是任意的。建议新建一个类专门存放这些Binding Adapters,官方就是这么做的: 如所有TextView有关的自定义绑定适配器都存放在TextViewBindingAdapter中。该方法第一个参数是XML中的目标View对象,第二个参数是XML中传入的值。这样定义后,IDE就能识别该自定义属性,Databinding框架就会自动调用此方法绑定数据。

  • @InverseBindingAdapter注解,该注解用于双向绑定。如果说@BindingAdapter定义了如何为视图设置特定值,那么@InverseBindingAdapter就定义了如何从视图中获取特定值。如果某个自定义属性要实现双向绑定,必须同时实现@BindingAdapter注解和@InverseBindingAdapter注解。还是上面这个例子,如果我们需要实现视图的宽度改变时,自动改变XML中width变量的值,我们需要对XML做如下改动:

    android:width=”@={width}”

    然后实现反向绑定对应的方法:

    @InverseBindingAdapter(attribute = "android:width",event = "android:widthAttrChanged")
    public static int getWidth(@NotNull View view) {
        return view.getWidth();
    }
    

    这样,框架就知道如何从视图中获取android:width属性的值,以便传递给width变量。但是问题是,框架知道如何获取值,但是它并不知道该值何时改变啊,所以目前双向绑定还没有生效。不知你是否注意到上面代码中的event = "android:widthAttrChanged"。是的,顾名思义,当 android:widthAttrChanged 这个事件发生时,框架会读取该值。这个事件名默认为andoroid:xxxAttrChanged, xxx为我们自定义的属性名,所以这段event = "android:widthAttrChanged"其实可以省略。接下来的问题是,widthAttrChanged事件是啥,什么时候发生?为此,我们还需要配置一个额外的Binding Adapter:

    @BindingAdapter("android:widthAttrChanged")
    public static void setWidthWatcher(View view, final InverseBindingListener listener) {
        View.OnLayoutChangeListener newValue = (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
            boolean isChanged = left != oldLeft || right != oldRight || top != oldTop || bottom != oldBottom;
            if (isChanged)
                listener.onChange();
        };
        View.OnLayoutChangeListener oldValue = ListenerUtil.trackListener(view, newValue, R.id.onWidthChanged);
        if (oldValue != null) {
            view.removeOnLayoutChangeListener(oldValue);
        }
        view.addOnLayoutChangeListener(newValue);
    }
    

    是不是看懵了?解释一下。可以看到,这个Binding Adapter定义了一个android:widthAttrChanged属性,没错,就是反向绑定触发事件名(注:该自定义属性不可用于XML),该方法其实就是定义了该事件何时触发。首先,该方法有两个参数,第一个是目标视图,第二个是反向绑定监听器。反向绑定监听器就是一个通知视图属性改变,然后反向改变数据的回调。查看方法体,可以看到,我们为该View设置了一个OnLayoutChangeListener, 目的是监听该View的宽度改变,当宽度改变时,调用InverseBindingListener#onChange(),这样就相当于发出了widthAttrChanged事件。不过,方法体中好像还有一些意义不明的代码,这些代码其实是防止重复监听的模板代码。其中ListenerUtil.trackListener可以追踪指定指定view的指定id的listener,如果该view已经有指定id的listener,就会返回该listener,否则返回null。最后一个问题,既然这个自定义属性不能在XML中使用,那这个方法什么时候会调用呢?当框架检测到我们的android:width的双向绑定,就会查找该属性反向绑定事件所在的@BindingAdapter注解并调用此方法,InverseBindingListener由系统传入。

  • 如果自定义属性不存在,Databinding框架会自动查找该视图的gettersetter方法,而不用手动定义Adapters。关于这个特性,可以有骚操作。当我们自定义View时,可能会需要自定义属性,那么,通常情况下,需要在<declare-styleable/>标签下自定义attr,然后在代码中解析。利用Databinding,我们只需要定义好对应的gettersetter(须符合Java bean 规范)方法,就可以直接在xml中直接使用自定义属性了,省去大量工作。当然这样做布局编辑器是无法预览这些自定义属性的。

  • 如果自定义属性的settergetter方法已经存在,只是名称不符合settergetter规范。此时,可以使用@BindingMethods@InverseBindingMethods注解进行声明,而不需要手动定义Adapters。参见AdapterViewBingAdapter等官方实现类。

  • Databinding表达式支持空值判断三元运算语法糖:@{a??b}=@{a==null?b:a}

  • 默认数据绑定是在数据更新后一帧执行,如果遇到需要改变数据后立即更新View的需求,可以调用executePendingBinding立即刷新数据绑定。通常,遇到数据和View的某些属性无法对应的情况,可能就是掉进了这个坑。比如:某个View的enabled状态是由XML中的变量enabled指定的,如果在代码中设置了enabled=true,然后立刻(在一个事件循环内)获取isEnabled,就会发现变量enabled的值和View的状态并不一致。

  • @BindingAdapterrequiredAll属性的作用在于,当requireAlltrue时(默认),只有当View同时定义了value中的所有属性,才会调用此方法(否则会报错)。当requiredAllfalse时,只要定义了value中的任意一个属性,就会调用此方法。

  • XML中属性文本定义时的先后位置确定这些属性在数据绑定时的先后顺序,如果某个属性依赖前一个属性,那么确保它们的位置顺序是正确的。如果无法确保(比如IDE格式化XML后会自动更换顺序),或者为了提高可维护性,那么请使用上面提到的requireAll属性,建立一个@BindingAdapter,在value中同时包含这两个属性,并使用requireAll=true,然后在方法体中自己定义两个属性的设置顺序。

  • Databinding表达式中语句最后的空括号可省略,比如onClick="@{()->activity.onClick(edit.getText())}"可省略为onClick="@{()->activity.onClick(edit.getText)}",但是如果语句为onClick="@{()->activity.onClick(edit.getText().toString())}",那么,
    只能省略为onClick="@{()->activity.onClick(edit.getText().toString)}"而不能省略为onClick="@{()->activity.onClick(edit.getText.toString)}"

  • 如果在表达式中引用其他视图的属性并使用了无get语句(如.getText()变成.text),那么系统会默认你在使用这个属性的反向绑定,比如text="@{edit.text.toString()}",那么当edit.text改变时,此控件的text也会改变(系统已经提前实现了text属性的反向绑定方法),如果你使用了一个未实现反向绑定的属性,如padding="@{textView.height}",那么,构建应用时系统就会报错,此时你要自己使用@InverseBindingAdapter@BindingAdapter来获取并通知height的变化,此方法才可用。如果只是想引用某个属性,而不是与某个属性绑定,那么请使用未省略get的语句,如padding="@{textView.getHeight()}"。这时候,只有执行数据绑定时,两个数据才会同步,而不是当一个改变时,另一个也改变。其实系统这样的设计是正确的,通常情况下,我们引用另一个视图的属性,就是为了保持与之同步。

  • 在xml中调用方法传参为null时,如果不将null显式强转为参数所要求的类型,Databinding框架会将null强转为Object类型,若该方法所需参数不是Object,那么就会无法通过编译。

  • ViewDataBinding.inflate(inflater)直接可以获得实例,我居然用ViewDataBindingUtils.inflate()用了这么久。

  • Databinding支持LiveData,也就是说LiveData(通常是MutableLiveData)可以代替ObservableXx进行数据绑定。LiveDataObservable的区别在于,LiveDataLifecycle Perceptive(生命周期可感知)的,即LiveData的订阅事件只在其宿主处于活跃状态时才会执行。正因为如此,使用LiveData进行数据绑定需要通过ViewDatabinding.setLifecyclerOwner设置宿主,绑定才会生效。

知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

本文链接:http://www.xjunz.top/post/DatabindingNotes/