0. 前言

在项目中使用到了 Data Binding,总结使用经验后写成本文。 本文涉及安卓自带框架 DataBinding 的基础使用方法,适合初次接触 Data Binding 的同学阅读。

1. Data Binding 利弊

优势

DataBinding 出现以前,我们在实现 UI 界面时,不可避免的编写大量的毫无营养的代码:比如 View.findViewById();比如各种更新 View 属性的 setter:setText(),setVisibility(),setEnabled() 或者 setOnClickListener() 等等。

这些“垃圾代码”数量越多,越容易滋生 bug。

使用 DataBinding,我们可以避免书写这些“垃圾代码”。

劣势

使用 Data Binding 会增加编译出的 apk 文件的类数量和方法数量。

新建一个空的工程,统计打开 build.gradle 中 Data Binding 开关前后的 apk 文件中类数量和方法数量,类增加了 120+,方法数增加了 9k+(开启混淆后该数量减少为 3k+)。

如果工程对方法数量很敏感的话,请慎重使用 Data Binding。

2. 怎么使用 DataBinding

2.0 配置 build.gradle

Gradle 1.5 alpha 及以上自带支持 DataBinding,仅需在使用 DataBinding 的 module 里面的 build.gradle 里面加上配置即可:

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

Android 11修改系统隐藏导航栏属性 安卓11默认隐藏data_Data

在详细学习 DataBinding 之前,我们可以先来看下一个简单的例子:LoginDemo4DataBinding。该例子使用非 DataBinding 技术和 DataBinding 技术 2 种方式,实现了一个简单的登录页面。通过该例子,我们可以直观的感受下 DataBinding 的不同。

下面我们详细讨论 DataBinding 的使用方法,下面的例子实现的是在界面上显示两个文本:姓氏和名字。

2.1 公共代码

公用的 Activity 如下:

public class DataBindingActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        TestDbBinding binding = DataBindingUtil.setContentView(this, R.layout.test_db_layout);

        // viewmodel
        UserViewModel viewModel = new UserViewModel();
        binding.setUser(viewModel);

    }
}

其中,TestDbBinding 是根据 R.layout.test_db_layout 自动生成的,binding.setUser() 方法也是根据 layout 中 variable name 自动生成的。

公用的 R.layout.test_db_layout 如下:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="userViewModel" type="com.example.UserViewModel"/>
   </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="@{userViewModel.firstName}"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{userViewModel.lastName}"/>
   </LinearLayout>
</layout>

做过 J2EE 开发的同学,有没有似曾相识感觉?感人感觉,不管是 Data Binding,还是 React Native,都是将 Web 开发的先进思想或技术引进到移动开发领域的一种尝试。更具体的说,是将声明式编程(Declarative programming,如 JavaScript,CSS 等)引入命令式编程(Imperative programming,如 Java 等)中。

<div id="welcome">Welcome <a href="#">{user.username}</a> <a href="{site}home/logout">logout</a></div>
        <div class="clear"></div>
        {if:isset(menus)}
        {if:menus}
        <div id="moduleList">
            <ul>
                {foreach:menus,$menu}
                <li {if:$menu.is_active} class="current" {end}><div><a href="{site}{$menu.m_uri}/">{__($menu.m_label)}</a></div></li>
                {end}
            </ul>
        </div>

2.2 仅作静态展示

如果 UI 比较简单,界面仅仅是静态展示,不涉及 UI 的动态更新,以下代码就能满足需求了。

public class UserViewModel {
   public final String firstName;
   public final String lastName;
   public User(String firstName, String lastName) {
       this.firstName = firstName;
       this.lastName = lastName;
   }
}

即使在复杂的 UI 界面中,多数界面元素仅是静态展示,只有少数界面元素才需要根据用户的操作动态更新。

以微信朋友圈界面为例,一条状态的用户头像、昵称、内容、时间等这些元素一旦加载成功,就不再改变了;而点赞列表和评论列表是动态改变的。

如何使用 DataBinding 动态更新界面数据呢?

2.3 动态更新数据

