Android MVVM 系列之 Databinding(三)

Android MVVM 系列之 Databinding(三)

所有博文会不定期的更新一下的,文章有不妥欢迎大家提建议!

这篇文章主要讲 Databinding 中注解的使用,给大家整理个字典出来,Databinding 中公开的 Api 中有以下注解

  • @Bindable
  • @BindingAdapter
  • @BindingConversion
  • @BindingMethod
  • @BindingMethods
  • @InverseBindingAdapter
  • @InverseBindingMethod
  • @InverseBindingMethods
  • @InverseMethod

[TOC]

@Bindable

使用:

  • 此注解加在需要实现绑定观察的 get 或 is 开头的方法上,与 notifyPropertyChanged() 方法配合使用。

  • 实体类需继承 BaseObseravle 或实现 Observable 接口的时候(必须),

  • @Bindable 注解的方法必须是 public

添加 @Bindable 注解后,会在 BR 类中生成对应的字段,然后 与notifyPropertyChanged() 方法配合使用,当该字段中的数据被修改时,DataBinding 会自动刷新对应view的数据,而不用我们在拿到新数据后手动赋值。

当然,这也是实现双向绑定必须的,UI 改变布局通知 Java 代码也是靠它实现的

public class User3 extends BaseObservable {
    private String name;
    private int age;

    @Bindable
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(BR.name);
    }

    @Bindable
    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
        notifyPropertyChanged(BR.age);
    }

    /**
     * “@Bindable” 注解的方法必须是 public
     * “@Bindable” 注解也可以添加一组参数的,这样的话 name 或者 age 发生变化的时候都会通知 getNameAndAge 方法
     * 需要注意的是这样做并不等同于 notifyPropertyChanged(BR.nameAndAge); {@link OnPropertyChangedCallback#onPropertyChanged(Observable, int)} 并不会被调用
     * 大家可点进源码看注释
     * 那个 onPropertyChanged 方法现在显然没什么用,但是以后不保证你会用它来实现什么骚操作,比如来实现 EventBus
     */
    @Bindable({"name", "age"})
    public String getNameAndAge() {
        return name + age;
    }
}

@BindingAdapter

@Target(ElementType.METHOD)
public @interface BindingAdapter {

    /**
     * 返回自定义的属性数组.
     */
    String[] value();

    /**
     * 是否需要全部的属性
     */
    boolean requireAll() default true;
}

这个注解可是 DataBinding 的核心了,用来自定义 View 的属性,注意:

  • 注解用于 public 修饰的方法
  • 方法的第一个参数必须是 View 或其子类

对于一些View本身就没有android:xxxx或者app:xxx属性,或者 View 中对应的 setter 方法不是我们需要的,我们使用不了,使用 BindingAdapter 注解可以帮助我们来解决这个问题。

咱讲个 Glide 加载图片的例子吧

    <ImageView
        app:imageUrl="@{imageUrl}"
        android:layout_width="200dp"
        android:layout_height="200dp" />
@BindingAdapter("imageUrl")
    public static void bindImageUrl(ImageView imageView, String url) {
        GlideApp.with(imageView.getContext())
                .load(url)
                .into(imageView);
    }

这样在 xml 文件中就可以使用自定义的属性了,大家可能想这种用法好处是什么呢?先透露一下,在我目前的项目中所使用的控件,基本没有 id 这个属性的,findviewbyid butterknife 什么的都是浮云了,进阶的用法在后面的 MVVM 正式篇中会讲到的。这里需要注意的是:

  • BindingAdapter("imageUrl") 里面的 "imageUrl" 就是自定义生成的方法,在 xml 文件种使用时命名控件随意
  • BindingAdapter("bind:imageUrl") 这种是指定命名空间的写法,xml 中的属性必须写成 bind:imageUrl="@{...}" 这种形式
  • 如果定义的方法重复了,使用最后一个,系统找 BindingAdapter 注解的方法时有它自己的先后顺序,先找系统的再找自己定义的,这样的话自己定义的方法就能覆盖系统提供的方法了

