DataBinding(一)

73 阅读8分钟

DataBinding

一、使用入门

在app模板下开启

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

二 、布局和变量表达式

数据绑定布局文件略有不同,以根标记 layout 开头,后跟 data 元素和 view 根元素。

<?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: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>
  • data中声明使用的数据,有variable和type两个属性,variable是表达式中声明的变量名称,type中指明变量的类型(type中是类型class的全称)

三、绑定数据

绑定数据通过两部完成,1.绑定视图 2. 绑定数据

绑定类的名称为布局文件名称后面加Binding,如果上述文件名称为 activity_main.xml , 那么生成的对应类为 ActivityMainBinding

val binding: ActivityMainBinding = DataBindingUtil.setContentView(
                this, R.layout.activity_main)

        binding.user = User("Test", "User")

binding.user = User("test","user")给视图绑定数据

也可以通过ActivityMainBinding获取绑定数据

 val binding: ActivityMainBinding = ActivityMainBinding.inflate(getLayoutInflater())

ActivityMainBinding的类名来源于activity_main.xml,因此绑定的布局是确定的

在fragment或者listItem中,一般通过下面两种方式绑定视图

 val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)
    // or
 val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)

四、表达式语言

运算符和关键字:

可用运算符和关键字:

  • 算术运算符 + - / * %
  • 字符串连接运算符 +
  • 逻辑运算符 && ||
  • 二元运算符 & | ^
  • 一元运算符 + - ! ~
  • 移位运算符 >> >>> <<
  • 比较运算符 == > < >= <=(请注意,< 需要转义为 <
  • instanceof
  • 分组运算符 ()
  • 字面量运算符 - 字符、字符串、数字、null
  • 类型转换
  • 方法调用
  • 字段访问
  • 数组访问 []
  • 三元运算符 ?:
android:text="@{String.valueOf(index + 1)}"
    android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"
    android:transitionName='@{"image_" + id}'

不可用的运算

  • this
  • super
  • new
  • 显式泛型调用

Null 合并运算符

android:text="@{user.displayName ?? user.lastName}"

属性引用

android:text="@{user.lastName}"

避免出现 Null 指针异常

生成的数据绑定代码会自动检查有没有 null 值并避免出现 Null 指针异常。例如,在表达式 @{user.name} 中,如果 user 为 Null,则为 user.name 分配默认值 null。如果您引用 user.age,其中 age 的类型为 int,则数据绑定使用默认值 0

视图引用

表达式可以通过以下语法按 ID 引用布局中的其他视图:

android:text="@{exampleText.text}" // exampleText是一个控件,其真实id为 example_text

集合

为方便起见,可使用 [] 运算符访问常见集合,例如数组、列表、稀疏列表和映射。

<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]}"

要使 XML 不含语法错误,您必须转义 < 字符。例如:不要写成 List<String> 形式,而是必须写成 List&lt;String>

字符串字面量

使用字符串需要注字符串引号的问题

android:text='@{map["firstName"]}' 或者 android:text="@{map[`firstName`]}"

资源

android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}" // 与之前的方式相同

五、事件绑定

您可以使用以下机制处理事件:

  • 方法引用:在表达式中,您可以引用符合监听器方法签名的方法。当表达式求值结果为方法引用时,数据绑定会将方法引用和所有者对象封装到监听器中,并在目标视图上设置该监听器。如果表达式的求值结果为 null,则数据绑定不会创建监听器,而是设置 null 监听器。

     class MyHandlers {
            fun onClickFriend(view: View) { ... }
        }
    
       <?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}"/> // onClickFriend的方法参数必须是view,而且只能有一个参数
           </LinearLayout>
        </layout>
    

    方法引用表达式中的方法签名必须与监听器对象中的方法签名完全一致。

  • 监听器绑定:这些是在事件发生时进行求值的 lambda 表达式。数据绑定始终会创建一个要在视图上设置的监听器。事件被分派后,监听器会对 lambda 表达式进行求值。

    class Presenter {
           fun onSaveClick(task: Task){}
       }
    
    <?xml version="1.0" encoding="utf-8"?>
        <layout xmlns:android="http://schemas.android.com/apk/res/android">
            <data>
                <variable name="task" type="com.android.example.Task" />
                <variable name="presenter" type="com.android.example.Presenter" />
            </data>
            <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent">
                <Button android:layout_width="wrap_content" android:layout_height="wrap_content"
                android:onClick="@{() -> presenter.onSaveClick(task)}" /> // 表达式中的方法调用需要与定义的方法一致
            </LinearLayout>
        </layout>
    

在监听器绑定中,只有您的返回值必须与监听器的预期返回值相匹配(预期返回值无效除外)

​ 在上面的示例中,我们尚未定义传递给 onClick(View)view 参数。监听器绑定提供两个监听器参数选项:您可以忽略方法的所有参数,也可以命名所有 参数。如果您想命名参数,则可以在表达式中使用这些参数。例如,上面的表达式可以写成如下形式:

    android:onClick="@{(view) -> presenter.onSaveClick(task)}" // 监听器绑定可以忽略参数,也可以用这些参数;

六 、导入、变量和包含

导入

类似与java的import,导入到data中后,就可以在表达式中使用

    // 导入View类
    <data>
        <import type="android.view.View"/>
    </data>

    <TextView
       android:text="@{user.lastName}"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/> // 表达式中使用View的类变量

    <import type="android.view.View"/>
    <import type="com.example.real.estate.View"
            alias="Vista"/> // 导入的类型有冲突的时候,可以创建alias别名