有3种方式实现动态更新界面数据:

  • 实现 Observable 接口;
  • 继承 BaseObservable;
  • 使用 ObservableField;

2.3.0 实现 Observable 接口

由于 Java 不允许多继承,而允许同时实现多个接口,所以该方法更具有通用性。

2.3.1 继承 BaseObservable

public class UserViewModel extends BaseObservable {
    private String firstName;
    private String lastName;

    public UserViewModel(String firstname, String lastname) {
        this.firstName = firstname;
        this.lastName = lastname;
    }

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

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

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

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

BR.java 是类似 R.java 的资源文件,是 Binding Resources 的缩写,由框架自动生成。

注意,BR 中的 id 生成的依据是 @Bindable 修饰的方法名 getXXX(),而非方法体的内容。当在 getXXX() 方法前加 @Bindable 之后, BR.java 中就立即生成常量 BR.xXX。

还有另外一种写法:

public @Bindable String firstName;

@Bindable 可以放在 public 之前或之后,但是不能放在 String 之后。

这种方式,框架会自动生成 getFirstName() 方法。注意,此时变量的访问权限必须是 public。

如果 @Bindable 修饰的变量和 @Bindable 修饰的该变量的 getter 方法同时存在,则 getter 方法失效。

上述两种方式的区别在于,@Bindable 修饰的 get 方法体,不一定是简单的 return xxx,也可以是复杂的处理过程。例如,界面上显示的是 displayName,而 displayName 是由 firstName 和 lastName 按一定规则加工生成,改变 firstName 或 lastName 均会导致 displayName 对应的 UI 元素更新。这时,我们可以这么写:

// ...
    private String displayName;

    // ...

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

        notifyPropertyChanged(com.mmlovesyy.displaynamedemo.BR.displayName);
    }

    @Bindable
    public String getDisplayName() {
        return firstName + "." + lastName;
    }

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

        notifyPropertyChanged(com.mmlovesyy.displaynamedemo.BR.displayName);
    }

<TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text='@{user.displayName}' />

而如果使用 public @Bindable String displayName;,由于 get 方法是框架自动生成的,方法体是 return displayName;, 我们将无法做到这种效果。

其实,从这个案例我们可以一窥其动态更新的原理:通过 setLastName() 改变 lastName,并在该方法中通知订阅者,订阅者再调用 getDisplayName() 方法来代替 layout 文件中的 user.displayName,

2.2.2 使用 ObservableXXX / ObservableField<T>

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

ObservableByte / ObservableChar / ObservableShort / ObservableInt /ObservableLong / ObservableFloat / ObservableDouble / ObservableBoolean / ObservableParcelable 为基本数据类型;ObservableField<T> 对应应用类型,如 String,Integer 等。

注意,firstName 的操作方法是 get() 和 set()方法,例如要更新 firstName 的值:

firstName.set("linus chen");
age.set(age.get() + 1);

推而广之,不管是 ObservableInt/ObservableBoolean/ObservableFloat 等基本数据类型,还是ObservableField<T>的变量,只有调用其 set() 方法,其绑定的 UI 元素才会更新。这是因为,更新 UI 元素操作(notifyChanged() 方法)是在 set() 方法中触发的,具体见如下代码:

ObservableInt.java

public class ObservableInt extends BaseObservable implements Parcelable, Serializable {

    /**
     * Set the stored value.
     */
    public void set(int value) {
        if (value != mValue) {
            mValue = value;
            notifyChange();
        }
    }
}

ObservableField.java

public class ObservableField<T> extends BaseObservable implements Serializable {

    /**
     * Set the stored value.
     */
    public void set(T value) {
        if (value != mValue) {
            mValue = value;
            notifyChange();
        }
    }
}

3. 动态更新 ViewGroup

前几节写的都是比较简单的使用方法,是在 View 已经确定的情况下更新其属性(数据、可见性等)。那么如何更新 ViewGroup 呢,即动态的向 ViewGroup 添加或移除子 View 呢?这是我们需要使用 @BindingAdapter({“bind:attr1”, “bind:attr2”}) 注解。

