JetPack之DataBinding

Jetpack之DataBinding

官方地址

1.简述

DataBinding是一个用于将数据绑定到应用界面布局文件中view元素的组件,能够避免我们手动重复逻辑,例如findViewById、setText等。

2.简单使用入门

需要注意的是,DataBinding能运行到API 14(4.0)或更高版本上;Gradle(android 版本)需要1.5.0或以上。

2.1 启用DataBinding

在module下的build.gradle的android节点下启用DataBinding。

android {
    ...
    dataBinding {
        enabled = true
    }
}

2.2 使用



    
        
    

    

        

    


上面中,根节点变成了layout,旗下有两个节点,分别是data和我们原始的布局的根节点,我们的数据源定义在data节点中。
在下面的TextView中,通过DataBinding把数据Chapter的name字段绑定到它的text属性上。
点击编译之后,DataBinding会生成一个Binding类,名字与布局文件名有关。一般情况下,Activity根布局情况下,使用DataBindingUtil.setContentView获取。如果是在View、Fragment、ListView或RecyclerView适配器中使用时可以通过指定生成类的inflate或者DataBindingUtil的inflate方法获取。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
//        setContentView(R.layout.activity_main);
    ActivityMainBinding mainBinding = DataBindingUtil
            .setContentView(this, R.layout.activity_main);
}

在DataBinding绑定时,可以使用一些表达式:

算术运算符 + - / * %

字符串连接运算符 +

逻辑运算符 && ||

二元运算符 & | ^

一元运算符 + - ! ~

移位运算符 >> >>> <<

比较运算符 == > < >= <=(请注意,< 需要转义为< ;,此处没有空格,主要时为了避免被自动转义)

instanceof

分组运算符 ()

字面量运算符 - 字符、字符串、数字、null

类型转换

方法调用

字段访问

数组访问 []

三元运算符 ?:

例如:


    
    



如果需要引用外部的类,可以通过在data中添加import标签来引入。如果有import的类出现冲突,那么需要使用alias来标识import的类的别名。另外import表示的是导入的类,并不能直接在后面的视图布局中作为变量使用,而只能使用其静态属性或者方法,要使用变量需要通过variable标签。
值得注意一些特殊情况,例如

1)表达式中有 <时,需要使用< ;(此处没有空格,主要时为了避免被自动转义)替换
2)字符串拼接时,外部应该用单引号,内部使用双引号

另外,DataBinding能够自动判断字段是否是null值,如果是,则不会显示内容;如果引用对象是null,引用的该对象的字段值时并不会报空指针异常,而是直接作为无数据处理。引用数据的初始值和类成员变量初始值类似,例如int默认为0.

2.3 集合类型的使用




    

在Activity中可以使用生成的类直接通过set/get方法操作绑定的数据

ActivityMainBinding mainBinding = DataBindingUtil
            .setContentView(this, R.layout.activity_main);

mainBinding.setChapter(null);
List datas = new ArrayList<>();
datas.add("jetpack");
mainBinding.setDatas(datas);
mainBinding.setIndex(0);

2.4资源的引用

这里仅简单介绍string资源的引用,其他参考官方文档。
因为我们普通的@string方式可以在不用dataBinding的情况下直接使用。所以这里以特殊情况,带参数的格式化字符串来示例。

名字是%1$s

布局文件中如下:


还有一种针对英文的复数形式,例如 1 time; 2 times,那么这时time后面得多一个s,这时使用@plurals,可以参考这篇文章
还有其他资源的引用并混合表达式,参考官方文档

2.5 方法关联

官方文档中归类为:事件处理、方法引用、监听器绑定。大致其实都一样,都是对布局文件中一些属性和方法的关联,最常见的也就是onClick属性,有以下两种形式:

形式一:

public class MyHandlers {
    public void onClickFriend(View view) { ... }
}


形式二:

public class Presenter {
    public void onSaveClick(Task task){}
}


    
    

注意:上述onClick方法会传递一个view参数,你可以选择接收或不接受,如下所示:

public class Presenter {
    public void onSaveClick(View view, Task task){}
}

android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"

形式一也可以按照形式二的方式写,例如:


另外,关联的方法如果有返回值,则必须保持一致。而且,如果请求的对象的值为null,如上面的simpleViewModel为null,系统也会自动帮助处理,而避免报空指针异常,引用类型返回 null,int 返回 0,boolean 返回 false,等等。
此外,还可以根据情况,返回不同的结果

android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"

注意:
1)内部表达式不宜太复杂,否则会使得布局代码难以阅读。

2)如果不同配置(例如横向或纵向)有不同的布局文件,则变量会合并在一起。这些布局文件之间不得存在有冲突的变量定义。

3)在生成的绑定类中,每个描述的变量都有一个对应的 setter 和 getter。在调用 setter 之前,这些变量一直采用默认的托管代码值,例如引用类型采用 null,int 采用 0,boolean 采用 false,等等。

2.5 特殊情况 include

我们可以通过include来包含其他布局文件,这时,子布局绑定的数据可以从父布局传递过来,如:



   
       
   
   
       
       
   

3.数据类

上面来看,布局绑定的值来自与数据类,如果数据类还没初始化,那么布局中值将会使用该类型的默认值,null则不显示内容。那么如果我们数据类实例设置了数据,那布局怎么知道要刷新界面呢?这就要用到可观察的数据对象了。使用了可观察的数据对象之后,数据的任何变动,都能及时反映到界面上。

3.1 通过实现Observable接口

通过类实现Observable接口来允许注册监听器。系统为我们封装了BaseObservable类,我们实现该类,并在对应的set方法中调用notifyPropertyChanged方法,以及在get方法上添加@Bindable注解。

private static class User extends BaseObservable {
    private String firstName;
    private String lastName;

    @Bindable
    public String getFirstName() {
        return this.firstName;
    }

    @Bindable
    public String getLastName() {
        return this.lastName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
        notifyPropertyChanged(BR.firstName);
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
        notifyPropertyChanged(BR.lastName);
    }
}

数据绑定在模块包中生成一个名为 BR 的类,该类包含用于数据绑定的资源的 ID(注意:需要get对应方法有@Bindable注解才能使用)

3.2 使用已有可观察对象

系统提供了以下直接使用的Observable类来使用,避免每次都需要写大量set/get方法,并在内部增加notifyPropertyChanged操作或注解。
ObservableBoolean、ObservableByte、ObservableChar、ObservableShort、ObservableInt、ObservableLong、ObservableFloat、ObservableDouble、ObservableParcelable

例如:

public class Chapter {
    public final ObservableInt visibleObservable = new ObservableInt();
    public int visible;
}




Chapter chapter = new Chapter();
mainBinding.setChapter(chapter);//mainBinding是一个binding对象
chapter.visibleObservable.set(1);

使用了这种方式后,变量必须设置为只读属性 public final ,并通过get/set方法操作数据。
此外,还有可观察的集合,就不再赘述了。

4.绑定类

在布局文件中绑定了视图属性和相关的数据类之后,通过make project构建项目,便会在编译期动态生成一个ViewDataBinding类,该类名与布局文件名关联。在前面我们已经说过,可以通过生成类的inflate或者DataBindingUtil来获取实例。常见形式有:

 ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);


ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);

MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);


View viewRoot = LayoutInflater.from(this).inflate(layoutId, parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bind(viewRoot);

在生成类中,我们可以直接引用布局文件中定义的id控件,这种方式避免了通过findViewById来获取view。
另外,生成类可以直接通过set/get方法来设置和获取数据。

4.1特殊情况

1)ViewStub

见官方文档

或者这篇文章中关于ViewStub部分的介绍

2)RecyclerView的ViewHolder的绑定

在RecyclerView的adapter中,onBindViewHolder中如果还不知道数据类的具体类型,这时可以通过动态变量来完成。通过BindingViewHolder来获取ViewDataBinding类,并调用setVariable方法类设置item,注意:前提是我们约定了每一项布局中对应的数据类variable变量名为item。
UnknownTypeAdapter.java

@Override
public void onBindViewHolder(@NonNull BindingHolder holder, int position) {
    //约定绑定的数据类变量名为item。
    holder.getBinding().setVariable(BR.item, mDatas.get(position));
    holder.getBinding().executePendingBindings();
}

