Android Data Binding 详解及使用案例

构建环境

使用条件

  • 支持 Android 2.1 (API level 7+).
  • Android Plugin for Gradle 1.5.0-alpha1 或更高
    buildscript {
    ...
    dependencies {
      classpath 'com.android.tools.build:gradle:1.5.0-alpha1'
    }
    }

    开启

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

    基本用法 (Data Binding Layout Files)

    绑定实体数据到指定布局(Layout)

    public class User {
     private final String firstName;
     private final String lastName;
     public User(String firstName, String lastName) {
         this.firstName = firstName;
         this.lastName = lastName;
     }
     public String getFirstName() {
         return this.firstName;
     }
     public String getLastName() {
         return this.lastName;
     }
    }

如果我需要用Layout展示用户信息 User,通常我们的做法就是

  1. 新建Layout.xml
  2. 找到对应控件,findIdByView或 Butterknife
  3. 设置数据;

感觉用Butterknife已经很高大上,很高效了,看一下Data Binding的效率:
main_activity.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
       android:id="@+id/mFirstName"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"/>
   </LinearLayout>
</layout>

MainActivity.java

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
   User user = new User("Test", "User");
   binding.setUser(user);
}

高效没看出来呀!为了让接手的下一个兄弟不骂娘,我还是选择普通写法o(╯□╰)o,那如果是这种布局呢


你用几百行的代码,DataBinding一行搞定,Boss此刻应该蹲在厕所喜极而泣,我XX我早该找一个这样的程序员。
data_binding_funny.gif

小知识点

  • Fragment ,ListView,RecyclerView 中的使用
    // ListView RecyclerView
    ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
    //Fragment
    MainFragmentBinding binding =
              DataBindingUtil.inflate(inflater, R.layout.main_fragment, container, false);
  • 如果想操作对应的控件需要设置id号,如main_activity.xml, 设置android:id="@+id/mFirstName" ,可以直接用binding.mFirstName 找到控件 ;

【待处理】
测试中得到binding

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

事件绑定(Event Handling)

最简单的应用

main_activity.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="handlers" type="com.example.MyHandlers"/>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"
           android:onClick="@{handlers::onClickFriend}"/>
   </LinearLayout>
</layout>

事件处理类

public class/*也可以是接口*/ MyHandlers  {
    public void onClickFriend(View view) { ... }
}

控制代码同上,设置变量就可以binding.setHandlers(...)

带参数的事件绑定(Android Plugin for Gradle 2.0 或更高)

main_activity.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="handlers" type="com.example.MyHandlers"/>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"
           android:onClick="@{() -> handlers.onSaveClick(user)}"/>
   </LinearLayout>
</layout>

事件处理类

public class MyHandlers {
    public void onSaveClick(User user){}
}

传入View方式

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

  <CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content"
        android:onCheckedChanged="@{(cb, isChecked) -> handlers.completeChanged(user, isChecked)}" />

注意:避免一些比较复杂的listeners,这样会使得Layout更易读更容易维护

Data Layout 小细节及简单语法

Imports

<data>
    <import type="android.text.TextUtils"/> <!--静态类的使用-->
    <import type="android.view.View"/> <!--导入-->
     <variable name="note"  type="String"/><!--自动导入java.lang.*-->
    <import type="com.example.real.estate.View"
        alias="Vista"/> <!--别名解决同名类-->
</data>
<TextView 
   android:text="@{TextUtils.isEmpty(user.lastName)? "default":user.lastName }"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/> <!-【使用】-->

Note: 默认自动导入java.lang.*,常用的有Integer,String,StringBuffer,Math类

Binding Class Names 如上:MainActivityBinding 类

  • 默认命名规则 包名+布局名去‘_’首字母大写+Binding 如: main_activity.xml ==> MainActivityBinding.class
  • 自定义的三种解释
    <data class="ContactItem"></data> <!--自定义名称-->
    <data class=".ContactItem"></data> <!--使用module包名-->
    <data class="com.example.ContactItem"></data> <!--指定包名-->

    Includes

    Data layout 复用
    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:bind="http://schemas.android.com/apk/res-auto">
     <data>
         <variable name="user" type="com.example.User"/>
     </data>
     <LinearLayout
         android:orientation="vertical"
         android:layout_width="match_parent"
         android:layout_height="match_parent">
         <include layout="@layout/name"
             bind:user="@{user}"/>
         <include layout="@layout/contact"
             bind:user="@{user}"/>
     </LinearLayout>
    </layout>

    简单语法

    Java共同特征

  • Mathematical + - / * %
  • String concatenation +
  • Logical && ||
  • Binary & | ^
  • Unary + - ! ~
  • Shift >> >>> <<
  • Comparison == > < >= <=
  • instanceof
  • Grouping ()
  • Literals - character, String, numeric, null
  • Cast
  • Method calls
  • Field access
  • Array access []
  • Ternary operator ?:

