Android Jetpack组件之DataBinding详解

5,206 阅读13分钟

原文首发于微信公众号:躬行之(jzman-blog)

前面总结了 ViewModel、LiveData 及 Lifecycle 架构组件的使用,可先阅读下面文章详细了解:

本篇主要侧重 dataBinding 的基本使用,主要内容如下:

  1. dataBinding支持
  2. 布局文件配置
  3. 绑定数据
  4. 特殊表达式
  5. 事件绑定
  6. 自定义绑定类
  7. Others

dataBinding支持

使用 dataBinding 需要在 app module 下面的 build.gradle 文件中进行配置,具体如下:

//设置支持dataBinding   
dataBinding {
    enabled = true
}

布局文件配置

Data Binding Library 会自动生成将布局中的视图和数据对象绑定所需要的类,Data Binding Library 的布局文件中以 layout 标签为根标签,然后是具体的数据元素和视图元素,此视图元素是绑定布局文件的位置,布局文件参考如下:

<?xml version="1.0" encoding="utf-8"?>
<!--dataBinding必须以layout作为根标签-->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <!--数据对象-->
    <data>
        <variable name="user" type="com.manu.databindsample.data.User"/>
    </data>
    <!--视图元素-->
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!--dataBinding中具体属性值的配置在"@{}"中进行配置-->
        <TextView android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.name,default=姓名}"/>

    </LinearLayout>
</layout>

数据实体

在 "@{user.name}" 中的 name 属性最终映射调用数据对象的 getter 方法,也就是 getter 方法,当然,如果数据对象中有对应的 name 方法,在没有与之对应 getter 方法的时候会调用与之同名的方法,如果两者都存在,则会优先调用与之对应的 getter 方法,参考如下:

/**
 * 数据实体
 * Powered by jzman.
 * Created on 2018/11/28 0028.
 */

public class User {
    private String name;

    public User() {
    }

    public User(String name) {
        this.name = name;
    }

    //两者存在优先调用
    public String getName() {
        return name;
    }
    //getter方法不存在会调用
    public String name() {
        return "name";
    }

    //...
}

绑定数据

dataBinding 会为内个布局文件生成对应的绑定类,默认情况下,类的名称基于布局文件的名称,如布局文件名为 activity_main,则该布局文件对应的绑定类是 ActivityMainBinding,该类包含数据对象到布局文件的所有绑定,那么如何绑定数据和视图呢,在 Activty 中绑定方式如下:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //生成绑定类
        ActivityMainBinding binding = DataBindingUtil.setContentView(this,R.layout.activity_main);
        //绑定视图与数据
        User user = new User("张三");
        binding.setUser(user);
    }
}

在 Fragment 中绑定方式如下:

//inflate方法
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState)
 
{
    FragmentOneBinding oneBinding = DataBindingUtil.inflate(inflater,R.layout.fragment_one,container,false);
    User user = new User("小明");
    oneBinding.setUser(user);
    return oneBinding.getRoot();
}
//bind方法
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
    View view = inflater.inflate(R.layout.fragment_one,container,false)
;
    FragmentOneBinding oneBinding = FragmentOneBinding.bind(view);
    User user = new User("小明");
    oneBinding.setUser(user);
    return view;
}

其他布局的绑定方式基本是都是使用某个生成的绑定类的 inflate 方法和 bind 方法就可以完成。

特殊表达式

  • 三目运算符简化
//完整写法
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
//简写
android:text="@{user.displayName ?? user.lastName}"
  • 空指针异常处理

生成的绑定类会自动检查 null 值以避免 NullPointerException,在表达式 @ {user.name} 中,如果 user 为 null,则为user.name 分配其默认值 null。 如果引用 user.age,其中 age 的类型为 int,则数据绑定使用默认值0。

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

    <data>

        <import type="java.util.Map" />

        <import type="java.util.List" />

        <!--Map-->
        <variable
            name="map"
            type="Map&lt;String,String>" />


        <variable
            name="key"
            type="String" />


        <!--List-->
        <variable
            name="list"
            type="List&lt;String>" />


        <variable
            name="index"
            type="int" />

    </data>
    <!--注意Map和List取值方式-->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">


        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{map.key}" />


        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{list[0]}" />


    </LinearLayout>
</layout>

对于 Map 类型的数据可以在表达式 @{} 中使用 map.key 来获取 Map 集合中 key 对应的 value 值,List 类型的数据直接使用索引来取值,此外在 variable 标签中使用到的 < 要进行转义,及使用 < 来代替 <,否则报错如下:

Error: 与元素类型 "variable" 相关联的 "type" 属性值不能包含 '<' 字符。
  • @{} 表达式中使用字符串

如何在 @{} 表达式中使用字符串而不是字符串变量呢,有两种方式,具体如下:

<!--使用单引号-->
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text='@{map["key"]}' />

<!--使用后引号-->
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{map[`key`]}" />