4.3 自定义绑定类名和位置

通过给data标签设置class属性来决定生成的绑定类名和位置

在当前模块的 databinding 包中生成 ContactItem 绑定类:


您可以在类名前添加句点和前缀,从而在其他文件包中生成绑定类。以下示例在模块包中生成绑定类:


您还可以使用完整软件包名称来生成绑定类。以下示例在 com.example 包中创建 ContactItem 绑定类

5.绑定适配器

一般情况下,我们视图属性绑定数据类时,数据类中包含了该属性的set/get方法,同时视图中包含了对应的视图属性的set/get方法,绑定类会自动帮我们把视图属性和数据类属性绑定。例如,下面的数据变量chapter所对应的类中包含了name属性,同时也存在对应的set/get方法(如果是public类型或者ObservableXX类,则没有),而text属性对应了视图的setText和getText方法。

android:text="@{chapter.name}"

但也有例外:
1)即使不存在具有给定名称的属性,数据绑定也会起作用。然后,您可以使用数据绑定为任何 setter 创建特性。例如,支持类 DrawerLayout 没有任何属性,但有很多 setter。以下布局会自动将 setScrimColor(int) 和 setDrawerListener(DrawerListener) 方法分别用作 app:scrimColor 和 app:drawerListener 特性的 setter:


2)如果属性名和setter方法不匹配,这时我们可以通过@BindingMethods注解来绑定属性名和setter方法。该注释使用在类上,且可以包含多个 BindingMethod 注释,每个注释对应一个绑定规则。 例如,下面使得视图属性tint与视图的setImageTintList绑定

@BindingMethods({
       @BindingMethod(type = "android.widget.ImageView",
                      attribute = "android:tint",
                      method = "setImageTintList"),
})

3)如果视图不存在对应的set方法时,可以在视图中创建静态方法,结合@BindingAdapter注解,并传入视图view实例,来修改相关属性,例如,paddingLeft没有对应的setPaddingLeft方法。

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
  view.setPadding(padding,
                  view.getPaddingTop(),
                  view.getPaddingRight(),
                  view.getPaddingBottom());
}

第一个参数必须为对应的视图实例,后面的参数对应值类型。上面在注解中指明了对应的视图属性。我们可以传入多个值参数,例如:

示例

@BindingAdapter(value = {"imageUrl","errorDrawable"},requireAll = false)
public static void loadImage(ImageView imageView, String url, Drawable error){
    Glide.with(imageView.getContext()).load(url).error(error).into(imageView);
}

第二个参数对应了app:imageUrl,第三个参数对应了app:error,这里使用了app的命名空间。同时可以设置requireAll来决定布局文件中是否需要设置所有值才调用到该方法,例如上面设置false,那么可以只设置一个,也会调用到上面的loadImage方法中来。例如:


注意:
1)数据绑定库在匹配时会忽略自定义命名空间。
2)出现冲突时,绑定适配器会替换默认的数据绑定适配器。
3)由于BindingAdapter注解的是静态方法,因此通常用一个类统一管理。

5.1 BindingAdapter新旧值判断

上面的@BindingAdapter方式可以选择性在处理程序,根据旧值和新值来决定如何生效,但是定义的方法中头一个参数是旧值,后一个参数是新值。

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int oldPadding, int newPadding) {
  if (oldPadding != newPadding) {
      view.setPadding(newPadding,
                      view.getPaddingTop(),
                      view.getPaddingRight(),
                      view.getPaddingBottom());
   }
}

5.2 BindingAdapter的设置监听方法

前面通过onClick属性设置监听点击事件对应的方法时,本质是View.OnClickListener接口中的onClick方法。实际上,在布局文件中指定该视图的相关监听方法时,必须对应的是具有一种抽象方法的接口或抽象类。例如:

示例

@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
       View.OnLayoutChangeListener newValue) {
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
    if (oldValue != null) {
      view.removeOnLayoutChangeListener(oldValue);
    }
    if (newValue != null) {
      view.addOnLayoutChangeListener(newValue);
    }
  }
}