如果我们需要定义多个属性名相同,参数类型各不相同的方法,Databinding 在某些方面是支持的,但是我在这里就不列出了(其实是我懒的去一个个验证了),怕误人子弟。。

上面的例子是定义单个属性,这个注解的方法还可以定义多个属性,在参数中传入多个值,属性的定义顺序要和参数的顺序一致

    <ImageView
        app:url="@{imageUrl}"
        app:holder="@{@drawable/logo}"
        app:error="@{@drawable/logo}"
        android:layout_width="200dp"
        android:layout_height="200dp" />
    @BindingAdapter(value = {"url", "holder", "error"}, requireAll = true)
    public static void bingImgs(ImageView imageView, String url, Drawable holder, Drawable error) {
        //Glide 要求的这些值都是非空的,我这里就 requireAll = true 了,你也可以在这里判空给自定义的默认值,设置 requireAll = false
        GlideApp.with(imageView.getContext())
                .load(url)
                .placeholder(holder)
                .error(error)
                .into(imageView);
    }

Glide 要求的这些值都是非空的,我这里就 requireAll = true 了,你也可以在这里判空给自定义的默认值,设置 requireAll = false。

@BindingMethod 和 @BindingMethods

我们之前了解了 Databinding 在 xml 中使用属性需要 View 本身提供这个属性的 set 方法,如果找不到对应的 set 方法就会报错。
其实这时候我们就想说,这个不就是上面 BindingAdapter 能解决的问题吗?

咳,为了说明这两个注解存在的意义,我强行造了一个例子一个例子:

    android:text='@{user.name}'

我们在给 TextView 设置 text 属性的时候,TextView 的 Java 类中一定存在一个 setText() 方法

    @android.view.RemotableViewMethod
    public final void setText(CharSequence text) {
        setText(text, mBufferType);
    }

它接收 CharSequence 类型的数据,而且我们的 String 类实现了这个接口,所以 Databinding 才能把字符串给对应的 TextView,大家再看一个

    android:tint="@{@color/colorAccent}"

这个 tint 属性在 ImageView 的类中是没有的,它对应的方法实际上是

    public void setImageTintList(@Nullable ColorStateList tint) {
        mDrawableTintList = tint;
        mHasDrawableTint = true;

        applyImageTint();
    }

所以 Databinding 找不到属性对应的 set 方法,在编译时就会报错,这时候 @BindingMethod 与 @BindingMethods 就派上用场了。

ps:可能会有人说,我没有做任何处理,直接使用android:tint="@{@color/colorAccent}"也没有报错啊,其实是因为dataBinding已经帮我们定义好了转换方法了,在你使用android:tint="@{@color/colorAccent}"时,已经帮你自动使用setImageTintList()方法生成代码了。

@BindingMethod

有3个字段,这3个字段都是必填项,少一个都不行:

  • type:要操作的属性属于哪个View类,类型为class对象,比如:ImageView.class
  • attribute:xml属性,类型为String ,比如:”android:tint”
  • method:指定xml属性对应的set方法,类型为String,比如:”setImageTintList”

@BindingMethods

因为@BindingMethod注解不能单独使用,必须要结合@BindingMethods才能发挥其功效,所以@BindingMethods注解其实就是一个容器,它内部有一个BindingMethod数组,存放的是一个一个的BindingMethod。

我们拿系统提供的类来看看 @BindingMethod 是如何使用的,系统提供的类的位置在

image
@BindingMethods({
        @BindingMethod(type = android.widget.ImageView.class, attribute = "android:tint", method = "setImageTintList"),
        @BindingMethod(type = android.widget.ImageView.class, attribute = "android:tintMode", method = "setImageTintMode"),
})
public class ImageViewBindingAdapter {
    @BindingAdapter("android:src")
    public static void setImageUri(ImageView view, String imageUri) {
        if (imageUri == null) {
            view.setImageURI(null);
        } else {
            view.setImageURI(Uri.parse(imageUri));
        }
    }