Examples:

android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}' 

android:text="@{user.displayName ?? user.lastName}" 【重点】<==> 等价于
android:text="@{user.displayName != null ? user.displayName : user.lastName}"

Resources引用

Type Normal Reference Expression Reference
String[] @array @stringArray
int[] @array @intArray
TypedArray @array @typedArray
Animator @animator @animator
StateListAnimator @animator @stateListAnimator
color int @color @color
ColorStateList @color @colorStateList

Sample

android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"

String formatting

<resources>
    <string name="greeting">Hello, %s</string>
</resources>

<TextView
    android:text="@{@string/greeting(user.firstName)}"/>

Math in expressions

<TextView
    android:padding="@dimen/padding"
    android:padding="@{@dimen/padding}"
    android:padding="@{@dimen/padding * 2}"
    android:padding="@{@dimen/padding + @dimen/padding}"
    android:padding="@{largeScreen ? @dimen/padding * 2 : @dimen/padding}"
/>

更改数据自动更新UI界面 (Data Objects)

经过上的学习,有木有发现, 布局Layout中的数据variables是固定的,只有初始化的时候赋值 数据变动时(设置数据,得到数据),还需要找控件,设置参数,更改UI状态, 基本还是老套路,官方的解决方案是Observable,使用观察者模式,当数据变化时通知layout自动变化,
官方提供的类:

  • Observable Objects ;
  • ObservableFields ;
  • Observable Collections;

Observable Objects

public  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);
   }
}

经过改造后的User,同上的初始化

private User user ;
@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
   user = new User("Test", "User");
   binding.setUser(user);



   //点击LastName触发
   binding.setHandlers(new MyHandlers() { 
     @Override public void onClickFriend(View view) {
      user.setFirstName("FirstName"); //布局自动更新为`FirstName`
      //此后常用的操作
      //布局中Progress android:visibility="@{TextUtil.isEmpty(user.firstName) ? View.GONE : View.VISIBLE}"
      // 自动控制对应的Progress是否显示,或进行一些简单的逻辑处理
     }
   });
}

ObservableFields

private static class User {
   public final ObservableField<String> firstName =
       new ObservableField<>();
   public final ObservableField<String> lastName =
       new ObservableField<>();
   public final ObservableInt age = new ObservableInt();
}

// 对应的set/get
user.firstName.set("Google");
int age = user.age.get();

Observable Collections

ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);

高阶用法

在RecyclerView中的使用 (Dynamic Variables)

BindingHolder.java

  public static class BindingHolder extends RecyclerView.ViewHolder{
    //加入 ViewDataBinding setter/getter 方法
    private ViewDataBinding binding ; 

    public ViewDataBinding getBinding() {
      return binding;
    }
    public void setBinding(ViewDataBinding binding) {
      this.binding = binding;
    }

    public BindingHolder(View itemView) {
      super(itemView);
    }
  }

MyAdapter.java

private class MyAdapter extends RecyclerView.Adapter<BindingHolder>{
    private  List<User> users;

    public MyAdapter(List<User> users) {
      this.users = users;
    }

    @Override public BindingHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
      //核心代码
      ViewDataBinding binding = DataBindingUtil.inflate(
          LayoutInflater.from(viewGroup.getContext()),
          R.layout.list_item,
          viewGroup,
          false);
      BindingHolder holder = new BindingHolder(binding.getRoot());
      holder.setBinding(binding);
      return holder;
    }

    @Override public void onBindViewHolder(BindingHolder holder, int position) {
      User user = users.get(position);
      holder.getBinding().setVariable(BR.user, user);
      //不等待下一帧(Frame),直接更新,这个方法必须运行在UI线程
      holder.getBinding().executePendingBindings();
    }

    @Override public int getItemCount() {
      return users.size();
    }
  }

更简洁的用法

public class UserAdapter extends RecyclerView.Adapter<UserAdapter.UserHolder> {
    private static final int USER_COUNT = 10;

    @NonNull
    private List<User> mUsers;

    public UserAdapter() {
        mUsers = new ArrayList<>(10);
        for (int i = 0; i < USER_COUNT; i ++) {
            User user = new User(RandomNames.nextFirstName(), RandomNames.nextLastName());
            mUsers.add(user);
        }
    }

    public static class UserHolder extends RecyclerView.ViewHolder {
        private UserItemBinding mBinding;

        public UserHolder(View itemView) {
            super(itemView);
            mBinding = DataBindingUtil.bind(itemView);
        }

        public void bind(@NonNull User user) {
            mBinding.setUser(user);
        }
    }