还有一种特殊情况是,如果该监听接口或者抽象类有多个抽象方法时,该如何指定呢?如下接口,有两个方法。

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
  void onViewDetachedFromWindow(View v);
}

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
  void onViewAttachedToWindow(View v);
}

这时需要在视图中对不同的方法设置不同的视图属性,并在BindingAdapter注解的静态方法中处理接口或抽象类的注册和解注册,使得这两个方法合并到一个接口或抽象类中。例如:

@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"}, requireAll=false)
public static void setListener(View view, OnViewDetachedFromWindow detach, OnViewAttachedToWindow attach) {
    if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
        OnAttachStateChangeListener newListener;
        if (detach == null && attach == null) {
            newListener = null;
        } else {
            newListener = new OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(View v) {
                    if (attach != null) {
                        attach.onViewAttachedToWindow(v);
                    }
                }
                @Override
                public void onViewDetachedFromWindow(View v) {
                    if (detach != null) {
                        detach.onViewDetachedFromWindow(v);
                    }
                }
            };
        }

        OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view, newListener,
                R.id.onAttachStateChangeListener);
        if (oldListener != null) {
            view.removeOnAttachStateChangeListener(oldListener);
        }
        if (newListener != null) {
            view.addOnAttachStateChangeListener(newListener);
        }
    }
}

上面操作中处理了旧监听器的解注册。

5.3自定义转换

见官方文档

6.双向数据绑定

默认情况下,我们只需要在之前绑定的表达式前添加一个=号,就能实现双向绑定,需要数据类中按照之前的规定添加@Bindable注解到getter上,同时有相关的setter方法(通过ObservableField的类似)


但有一种情况,如果我们通过绑定适配器来完成,那么这时视图的属性就会出现和视图内部的setter方法不对应的情况,例如上面例子中的

@BindingAdapter(value = {"imageUrl","errorDrawable"},requireAll = false)
public static void loadImage(ImageView imageView, String url, Drawable error){
    Glide.with(imageView.getContext()).load(url).error(error).into(imageView);
}

这个时候,我们可以布局文件中绑定数据时,通过app:imageUrl和app:errorDrawable属性配置图片地址和错误状态时的placeholder。那么这种情况下,ImageView中并没有setImageUrl和setErrorDrawable方法,那么这个时候,就没法实现双向绑定了,因为无法从视图上获取imageUrl和errorDrawable,编译器将会报错提示没有对应的方法存在。
这种情况下,我们一般需要自定义控件来完成从视图获取现有视图上的数据。前面数据驱动视图的时候,在数据类中的setter方法中,我们通过notifyPropertyChanged方法来通知视图刷新,数据绑定通过@BindingAdapter注解的方法找到对应的绑定操作,在通过getter获取数据并刷新数据到视图上。
可见,在数据驱动视图的情况下,有三个必要条件:

1)数据类中setter中内容变动时发出变动通知
2)根据绑定的方法,调用视图类中的setter来准备刷新界面
3)视图通过数据类的getter获取到数据类的数据

那么相反地,如果视图需要驱动数据变动,那么,应该也是类似的:

1)视图类中与数据相关的内容变动时,需要发出变动通知
2)调用数据类的setter来准备写回数据
3)数据类可以通过视图类的getter获取到视图中的现有数据

首先解决第一个问题,当视图中的内容变动时,需要通知给数据类。但值得注意的是,如果这个内容变动来源与数据类,应该避免通知,以避免出现数据类又重新通知给视图类,导致死循环。那这个怎么判断数据是否来源于数据类,如果增加参数这会导致所有地方都需要这样处理。更高效的做法是,通过判断当前的数据变动内容和之前是否一致,如果一致,那么不再刷新视图,这样如果数据变动来源于数据类,即使第一次不同会重新通知回数据类,后续将不会再刷新视图。系统提供了InverseBindingListener接口来通知到数据类,需要我们通过@BindingAdapter注解对应属性的静态方法,并第二个参数以InverseBindingListener作为参数,绑定数据工具会协助我们完成视图驱动数据的工作。例如下面中,我们想要监听的是content属性。

public class MyTextView extends AppCompatTextView {
    private InverseBindingListener mInverseBindingListener;

