Android Data-Binding简记

版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/dahaohan/article/details/51788344

What‘s Data-Binding?

看过我之前转发的博文Android App的设计架构:MVC,MVP,MVVM经验谈
可以了解到移动端App开发架构从传统MVC–>MVP–>MVVM的一些进展和演化,而目前发展成的MVVM架构则需要使用Data-Binding机制来完成View和ViewModel之间的通信。
2015年google I/0开发者大会发布的Data-binding库,使得开发者可以更加简洁优雅的编写代码实现复杂的业务逻辑,而不必去关注数据变更后UI View的更新问题。View的变化会直接影响ViewModel,ViewModel的变化或者内容也会直接体现在View上,这就是Data Binding Library默默帮您完成的工作。
Android官方Data Binding介绍:https://developer.android.com/topic/libraries/data-binding/index.html

Data Binding Requirements

The Data Binding Library offers both flexibility and broad compatibility — it’s a support library, so you can use it with all Android platform versions back to Android 2.1 (API level 7+).
To use data binding, Android Plugin for Gradle 1.5.0-alpha1 or higher is required.
Also, make sure you are using a compatible version of Android Studio. Android Studio 1.3 and later provides support for data binding.

Data binding 是一个类似support-v4/v7的支持库,API 7+都可以使用;
Android Studio1.3+
Android Gradle插件 1.5.0 +

How To Use Data Binding?

要使用Data binding功能需要在你app module 的build.gradle中开启。

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

Data binding Layout

使用Data binding核心是将viewModel数据嵌入到布局layout xml文件,故而layout xml文件与之前的纯view布局文件的构成略有不同。

//以下为官方给出的最简单的一个示例:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="user"
            type="test.example.com.databindingtest.User">
        </variable>
    </data>

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:id="@+id/firstname_text"
            android:layout_width="wrap_content"
            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>

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

Data binding的布局文件的根节点为layout标签,由一个数据data标签以及一个根视图标签构成。
data标签声明了该View使用的数据模型viewModel为变量名为user的User类对象,然后在Root View标签下通过
@{“变量名” + “.” + ”属性名/函数名“}来给指定的view设置值。

PS:

//对于User类的另一种写法
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;
   }
}

关于Data binding框架识别语句 @{user.firstName} 的一个机制是对于第一种写法访问User类的 firstName属性,而第二种写法则是访问User类的getFirstName()方法,或者也有可能访问firstName()方法如果存在的话。

Binding Data

编写完成data binding layout之后,类似IDE自动编译资源文件产生资源ID R文件,默认将以layout xml文件的名字为基础生成一个继承自ViewDataBinding的类,此例xml文件为activity_main.xml故而生成ActivityMainBinding.java
生成的ViewDataBinding类默认命名规则为,布局xml文件名去掉”_”以驼峰式写法+“Binding”,当然也支持自定义命名,后续提及

对于Android Studio IDE可以在
app/build/intermediates/classes/debug/+”对应包目录”+databinding文件夹下查看该生成的ViewDataBinding类
这里写图片描述

PS:注意这里生成的ActivityMainBinding内的以下几个属性和函数

public class ActivityMainBinding extends android.databinding.ViewDataBinding  {
    ..........
    .....
    // views
    //在layout xml内定义了id的 android:id="@+id/firstname_text"
    //Viewdatabinding将会以驼峰命名生成对应:
    //public final android.widget.TextView firstnameText;
    //通过获得的binding类以及view的id名称可以直接获取该view的引用,而不需要再findViewById
    public final android.widget.TextView firstnameText;

    //未设置id的view则只会定义为private,外部将不可访问
    private final android.widget.LinearLayout mboundView0;
    private final android.widget.TextView mboundView2;