先简要介绍一下该注解的使用方法,然后用一个例子来具体说明。 在 layout 中,使用 “app:attr1” 的格式来添加参数,这些参数会被传递到 @BindingAdapter 修饰的方法中,方法必须是 public static void 类型。注意其中的 static,这就限制了方法体中使用的变量(基本类型,引用类型,各种 XXXListener等)和方法要么通过 @BindingAdapter 传进去的,要么是 static的。

当你被 static 困扰时,请考虑一下通过 @BindingAdapter 里面的参数传进去。

举个动态生成的 View 的 click 事件和 layout 中 DataBinding 的事件的交互的例子,就是通过参数将 OnClickListener 的实例以参数的形式传给 static 方法,而实例 onClick() 实际调用 ViewModel 中的 handleOnClick() 方法。

举个例子,效果图如下,向一个 LinearLayout 中动态添加 TextView:

Android 11修改系统隐藏导航栏属性 安卓11默认隐藏data_UI_02

layout 代码如下:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="viewModel"
            type="com.mmlovesyy.bindingadapterdemo.NamesViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin">

        <Button
            android:id="@+id/add_btn"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{viewModel.onClick}"
            android:text="+" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            app:context="@{viewModel.context}"
            app:names="@{viewModel.names}"></LinearLayout>


    </LinearLayout>


</layout>

Activity 代码如下:

public class MainActivity extends AppCompatActivity {

    private NamesViewModel viewModel = new NamesViewModel(this);

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

NamesViewModel 代码如下:

public class NamesViewModel {

    public Context context;
    public final ObservableArrayList<String> names = new ObservableArrayList<>();

    public NamesViewModel(Context context) {
        names.add("linus chen");
        names.add("lin xueyan");
        names.add("zhang xiaona");
        names.add("chen lei");
        names.add("liu yuhong");

        this.context = context;
    }

    @BindingAdapter({"bind:names", "bind:context"})
    public static void setNames(ViewGroup linearLayout, ArrayList<String> names, Context context) {

        linearLayout.removeAllViews();

        for (String s : names) {
            TextView t = new TextView(context);
            t.setText(s);
            linearLayout.addView(t);
        }
    }

    public void onClick(View v) {

        int id = v.getId();

        if (id == R.id.add_btn) {
            names.add("yanyu cai");
        }
    }
}

4. 如何调试

如果编译出错,log 日志的报错信息,即出错的代码行是指框架根据 xml 文件生成的 xxxBinding.java,,由于目前 Android Studio 尚不支持自动定位出错代码行,所以我们要手动去找该文件。xxxBinding.java 文件位置:将 AS 切换成 Project 视图 - 对应的 module(如 app)- build - intermediates - classes - debug - 对应的 package。

然后根据出错代码行,推测对应的 xml 文件中出错的位置。

希望以后 Android Studio 能改善这方面的体验。其实,这也间接要求我们不要在表达式使用复杂的逻辑,越简单越容易调试。

5. 工作原理

请参考这篇文章:《Android Data Binding从抵触到爱不释手》。

6. 性能如何

请参考 Marshmallow Brings Data Bindings to Android 视频下方文字部分,在 Performance 一节中,Data Binding 的作者详细论述了其性能。

7. 单元测试

这里要讲的单元测试主要是针对 @BindingAdapter 修饰的方法的。

我们可以这么写:

public class MyBindingAdapters {
    @BindingAdapter("android:text")
    public static void setText(TextView view, String value) {
        if (isTesting) {
            doTestingStuff(view, value);
        } else {
            TextViewBindingAdapter.setText(view, value);
        }
    }
}

这有点恶心,而且由于 setText() 方法是 static 的,所以它里面使用的变量或方法都必须是 static 的,即变量 isTesting 和 doTestingStuff() 都是 static 的,更加不方便,这显然是面向过程编程的方法,不符合 OCP 原则(对扩展开放,非修改封闭)。

我们有一种更好的方法来做单元测试。不过首先我们要先来了解 android.databinding.DataBindingComponent 这个接口的用法,弄懂了它的用法,就知道怎么做单元测试。而且不仅仅可以做单元测试,还有其他用途。

UML 图:

Android 11修改系统隐藏导航栏属性 安卓11默认隐藏data_databindin_03

DataBindingComponent.java

/**
 * This interface is generated during compilation to contain getters for all used instance
 * BindingAdapters. When a BindingAdapter is an instance method, an instance of the class
 * implementing the method must be instantiated. This interface will be generated with a getter
 * for each class with the name get* where * is simple class name of the declaring BindingAdapter
 * class/interface. Name collisions will be resolved by adding a numeric suffix to the getter.
 * <p>
 * An instance of this class may also be passed into static or instance BindingAdapters as the
 * first parameter.
 * <p>
 * If using Dagger 2, the developer should extend this interface and annotate the extended interface
 * as a Component.
 *
 * @see DataBindingUtil#setDefaultComponent(DataBindingComponent)
 * @see DataBindingUtil#inflate(LayoutInflater, int, ViewGroup, boolean, DataBindingComponent)
 * @see DataBindingUtil#bind(View, DataBindingComponent)
 */
public interface DataBindingComponent {
}

这是一个空的接口,没有声明任何方法。注意文件开头的注释部分。定义一个抽象类,抽象 @BindingAdapter 修饰的方法,这里我们使用 setText() 方法作为例子。

MyBindingAdapter.java

public abstract class MyBindingAdapters {