    @BindingAdapter("android:src")
    public static void setImageUri(ImageView view, Uri imageUri) {
        view.setImageURI(imageUri);
    }

    @BindingAdapter("android:src")
    public static void setImageDrawable(ImageView view, Drawable drawable) {
        view.setImageDrawable(drawable);
    }
}

我们可以看到系统给我们提供了很多预置的 BindingAdapter 和 BindingMethods,比如看这个 "android:src",我们其实可以再写个直接把 url 传进去,还可以干其他的,仔细一想,BindingAdapter 能做的太多了,本人项目的小伙伴直接把能重复利用的代码都搞这里了。。。其实我觉得并不太好,因为写这里代码不明显,沟通成本就变高了。

@BindingConversion

  • 注释方法,用于将表达式类型自动转换为setter中使用的值。
  • 转换器应该有一个参数,表达式类型,返回值应该是setter中使用的目标值类型。
  • 只要可以应用转换器,转换器就会被使用,并且不特定于任何属性。

问:那到底什么属性要被转换?又要把属性转换成什么呢?

答:举个例子,比如 View 的 android:background="" 属性,属性值为 drawable 类型的对象,如果你要放置一个 @color/black 的属性值,在不使用 dataBinding 的情况下没有任何问题,但是如果你要使用了 dataBinding 方式开发,则会报错,原因在于 android:background="" 的目标值类型为 drawable ,而 databinding 会把 @color/black 先解析成 int 类型的颜色值,这时如果把 int 类型的颜色值直接赋值到目标为 drawable 类型的参数中去,那绝逼是要报错的,如果我们可以在int类型的颜色值赋值之前,让 int 类型的颜色值自动转换为 colorDrawable 对象,就可以解决这个问题,而 @BindingConversion 注解就是干这个事的。
我们先来看一个官网上的示例:

<View
   android:background="@{isError ? @color/red : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

如果只有上面那段代码,那肯定会报错的,原因上面也说了,所以我们需要定义一个转换方法,把整型的颜色值转换为 drawable 对象:

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
   return new ColorDrawable(color);
}

这时再运行就不会出错了,因为在 int 型颜色值赋值之前已经被自动转换为 ColorDrawable 了。
肯定有人会有疑问:

  • 为什么我没有定义 convertColorToDrawable 这样的方法,直接写完布局文件就运行也没有报错呢?
  • convertColorToDrawable 这个方法是什么时机被调用的呢?

首先之所以没有报错的原因,其实是因 为convertColorToDrawable 这个方法在 dataBinding 的 jar 包中已经帮我们定义好了,我们不用再定义了,所以不会报错。源码路径为:android.databinding.adapters.Converters。
下面再说说调用时机:Android 中的每个 xml 中的属性都对应着相应的 set/get 方法的,如果 在xml 中设置的属性值的类型与对应的 Java 方法的参数类型不符,这时 dataBinding 就会去寻找可以让属性值转换为正确类型的方法,而寻找的根据就是所有被 @BindingConversion 注解标记的方法,这时 convertColorToDrawable 方法就会被调用了。

如果我们自己定义了一个功能相同的 convertColorToDrawable 方法,那么 dataBinding 会优先使用我们自己定义的方法。
如果我们自己定义了多个功能相同的convertColorToDrawable方法,比如convertColorToDrawable01,convertColorToDrawable02,convertColorToDrawable03,dataBinding会选择顺序靠后的方法去使用。

@InverseBindingAdapter

  • 用于双向绑定
  • 需要与@BindingAdapter配合使用
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
public @interface InverseBindingAdapter {

    String attribute();