<!--在@{}中可以使用字符串资源-->
<TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{@string/app_name}"/>

事件绑定

使用 databinding 时可以采用方法引用或监听绑定的方式来设置事件监听,这两者的区别是前者的事件监听器是在数据绑定时创建的,而后者是在事件触发时绑定。

  • 方法引用

事件可以直接绑定在事件处理方法上,与普通的 android:onClick 属性相比较,这种配置方式会在编译时进行相关处理,如果该方法不存在或该方法签名不正确,则会收到编译时错误。首先创建一个事件处理方法如下:

/**
 * Powered by jzman.
 * Created on 2018/11/30 0030.
 */

public class MyHandler {
    /**
     * @param view
     */

    public void onClickEvent(View view){
        Toast.makeText(view.getContext(), "click me", Toast.LENGTH_SHORT).show();
    }
}

然后,在对应的布局文件中配置具体的 onClick 等事件,这里以 onClick 事件为例,具体如下:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="handler"
            type="com.manu.databindsample.handler.MyHandler"/>

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <!--第一种方式-->
        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="click me"
            android:onClick="@{handler::onClickEvent}"/>

        <!--第二种方式-->
        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="click me"
            android:onClick="@{handler.onClickEvent}"/>

    </LinearLayout>
</layout>

最后,在对应的 Activity 中设置数据对象 handler 即可,具体参考如下:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ActivityEventHandlerBinding binding = DataBindingUtil.setContentView(this,R.layout.activity_event_handler);
    binding.setHandler(new MyHandler());
}

这样通过方法引用事件就成功绑定了。

  • 监听绑定

这种方式是在事件发生时创建事件监听器,相较方法引用可以传递自定义参数在事件回调中,首先,创建一个事件回调方法如下:

/**
 * 监听绑定
 * Powered by jzman.
 * Created on 2018/12/3 0003.
 */

public class MyPresenter {
    private Context mContext;

    public MyPresenter(Context mContext) {
        this.mContext = mContext;
    }

    public void onClickEvent(User user) {
        Toast.makeText(mContext, user.getName(), Toast.LENGTH_SHORT).show();
    }
}

然后,在对应的布局文件中配置具体的 onClick 等事件,这里以 onClick 事件为例,具体如下:

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

    <data>
        <variable
            name="user"
            type="com.manu.databindsample.data.User" />


        <variable
            name="presenter"
            type="com.manu.databindsample.handler.MyPresenter" />

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">


        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{() -> presenter.onClickEvent(user)}"
            android:text="click me 3" />

    </LinearLayout>
</layout>

最后,在对应的 Activity 中设置数据对象 presenter 即可,具体参考如下:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ActivityEventHandlerBinding binding = DataBindingUtil.setContentView(this,R.layout.activity_event_handler);
    binding.setUser(new User("android"));
    binding.setPresenter(new MyPresenter(this));
}

这样通过事件监听事件就成功绑定了,在上面 xml 中调用事件方法时,可以在配置当前 View,具体如下:

<Button
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onClick="@{(view) -> presenter.onClickEvent(user)}"
    android:text="click me 3" />

则对应的事件回调方法如下:

public class MyPresenter {
    public void onClickEvent(View view, User user){}
}

此外,也可以在事件绑定时使用三目运算符,此时可将 void 作为操作符使用,使用方式参考如下:

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

自定义绑定类

从前面可知,默认状态下绑定类名称是由布局文件名称决定,那么如何自定义绑定类呢,在布局文件 data 标签上使用 class 属性指定自定义的绑定类名即可,当然也可以在自定义类名前面添加完成的包路径,参考如下:

<!--自定义绑定类-->
<data class="com.manu.test.CustomBinding">
    <variable name="user" type="com.manu.databindsample.data.User"/>
</data>

Others

在 databinding 中使用 import 关键字导入相关的类,java.lang.* 下面的相关类默认自动导入,如果有相同名字的 View 可以使用使用 alias 来区分,参考如下:

<import type="android.view.View"/>
<import type="com.manu.View"
        alias="MView"/>

使用 variable 关键字定义要在 xml 布局中使用的变量,如果使用了 include 布局,则要使用 bind 绑定 include 包含的布局与主布局使用同样的变量,创建一个 include 包含的布局 test_layout.xml 文件,具体如下:

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

    <data>
        <variable
            name="userTest"
            type="com.manu.databindsample.data.User" />

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">


        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{`this is include content...`+userTest.getName(),default=user}" />

    </LinearLayout>
</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.manu.databindsample.data.User" />

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

         <!--bind绑定变量-->
        <include
            layout="@layout/test_layout"
            bind:userTest="@{user}" />

    </LinearLayout>
</layout>

这样通过 bind 就绑定了两个布局中使用到的 User 类型的变量,使得两个布局中使用的变量是同一个变量,此外,databinding 不支持 merge 标签,下篇继续 Binding adapters 的介绍。