    @BindingAdapter("android:text")
    public abstract void setText(MyDataBindingComponent component, TextView view, String value);
}

再定义两个 MyBindingAdapters 的子类,分别用于单元测试和实际生产环境:TestBindingAdapters.java 和 ProdBindingAdapters.java:

TestBindingAdapters.java

private static final String TAG = "TestBindingAdapters";

    @Override
    public void setText(MyDataBindingComponent component, TextView view, String value) {
        // test code
        Log.d("TestBindingAdapters", "SETTEXT INVOKED");
    }

ProdBindingAdapters.java

public class ProdCBindingAdapters extends MyBindingAdapters {
    @Override
    public void setText(MyDataBindingComponent component, TextView view, String value) {
        TextViewBindingAdapter.setText(view, value);
    }
}

注意,这两个类提供的 @BindingAdapter 修饰的方法都是非 static 的。

我们在

DataBindingUtil.setContentView(Activity activity, int layoutId,DataBindingComponent bindingComponent);

中要使用的是 DataBindingComponent,所以我们还要定义一个 MyDataBindingComponent,及其两个子类:TestComponent 和 ProdComponent,分别与 TestBindingAdapters 和 ProdBindingAdapters 相对应。

MyDataBindingComponent.java

public interface MyDataBindingComponent extends android.databinding.DataBindingComponent {
    MyBindingAdapters getMyBindingAdapters();
}

TestComponent.java

public class TestComponent implements MyDataBindingComponent {

    private MyBindingAdapters mAdapter = new TestBindingAdapters();

    @Override
    public MyBindingAdapters getMyBindingAdapters() {
        return mAdapter;
    }
}

ProdComponent.java

public class ProdComponent implements MyDataBindingComponent {

    private String color;

    public ProdComponent(String _color) {
        color = _color;
    }

    private MyBindingAdapters mAdapter = new ProdCBindingAdapters();

    @Override
    public MyBindingAdapters getMyBindingAdapters() {
        return mAdapter;
    }

    public String getColor() {
        return color;
    }
}

然后在 MainActivity 中调用:

public class MainActivity extends AppCompatActivity {

    private UserViewModel user;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main, new ProdComponent("blue"));
        user = new UserViewModel("linus", "chen");
        binding.setUser(user);
    }

    @BindingAdapter("android:url")
    public static void setColor(ProdComponent component, TextView view, String url) {
        view.setText(component.getColor());
    }
}

注意,@BindingAdapter 修饰的方法包含在类 MyBindingAdapter 中,所以 DataBindingUtil.setContentView(this, R.layout.activity_main, new ProdComponent("blue")); 中的最后一个参数,DataBindingComponent 类中必须包含一个名为 getMyBindingAdapter() 的 getter 方法,遵循本节开头的文件注释中的规定。