    String event() default "";
}
  • attribute:String 类型,必填,表示当值发生变化时,要从哪个属性中检索这个变化的值,示例:"android:text"
  • event: String 类型,非必填,如果填写,则使用填写的内容作为 event 的值;如果不填,在编译时会根据 attribute 的属性名再加上后缀 "AttrChanged" 生成一个新的属性作为 event 的值,举个例子:attribute 属性的值为 "android:text",那么默认会在 "android:text" 后面追加 "AttrChanged" 字符串,生成 "android:textAttrChanged" 字符串作为 event 的值.
  • event属性的作用: 当 View 的值发生改变时用来通知 dataBinding 值已经发生改变了。开发者一般需要使用 @BindingAdapter 创建对应属性来响应这种改变。

我们找系统提供的 TextViewBindingAdapter 类中的方法来简单看一下(主要看我写的注释,我习惯把内容写到注释中):

    // 这里是咱们在 TextView 中使用 android:text="@{xxx}" 功能的具体实现,可以看到,它不是简单的 textView.setText(xxx)就完事了
    // 这里注意了,"android:text" 是支持双向绑定的,双向绑定会造成死循环,瞎面的代码添加了防止死循环的逻辑
    // @BindingAdapter("android:text")的作用是:代码中的值改变了通知布局让它显示出来
    @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);
    }

    /**
    * 注意了,这就是那个重点要看的,虽然内容就一行。。
    * 注解里面的参数翻译成大白话就是:
    *   当发现布局中的 text 发生改变的时候(EditTextView 用户输入),发一个事件出来("android:textAttrChanged")
    *   然后我们下边再建个 BindingAdapter 来响应这个改变,以此实现双向绑定
    */
    @InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
    public static String getTextString(TextView view) {
        return view.getText().toString();
    }
    
    //
    @BindingAdapter(value = {"android:beforeTextChanged", "android:onTextChanged",
            "android:afterTextChanged", "android:textAttrChanged"}, requireAll = false)
    public static void setTextWatcher(TextView view, final BeforeTextChanged before,
            final OnTextChanged on, final AfterTextChanged after,
            final InverseBindingListener textAttrChanged) {
        final TextWatcher newValue;
        if (before == null && after == null && on == null && textAttrChanged == null) {
            newValue = null;
        } else {
            newValue = new TextWatcher() {
                @Override
                public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                    if (before != null) {
                        before.beforeTextChanged(s, start, count, after);
                    }
                }

                @Override
                public void onTextChanged(CharSequence s, int start, int before, int count) {
                    if (on != null) {
                        on.onTextChanged(s, start, before, count);
                    }
                    if (textAttrChanged != null) {
                        // 主要看这里!
                        // 主要看这里!
                        // 其他自己看,这里会有个 InverseBindingListener 传入进来(原理我后边可能会写),
                        // 这个 listener 调用了一个 onChange() 方法,这里就会把布局修改 text 的事情告诉代码,然后代码中的值就变了
                        // onChange 的具体实现在对应的布局生成的 Binding文件中,想看的先自己研究去吧
                        textAttrChanged.onChange();
                    }
                }

                @Override
                public void afterTextChanged(Editable s) {
                    if (after != null) {
                        after.afterTextChanged(s);
                    }
                }
            };
        }
        final TextWatcher oldValue = ListenerUtil.trackListener(view, newValue, R.id.textWatcher);
        if (oldValue != null) {
            view.removeTextChangedListener(oldValue);
        }
        if (newValue != null) {
            view.addTextChangedListener(newValue);
        }
    }

@InverseBindingMethod 和 @InverseBindingMethods

@InverseBindingMethods 和 @BindingMethods 相似,都只是一个容器,用法也一样,真正的功能在 @InverseBindingMethod 中

@Target(ElementType.ANNOTATION_TYPE)
public @interface InverseBindingMethod {

    Class type();