变量

您可以在 data 元素中使用多个 variable 元素。每个 variable 元素都描述了一个可以在布局上设置、并将在布局文件中的绑定表达式中使用的属性

<data>
        <import type="android.graphics.drawable.Drawable"/>
        <variable name="user" type="com.example.User"/>
        <variable name="image" type="Drawable"/>
        <variable name="note" type="String"/>
    </data>

包含

通过使用应用命名空间和特性中的变量名称,变量可以从包含的布局传递到被包含布局的绑定。

<?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}"/>  // 1
           <include layout="@layout/contact"
               bind:user="@{user}"/> // 2
       </LinearLayout>
    </layout>

数据绑定不支持 include 作为 merge 元素的直接子元素。

<?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>
       <merge><!-- Doesn't work --> 
           <include layout="@layout/name"
               bind:user="@{user}"/>
           <include layout="@layout/contact"
               bind:user="@{user}"/>
       </merge>
    </layout>

七、使用可观察的数据对象

观察性是指一个对象将其数据变化告知其他对象的能力。通过数据绑定库,您可以让对象、字段或集合变为可观察的数据,当其中一个可观察数据对象绑定到界面并且该数据对象的属性发生更改时,界面会自动更新。

可观察字段

简单的可观察数据

可观察字段是具有单个字段的自包含可观察对象。原语版本避免在访问操作期间封箱和开箱。

可观测集合:

可观察对象

实现 Observable 接口的类允许注册监听器

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。

八、生成绑定类

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

       val binding: MyLayoutBinding = MyLayoutBinding.inflate(layoutInflater) //1

        setContentView(binding.root)
    }

    val binding: MyLayoutBinding = MyLayoutBinding.inflate(getLayoutInflater(), viewGroup, false) //2

    val viewRoot = LayoutInflater.from(this).inflate(layoutId, parent, attachToParent) 
    val binding: ViewDataBinding? = DataBindingUtil.bind(viewRoot) // 3

    //如果您要在 Fragment、ListView 或 RecyclerView 适配器中使用数据绑定项
    val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)
    // or
    val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)

九、绑定适配器

绑定适配器负责发出相应的框架调用来设置值。

设置特性值

只要绑定值发生更改,生成的绑定类就必须使用绑定表达式在视图上调用 setter 方法。您可以允许数据绑定库自动确定方法、显式声明方法或提供选择方法的自定义逻辑。

DataBinding自动选择方法:

比如属性为android:text="@{user.name}",库会查找接受user.getName()所返回类型的setText(arg)方法,作为设置特征值的方法,也就是说,如果我给user设置值,那么,试图控件会查找setText(arg)方法来给自己的text属性赋值,arg类型是user.getName类型,比如说,user.getName 返回int,那么布局会查找setText(int arg)方法来作为android:text="@{user.name}"的响应,即使不存在具有给定名称的特性,数据绑定也会起作用。

比如:

<android.support.v4.widget.DrawerLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:scrimColor="@{@color/scrim}"
        app:drawerListener="@{fragment.drawerListener}">

那么布局会将 setScrimColor(int)setDrawerListener(DrawerListener)方法作为scrimColor和drawerListener熟悉的赋值响应

指定自定义方法名称

一些属性具有名称不符的 setter 方法。在这些情况下,某个特性可能会使用 BindingMethods 注释与 setter 相关联,注释与类一起使用

 @BindingMethods(value = [
        BindingMethod(
            type = android.widget.ImageView::class,
            attribute = "android:tint",
            method = "setImageTintList")])
// android:tint 属性与 setImageTintList(ColorStateList) 方法相关联,而不与 setTint() 方法相关联
提供自定义逻辑

自定义绑定逻辑,比如app:imageUrl = "@{user.logo}",系统中没有imageUrl熟悉,采用自定义逻辑则如下操作:

    @BindingAdapter("app:imageUrl") 
    public static void setImageUrl(ImageView view, String path) {
      Glide.with(imageView.getContext())
                .load(path)
                .into(imageView);
    }
    // 第一个参数类型ImageView表示,这个熟悉跟ImageView类型的视图关联,第二个属性用于确定表达式接受的类型

如此,系统将采用setImageUrl这个方法来作为app:imageUrl = "@{user.logo}"这个视图绑定的赋值响应方法

 @BindingAdapter("app:imageUrl") 
    public static void setImageUrl(ImageView view,String old, String url) {
      if(old == url) return  
      Glide.with(imageView.getContext())
                .load(url)
                .into(imageView);
    }
//绑定适配器方法可以选择性在处理程序中使用旧值。同时获取旧值和新值的方法应该先为属性声明所有旧值

十、数据双向绑定

比如一个登陆页面:

<androidx.appcompat.widget.AppCompatEditText
            android:layout_width="match_parent"
            android:layout_height="70dp"
            android:text="@{user.phone}" /> 

这种情况,user的phone熟悉的值会展示到控件中,但是实际场景往往需要你输入一个phone,然后把phone的值设置给user,这种登陆的时候就可以通过user来获取页面上输入的phone值了,这种情况就需要双向绑定:

<androidx.appcompat.widget.AppCompatEditText
            android:layout_width="match_parent"
            android:layout_height="70dp"
            android:text="@={user.phone}" />   // @ = {表达式}

现在编辑AppCompatEditText里面的内容,会同步到user.phone的属性上