    //layout内声明的变量,对应成员变量以及默认以驼峰命名生成set/get函数setUser
    //用于给dataBinding设置数据viewModel
    // variables
    private test.example.com.databindingtest.User mUser;
    ........
    public void setUser(test.example.com.databindingtest.User user) {
        this.mUser = user;
        synchronized(this) {
            mDirtyFlags |= 0x1L;
        }
        notifyPropertyChanged(BR.user);
        super.requestRebind();
    }
    public test.example.com.databindingtest.User getUser() {
        return mUser;
    }
    ........
    ..................
  }

略微查看默认生成的ActivityMainBinding类,可以看出binding类就是管理并维护View与View Model的关系的核心。包括给将user数据对应值赋值给对应的view视图;当user内值变化时更新视图;视图交互事件调用viewmodel的业务逻辑等。

ViewDataBinding相当于一个联系对应layout中View与对应ViewModel(data标签下的数据)的框架,我们使用时需要将对应的View和ViewModel实例对象传递给该ViewDataBinding框架,Data Binding库已经提供了多种方法来给实现:

@Override
protected void onCreate(Bundle savedInstanceState) {

   //获取ViewDataBinding实例对象的同时已经通过传递对应的View对象或者layout文件参数,
   //将对应的View实例对象传递给该ViewDataBinding框架

   //Activity最常用的取代之前的setContentView方法
   MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
   //可直接通过id名称访问对应View变量
   dataBinding.firstnameText.setText("hello");

   //使用默认生成的ActivityMainBinding直接bind对应View
   dataBinding = ActivityMainBinding.bind(viewRoot);
   //或者如下
   dataBinding = ActivityMainBinding.inflate(getLayoutInflater());

   ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
    parent, attachToParent);

   //给DataBinding框架设置viewModel数据源User
   User user = new User("Test", "User");
   //set方法根据layout中声明的数据类型自动生成
   binding.setUser(user);
}

上述步骤完成运行即可发现,user各属性数据自动绑定到了各自的View上,但是这只体现了dataBinding单方面从ViewModel到View的数据绑定;如何实现user数据更新然后DataBinding自动更新UiView? View的交互事件如何绑定到ViewModel的业务逻辑?

Data Binding View Event Handling

类似data binding提供的View的数据绑定,在layout的view标签内同样允许使用表达式直接引用相应的函数处理分发的事件。使用函数引用的View 属性由对应的事件listener的函数方法决定:

    //View一般有View.OnLongClickListener/ View.OnClickListener
    //对应的两个方法为onLongClick/onClick故而data binding在view标签下有如下属性:
    android:onClick="@{user.onLastNameClick}"
    android:onLongClick="@{user.onLastNameLongClick}"

若在User类加入以下函数,则在View内完整引用监听click事件写法如下:

public class User {
    ....
    ........
    public void onLastNameClick(View v){
        Log.d("Test"," onLastNameClick ");
    }
    public boolean onLastNameLongClick(View v){
        Log.d("Test"," onLastNameLongClick ");
        return false;
    }
    ....
    .......
}

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

进行编译后运行即可发现点击交互事件可以顺利触发和处理。

Android官方对于View标签引用的事件处理函数的要求说明:

In your expressions, you can reference methods that conform to the signature of the listener method. When an expression evaluates to a method reference, Data Binding wraps the method reference and owner object in a listener, and sets that listener on the target view. If the expression evaluates to null, Data Binding does not create a listener and sets a null listener instead.

官方说明View处理事件引用的方法的签名要与对应的clickListener的方法签名相符,而方法的签名侧重的是方法名和方法参数的顺序、类型、个数;这里测试之后其实需要的是与对应的clickListener的方法的参数以及返回值一致。

编译期间将对View#onClick attribute的表达式引用的方法进行合法性检查,若是方法参数/返回值对应不上,则会出现编译错误:

Error:(26, 36) Listener class android.view.View.OnLongClickListener with method onLongClick did not match signature of any method user.onLastNameLongClick 

若编译正确,实质其实还是将该方法包装进一个对应的listener然后给view设置对应监听接口。