    @Override
    public UserHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
        View itemView = LayoutInflater.from(viewGroup.getContext())
                .inflate(R.layout.user_item, viewGroup, false);
        return new UserHolder(itemView);
    }

    @Override
    public void onBindViewHolder(UserHolder holder, int position) {
        holder.bind(mUsers.get(position));
    }

    @Override
    public int getItemCount() {
        return mUsers.size();
    }
}

ViewStubs使用

ViewStub 是不可见的,0大小常用于惰性加载,说白了就是占给位置,在父容器的inflate时候不用处理,当在ViewStub setVisibility(int) or inflate()才会加载到布局中,应用场景:错误提示,帮助提示,用户引导等;
用法:

     <ViewStub android:id="@+id/stub"
               android:inflatedId="@+id/subTree"
               android:layout="@layout/mySubTree"
               android:layout_width="120dip"
               android:layout_height="40dip" />
  ViewStub stub = (ViewStub) findViewById(R.id.stub);
     View inflated = stub.inflate();

Data layout

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <LinearLayout
        ...> 
        <!--需要增加ID-->
        <ViewStub
            android:id="@+id/view_stub"
            android:layout="@layout/view_stub"
            ... />
    </LinearLayout>
</layout>

监听ViewStub inflate 事件

binding.viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
    @Override
    public void onInflate(ViewStub stub, View inflated) {
        ViewStubBinding binding = DataBindingUtil.bind(inflated);
        User user = new User("fee", "lang");
        binding.setUser(user);
    }
});

自定义Setter

实用小例子:

设置url,控件直接加载url地址的图片
attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <attr name="url" format="string"/>
</resources>

在任意代码中加入如下:

@BindingAdapter("url")
public static void loadImage(final ImageView imageView, final String url){
    Glide.with(imageView.getContext().getApplicationContext())
                    .load(url)
                     .into(imageView);
}

能得到变化的url及对应的ImageView控件

特殊说明: MasteringAndroidDataBinding中是用declare-styleable加自定义控件的方法来处理数据变化,上面的例子最能达到实践效果,在原生控件中加入自定义是属性,简洁易用;

BindingAdapter进价用法

观察多个属性 如: @BindingAdapter(value = {"url","drawable"},requireAll = false) requireAll指 布局控件中是否需要"url"``"drawable" 属性都存在,默认为true;

监听原生控件的属性变化,做响应的处理(重写),如:

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

获取变化的旧数据和新数据,自义定属性也能使用

@BindingAdapter("android:paddingLeft") //注意:android:paddingLeft 可以改为自定义的Setter属性,如上‘url’
public static void setPaddingLeft(View view, int oldPadding, int newPadding) {
   if (oldPadding != newPadding) {
       view.setPadding(newPadding,
                       view.getPaddingTop(),
                       view.getPaddingRight(),
                       view.getPaddingBottom());
   }
}

总结: Data Binding 核心思想是把控件的属性用静态变量代表,并监听属性的变化,通过BindingAdapter来处理数据变化;如果不是很有感觉, 有兴趣进一步研究,建议看一下MVVMLight源码, 一个Data Binding 和RxJava 结合使用的库

单元测试【待实践】

安卓 Data Binding 使用方法总结(姐姐篇)

学习资料

实在的博客

工作原理

开源库学习资料

补充 View Binding

使用条件 Android Studio 3.6 +

buildscript {
    ……
    dependencies {
        classpath "com.android.tools.build:gradle:3.6.0-beta01"
    }
}

激活配置

android {
  buildFeatures {
    viewBinding = true
  }
}

Activity 使用

布局activity_main.xml

使用 : 命名根据布局名称 + Binding

@Override
    protected void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding inflate = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(inflate.getRoot());
    }

Fragment 使用

布局fragement_main

使用: 命名同上

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        FragementMainBinding inflate = FragementMainBinding.inflate(inflater, container, false);
        inflate.tv.setText("Holy G00gie");
        return inflate.getRoot();
    }

附录

Resource 引用对照表

Type Normal Reference Expression Reference
String[] @array @stringArray
int[] @array @intArray
TypedArray @array @typedArray
Animator @animator @animator
StateListAnimator @animator @stateListAnimator
color int @color @color
ColorStateList @color @colorStateList
### 常用转义表
```shell
显示结果 描述 转义字符 十进制
空格    
< 小于号 < <
> 大于号 > >
& 与号 & &
“ 引号 " "
‘ 撇号 ' '
× 乘号 × ×
÷ 除号 ÷ ÷
```

引用:

  1. Data Binding Library
  2. MasteringAndroidDataBinding
  3. Data Binding in the Real World
  4. 安卓 Data Binding 使用方法总结(姐姐篇)