    @Override
    public void setText(CharSequence text, BufferType type) {
        super.setText(text, type);
        if(mInverseBindingListener != null){
            mInverseBindingListener.onChange();//通知数据类,当前视图内容变动了
        }
    }

    //用于数据变动刷新到视图view
    @BindingAdapter({"content"})
    public static void setContent(MyTextView view,String content){
        String current = view.getText().toString();
        if(content != null && !content.equals(current)){//2
            view.setText(content);
        }
    }
    
    @BindingAdapter(value = {"contentAttrChanged"},requireAll = false)
    public static void setContentChanged(MyTextView view, InverseBindingListener listener){//1
        view.mInverseBindingListener = listener;
    }
}

上面注释1处,定义了一个方法,并通过@BindingAdapter指明了该方法所对应的监听属性变动值(该值由一般我们监听的属性+"AttrChanged"组成,且与后面的getter方法指定的event对应),这样当绑定工具就能够找到该方法,给视图变动设置监听器。注释2处判断数据是否相同,避免陷入死循环。

看第二个问题前,先看看之前在上面例子中,loadImage方法使用了数据刷新图片内容到ImageView上,那相应地,如果双向绑定,也就是说图片显示的地址变动了,应该给数据类设置新值。由于ImageView并没有存储图片地址,因此如果需要真正双向绑定,仅仅使用ImageView是无法完成的,而需要自定义控件,并能通过绑定工具拿到数据类setter方法来写回数据。

第三个问题中,需要通过@InverseBindingAdapter注解来指明通过视图获取内容的方法。

@InverseBindingAdapter(attribute = "content",event = "contentAttrChanged")
public static String getContent(MyTextView view){
    return view.getText().toString();
}

最后需要注意,别忘了在布局文件中添加双向绑定表达式(增加=号)。

public class MyTextView extends AppCompatTextView {
    private InverseBindingListener mInverseBindingListener;

    @Override
    public void setText(CharSequence text, BufferType type) {
        super.setText(text, type);
        if(mInverseBindingListener != null){
            mInverseBindingListener.onChange();//通知数据类,当前视图内容变动了
        }
    }

    //用于数据变动刷新到视图view
    @BindingAdapter({"content"})
    public static void setContent(MyTextView view,String content){
        String current = view.getText().toString();
        if(content != null && !content.equals(current)){//避免进入死循环
            view.setText(content);
        }
    }

    @InverseBindingAdapter(attribute = "content",event = "contentAttrChanged")
    public static String getContent(MyTextView view){
        return view.getText().toString();
    }

    @BindingAdapter(value = {"contentAttrChanged"},requireAll = false)
    public static void setContentChanged(MyTextView view, InverseBindingListener listener){
        view.mInverseBindingListener = listener;
    }
}

上面方式中,都是在静态方法中添加注解完成的,还有另一种方式,直接在自定义类内部定义与属性名关联(在上面的方式中,方法名没有限定与属性值相关)的方法,这时需要通过@InverseBindingMethods类注解指定属性值、设置监听器和获取视图值的方法,而且必须严格按照注解中的值设定方法名。例如上面例子中可以改成如下:

@InverseBindingMethods(@InverseBindingMethod(type = MyTextView.class,
event = "contentAttrChanged",//方法名必须是setContentAttrChanged
method = "getContent",//方法名必须是getContent
attribute = "content"))//方法名必须是setContent
public class MyTextView extends AppCompatTextView {
    private InverseBindingListener mInverseBindingListener;

    @Override
    public void setText(CharSequence text, BufferType type) {
        super.setText(text, type);
        if(mInverseBindingListener != null){
            mInverseBindingListener.onChange();//通知数据类,当前视图内容变动了
        }
    }

    public void setContent(String content){
        String current = getText().toString();
        if(content != null && !content.equals(current)){
            setText(content);
        }
    }

    public String getContent(){
        return getText().toString();
    }

    public void setContentAttrChanged(InverseBindingListener listener){
        if(listener != null){
            mInverseBindingListener = listener;
        }else{
            mInverseBindingListener = null;
        }
    }
}

上述两种方式的源码

你可能感兴趣的:(JetPack之DataBinding)