Data Binding listening data/properties changes

Data binding真正的好处提现在data(user)数据变化时,能够自动更新对应的UI显示,如何使用这个核心功能呢?

Observable 对象

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

   //Bindable注解告诉data binding框架需要侦听该值的变化
   //编译期间生成的类似R文件的一个BR文件将会有该属性的一个public资源Id
   @Bindable
   public String getFirstName() {
       return this.firstName;
   }
   @Bindable
   public String getLastName() {
       return this.lastName;
   }
   public void setFirstName(String firstName) {
       this.firstName = firstName;
       //通过对应属性的资源ID告诉data binding框架该属性发生变化需要更新ui什么的
       notifyPropertyChanged(BR.firstName);
   }
   public void setLastName(String lastName) {
       this.lastName = lastName;
       notifyPropertyChanged(BR.lastName);
   }
}

限定了修改属性数据的方法在对应的set方法内,所以在set方法更新属性后通知data binding框架属性的更新即可。

ObservableFields

其实与上述的BaseObservable对象是类似的原理,只是将整个类的范围缩小到个别需要侦听的属性上,提供了ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, and ObservableParcelable.

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

user.firstName.set("Google");
int age = user.age.get();

事实上每一个ObservableFields都继承自BaseObservable包含了单独一个属性值,内部默认封装了set/get方法,将上述BaseObservable的方法封装好了,也是在set方法之后通知data-binding框架做出一些更新操作。

Observable Collections

对于一些需要使用到list/map等集合数据类型来说有: ObservableArrayMap,ObservableArrayList等

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

<data>
    <import type="android.databinding.ObservableMap"/>
    <variable name="user" type="ObservableMap&lt;String, Object>"/>
</data><TextView
   android:text='@{user["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
<TextView
   android:text='@{String.valueOf(1 + (Integer)user["age"])}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

PS: Android Studio IDE目前并不能很完美的支持到data标签的一些变量的import或者是类型声明;但是并不会影响编译运行。
这里写图片描述

ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);

<data>
    <import type="android.databinding.ObservableList"/>
    <variable name="user" type="ObservableList&lt;Object>"/>
</data><TextView
   android:text="@{user[0]}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
<TextView
   android:text="@{String.valueOf(1 + (Integer)user[2])}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

Some Details

关于data标签支持import类似java的import,以及其对于一些类似map/list/array等集合类型的数据的语法简要介绍:

//下面的 &lt; 为符号'<'字符实体
<data>
    <import type="android.util.SparseArray"/>
    <import type="java.util.Map"/>
    <import type="java.util.List"/>
    <variable name="list" type="List&lt;String>"/>
    <variable name="sparse" type="SparseArray&lt;String>"/>
    <variable name="map" type="Map&lt;String, String>"/>
    <variable name="index" type="int"/>
    <variable name="key" type="String"/>
</data>

…
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"

//集合类型数据都是类似数组使用的'[]'访问特定的数据,对于数组[]内是对应的下标,对于map则是对应的key值

PS: Map的key为一个String时,可能遇到引号内要使用引号包括字符串的冲突,这时使用:

//单引号在外包括,内部使用双引号标示字符串
android:text='@{map["firstName"]}'
//或者外部使用双引号,内部用back quote反引号标示字符串(反引号即'~'按键)
android:text="@{map[`firstName`}"

//或者使用&quot;即双引号的java字符实体来替代双引号
android:text="@{map[&quot;firstName&quot;]}"

About More Details

关于data binding的细节知识推荐阅读:
官方介绍文档:https://developer.android.com/topic/libraries/data-binding/index.html
比较全面的官方文档的翻译档:http://www.jianshu.com/p/b1df61a4df77
结合实例的介绍:https://github.com/LyndonChin/MasteringAndroidDataBinding

猜你喜欢

转载自blog.csdn.net/dahaohan/article/details/51788344