这样,我们使用 DataBindingUtil.setContentView(this, R.layout.activity_main, new TestComponent()); 进行单元测试。

我们还可以看到,DataBindingComponent 中可以书写其他方法(例如网络下载方法等),供 @BindingAdapter 修饰的方法(不论是 static 或非 static)调用。

如果要使用 Dagger2, 则代码如下: Dagger2

@Module
    public class TestModule {
        @Provides
        public MyBindingAdapters getMyBindingAdapter() {
            return TestBindingAdapter();
        }
    }
@Component(modlues = TestModule.class)
    public interface TestComponent extends android.databinding.DataBindingComponent {
    }
DataBindingUtil.setDefaultComponent(DaggerTestComponent.create());

还有一点需要注意的是,无自定义 DataBindingComponent 时框架生成的 ActivityMainBinding.java 相关代码:

android.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView2, firstNameUser);

自定义 DataBindingComponent 时生成的相关代码:

this.mBindingComponent.getMyBindingAdapters().setText(this.mboundView2, firstNameUser);

对比可以看出,当自定义 DataBindingAdapter 时,框架会自动调用自定义的 setText() 方法,而非默认的 TextViewBindingAdapter.setText()。

8. 最佳实践

至于 DataBinding 怎么写才是最好的,可以参考 Google 的这个开源项目:Android Architecture Blueprints [beta],使用 DataBinding、MVP、DataBinding+MVP 等多种方式实现同一个便笺应用,包括其中的单元测试的写法,都非常值得学习。

9. 一些建议

9.1 表达式尽量简单

xml 文件中不要出现业务逻辑,只出现简单的 UI 相关的表达式;

在 xml 绑定变量时尽量使用 ” 代替 “”,即使如此,转义依然不可避免: - ‘&’ –> ‘&amp;(用英文分号替换)’; - ‘<’ –> ‘&lt;(用英文分号替换)’; - ‘>’ -> ‘&gt;(用英文分号替换)’;

Android 11修改系统隐藏导航栏属性 安卓11默认隐藏data_mvvm_04

9.2 Clean 大法好

有时会遇到莫名其妙的检查错误,可尝试 clean 工程,或无视之;

9.3 关于代码结构

关于代码结构,使用 ViewModel(如 UserViewModel)+ Model(如 UserModel) + xml 的结构,对点击事件的处理以及 View 的状态数据(如评论列表是否展开,当前用户登录信息等)放在 ViewModel 中,而正常的数据放在 Model 中。

9.4 不要拒绝 findViewById

DataBinding 和 findViewById() 并不是互斥的,即使在使用 DataBinding 的工程中,我们仍然可以根据需要使用之,特别是在动态更新 ViewGroup 的情景中,有时不可避免的要是用该方法。

9.4 关于对 null 的处理

DataBinding 已经对 null 做了处理,我们无需再关心表达式 npe 的问题,例如 binding.setUser(viewModel); 中 viewModel 为 null 时,运行时不会出现 npe。

关于 @BindingMethod 这个注解是用来关联 SDK 中提供的控件的属性和方法的,这些属性的名称和其 setter 不匹配,需要 @BindingMethod 来“牵绳拉线”,以便自动更新属性的时候调用其对应的 setter。具体的使用方法可以参考 android.databinding.adapters.ImageViewBindingAdapter.java。

开发者一般用不到该注解。

10. 更多资料

也许本文不值得一看,但是下面这些资料则不然。

  • Data Binding Guide
  • Advanced Data Binding - Google I/O 2016
  • Android Architecture Blueprints [beta]
  • Data Binding – Write Apps Faster (Android Dev Summit 2015)
  • Marshmallow Brings Data Bindings to Android
  • LoginDemo4DataBinding
  • 精通 Android Data Binding
  • Android Data Binding从抵触到爱不释手
  • 安卓 Data Binding 使用方法总结(妹妹篇)