    /**
     * 要支持双向绑定的属性名  例如 android:text 这个 text 就是属性名
     */
    String attribute();

    /**
     * event 不是必须的,event 代表当属性发生变化的时候通知哪个 Binding 事件,默认 attribute() + "AttrChanged"
     * 如果在 InverseBindingMethod 的 event 里面任意定义了一个方法 “xxx”,那么必须在该注解类有一个同名方法如 setXxx() 或者 @BindingAdapter 注解一个 “xxx” 方法,然后在这里面调用 InverseBindingListener 的 onChange()。
     * 当然,如果不是自定义的,默认的 setter 或者 BindingAdapter 提供也得有
     */
    String event() default "";

    /**
     * 这个是属性调用的 getter 的方法,默认调用基于属性名的,也可以自己指定
     */
    String method() default "";
}

例子来个简单的:

@InverseBindingMethods({
        @InverseBindingMethod(type = RatingBar.class, attribute = "android:rating"),
})
public class RatingBarBindingAdapter {
    @BindingAdapter("android:rating")
    public static void setRating(RatingBar view, float rating) {
        if (view.getRating() != rating) {
            view.setRating(rating);
        }
    }

    // 这里和 InverseBindingAdapter 一样添加了 InverseBindingListener 进去,两个注解都可实现双向绑定
    @BindingAdapter(value = {"android:onRatingChanged", "android:ratingAttrChanged"},
            requireAll = false)
    public static void setListeners(RatingBar view, final OnRatingBarChangeListener listener,
            final InverseBindingListener ratingChange) {
        if (ratingChange == null) {
            view.setOnRatingBarChangeListener(listener);
        } else {
            view.setOnRatingBarChangeListener(new OnRatingBarChangeListener() {
                @Override
                public void onRatingChanged(RatingBar ratingBar, float rating, boolean fromUser) {
                    if (listener != null) {
                        listener.onRatingChanged(ratingBar, rating, fromUser);
                    }
                    ratingChange.onChange();
                }
            });
        }
    }
}

这里我感觉两种实现双向绑定的方式看起来没什么太大区别,InverseBindingMethod 多了可以指定 getter 方法,InverseBindingAdapter 可以自己实现 getter 方法,如果有什么说的不对的地方,希望大家指正,我目前项目中没有用到自定义的双向绑定,因为觉得这些方法虽然方便了使用,但是增加了沟通交流成本,所以我尽量使用系统提供的方法来开发。

@InverseMethod

这个方法的用途就是为一个方法提供一个相反的方法。

    public @interface InverseMethod {
        /**
         * @return 这个参数是需要转换的方法名,被注解的和被转换的需要在同一个类中
         */
        String value();
    }

注意:

  • 转换的方法的入参是被转换方法的出参,反之亦然
  • 这么拗口

比如下面:

    // 因为被转换的方法传入 int 所以这个方法返回 int
    // 因为被转换的方法返回 String 所以这个方法传入 String
    @InverseMethod("convertIntToString")
    public static int convertStringToInt(String value) {
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            return -1;
        }
    }

    // 这个方法提供了从 String 转 int 的功能
    // 我们在布局中给一个 EditTextView 传了个 int,但是 android:text 只接收 String 类型的值,
    // 但我们用这个参数的时候又需要从 EditTextView 拿到 int 类型的值,
    // 为了避免手动转来转去,这个就是个非常 nice 的功能
    public static String convertIntToString(int value) {
        return String.valueOf(value);
    }
    
    <EditText
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="@={vm.convertIntToString(vm.persion.age)}"/>

教程还有好多想写的,这里讲的也不全,差不多是按照我学习的先后顺序来了,后面给大家讲一些 Google jetpack 开发组件(Architecture components),源码的解析暂时不会写了,主要是自己没有全部看完,相信你真正看完这些注解的时候源码已经看懂很多了。

在这里附上参考博文:

DataBinding使用教程(三):各个注解详解

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