Android DataBinding学习(一)

4,218 阅读14分钟

本篇学习笔记基于官方文档中布局和表达式部分进行学习。点击这里查看原文

概述

 数据绑定库是一种支持库,借助该库,可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。

 之前,如果我们需要更新布局,我们可能是这样做的:

val user: User = User("name",20)
findViewById<TextView>(R.id.tv_content1).apply {
    text = user.name
}

 但是在使用数据绑定库后,可以在xml文件中直接设置View的一些属性,具体设置的过程如下:

  1. xml文件中定义的data标签下定义变量名称
<data>
    <variable
        name="title"
        type="String"
        />
    <variable
        name="user"
        type="com.project.databinding_moudle.data.User"
        />
</data>
  1. 在需要设置属性的地方直接获取user中的值:
<TextView
    android:id="@+id/tv_cntent2"
    style="@style/match_width"
    app:layout_constraintTop_toBottomOf="@id/tv_content1"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    android:padding="10dp"
    android:textColor="@android:color/black"
    android:gravity="center"
    android:text="@{user.name}"
    />

 注意查看上面TextView中对text设置值的时候使用了@{}这样的语法。

  1. xml所在的页面文件中设置user的值:
val user: User = User("name",20)
mBinding.user = user

 上面的mBindingDataBinding帮我们自动生成的文件,它是根据布局文件的名称生成的,和之前的ViewBinding是相似的。

 经过上面3步之后,运行程序,就会看到idtv_content2TextView已经设置上了name这个值。

ViewBinding的比较

  • 相似点是不管是ViewBinding还是DataBinding都会帮助我们根据layout文件生成一个相关类,这个类中包含我们设置了idView的引用,我们可以直接拿到这个引用去给View设置一些属性等。
  • 不同点是ViewBinding的目的是为了取代我们日常开发中比较繁琐的findViewById(),通过ViewBinding我们可以直接拿到View设置属性
  • DataBinding同样可以做到ViewBinding能够做到的事情,除此之外,通过DataBinding我们可以在layout布局文件中直接设置View的属性,进一步简化了Activity/Fragment中的操作。
  • 另外一个不同点在于ViewBinding只需要在gradle配置文件中开启即可为所有的layout文件自动生成ViewBinding类,而DataBinding除了需要在gradle中开启之外,只有在layout文件中以<layout>作为根标签的layout文件才会生成对应的DataBinding类。

 所以,如果主要是为了替换findViewById()的使用,那么ViewBinding是更加合适的解决方案。

开始使用

环境要求:

  1. 数据绑定库是一个支持库,兼容性广,可以运行在Android4.0(API 14)或更高级别的设备上
  2. Gradle1.5.0及更高版本都支持数据绑定。

配置过程:

  1. 在要使用数据绑定的模块的build.gradle文件中开启dataBinding,如下:
android {
        ...
        dataBinding {
            enabled = true
        }
    }
  1. 在需要使用数据绑定的layout文件中使用<layout>根标签:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    >
    
    ......
    
<layout>

 注意1:不能对layout标签使用layout_width或者layout_height属性,否则会报错,报错信息如下:

已经为元素 "androidx.constraintlayout.widget.ConstraintLayout" 指定属性 "android:layout_width"

 这里由于我的layout标签下子标签就是ConstraintLayout,所以会出现这样的提示,如果把这个ConstraintLayout换成LinearLayout,则提示如下:

已经为元素 "LinearLayout" 指定属性 "android:layout_width"

 注意2:不能在布局文件中只使用<layout>标签或者只有<layout>标签和<data>标签,否则也会出错:

android.databinding.tool.processing.ScopedException: [databinding] {"msg":"Only one layout element with 1 view child is allowed.

 一共就只需要这两步即可。需要注意的是:根据文档,布局表达式应保持精简,因为它们无法进行单元测试,并且IDE对它的支持也比较有限,为了简化布局表达式,可以使用自定义绑定适配器。

绑定数据

 系统会为每一个布局文件生成一个绑定类,默认情况下,类名称基于布局文件的名称,它会转换为Pascal大小写形式并再最后加上Binding结尾,例如布局文件为activity_main.xml,那么生成的对应类为ActivityMainBinding。此类包含从布局属性(如 user变量)到布局视图的所有绑定,并且知道如何为绑定表达式指定值。建议的绑定创建方法是在扩充布局时创建,如:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val binding: ActivityDataBindingTest2Binding = DataBindingUtil.setContentView(this,R.layout.activity_data_binding_test2)

    binding.title = "title"
}

 同时,也可以使用下面的方式获取视图:

val binding: ActivityDataBindingTest2Binding = ActivityDataBindingTest2Binding.inflate(layoutInflater)

 如果不是在Activity中使用DataBinding,那么也可以使用如下两种方式获取绑定布局:

val binding: ActivityDataBindingTest2Binding = ActivityDataBindingTest2Binding.inflate(layoutInflater,null,false)

或者:

val binding: ActivityDataBindingTest2Binding = DataBindingUtil.inflate(layoutInflater,R.layout.activity_data_binding_test2,null,false)

 使用以上四种方式均可以实现数据绑定。

表达式语言

 其实就是在布局文件中可以使用的表达式,包括:

  • 算术运算符 + - * / %
  • 字符串连接运算符 +
  • 逻辑运算符 && || (注意&需要使用转义字符&amp;)
  • 二元运算符 & | ^
  • 一元运算符 + - ! ~
  • 移位运算符 >> >>> <<
  • 比较运算符 == > < >= <= (注意<需要使用转义字符 &lt;)
  • instanceof
  • 分组运算符 ()
  • 字面量运算符 字符,字符串,数字,null
  • 类型转换
  • 方法调用
  • 字段访问
  • 数组访问 []
  • 三元运算符 ?:

 例如:

<TextView
    style="@style/match_width"
    app:layout_constraintTop_toBottomOf="@id/tv_cntent2"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    android:padding="10dp"
    android:visibility="@{user.age > 10 ? View.GONE : View.VISIBLE}"
    android:text="@{String.valueOf(user.age)}"
    android:transitionName='@{"tv_"+user.name}'
    />

缺少的运算

 不支持在布局文件中使用如下的运算符:

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

Null合并运算符 ??

 如果左边的运算数不是null,那么Null合并运算符取左边的运算数,如果左边的运算数为null,则选择右边的运算数,如:

android:text="@{user.name ?? user.phone}"

 这样的写法在功效上等同于:

android:text="@{user.name != null ? user.name : user.phone}"

属性引用

 表达式可以使用以下格式在类中引用属性,这对于字段,getter和ObservableField对象都一样

android:text="@{user.name,default=`12345`}"

避免出现空指针异常

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

集合

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

<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"
    />
<import type="java.util.List" />
<import type="android.util.SparseArray" />
<import type="java.util.Map" />

<TextView
    android:id="@+id/tv_content5"
    style="@style/match_width"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toBottomOf="@id/tv_content4"
    android:padding="10dp"
    android:gravity="center"
    android:text='@{"List中第"+index+"个元素为:"+list[index]}'
    />

<TextView
    android:id="@+id/tv_content6"
    style="@style/match_width"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toBottomOf="@id/tv_content5"
    android:padding="10dp"
    android:gravity="center"
    android:text='@{"数组中第"+index+"个元素为:"+sparse[index]}'
    />

<TextView
    android:id="@+id/tv_content7"
    style="@style/match_width"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toBottomOf="@id/tv_content6"
    android:padding="10dp"
    android:gravity="center"
    android:text='@{"map中的"+key+"对应的value为:"+map[key]}'
    />

 在Activity中分别创建List数组Map,然后设置到布局文件中:

val list = mutableListOf<String>()
list.add("哈哈哈")
list.add("嘻嘻嘻")
list.add("哼哼哼")

val array = SparseArray<String>()
array.put(0,"呵呵呵")
array.put(1,"呸呸呸")
array.put(2,"滚滚滚")

//创建Map
val map = mutableMapOf<String,String>()
map.set("哈","hahah")
map["哼"] = "哼哼哼"
map["滚"] = "滚滚滚"

mBinding.list = list
mBinding.sparse = array
mBinding.map = map

mBinding.index = 1
mBinding.key = "滚"

 运行程序之后,会显示如下内容:

DataBinding中使用集合
DataBinding中使用集合

 注意:下面的这个方法我在操作的时候没有效果,map没有引用到对应的值,但是这是官方文档的教程,暂时不清楚我哪里做错了。对于Map类型的元素,除了可以使用map[key]这种方式之外,还可以使用{map.key}这种方式来引用数据,如:

<TextView
    android:id="@+id/tv_content7"
    style="@style/match_width"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toBottomOf="@id/tv_content6"
    android:padding="10dp"
    android:gravity="center"
    android:text='@{"map中的"+key+"对应的value为:"+map.key}'
    />

 注意:根据官方文档我使用上面的做法没有效果,map并没有引用到对用的值,而是出现了null,暂时不清楚怎么回事

字符串字面量

 这个在上面已经演示了,就是使用单引号括住特性值,然后就可以在单引号中使用双引号了:

android:text='@{"map中的"+key+"对应的value为:"+map[key]}'

 同时,也可以使用双引号括住特性值,然后在里面使用反单引号:

android:text="@{`map中的`+key+`对应的value为:`+map[key]}"

资源

 可以使用以下语法访问表达式中的资源:

android:padding="@{largePadding ? @dimen/large_padding : @dimen/small_padding}"

 也可以使用格式化字符串:

//在strings.xml中定义一个格式化字符串
<string name="name_and_phone">姓名:%1$1s 手机号:%2$1s</string>

//在TextView中使用
android:text="@{@string/name_and_phone(user.name,user.phone)}"

//在Activity中指定User
val user: User = User("name",20,"1232142134")
mBinding.user = user

 最后运行的效果如下:

DataBinding中使用格式化字符串
DataBinding中使用格式化字符串

事件处理

 通过视图绑定,可以编写从视图分派的表达式处理事件(例如:onClick()方法),事件特性的名称由监听器方法的名称确定,但是有一些例外情况。也就说,View.onClickListener有一个onClick()方法,所以该事件的特性为android:onClick

 例如:

//首先在<data>标签中定义要处理点击事件的方法
<variable
    name="doClick"
    type="android.view.View.OnClickListener"
    />
    
//然后在需要监听的View上设置android:onClick特性:
android:onClick="@{doClick}"

//在Activity中设置doClick的值
mBinding.doClick = this

 经过上面的设置后,Activity中实现了点击事件的接口,然后将doClick设置为当前的Activity,最后在View上设置处理点击事件的操作为doClick,这样就可以直接在Activity中设置相关的点击操作了。

 注意以下两种设置方式的区别:

android:onClick="@{doClick}"
android:onClick="doClick"

 第一种方式就是上面说到的使用DataBing的设置方式,需要经过三步。

 第二种方式是之前就已经提过的方式,不用开启databinding选项也可以使用。这种方式要求在Activity中有一个方法doClick(View v),这样相当于直接将点击操作放在这个方法中处理了。

 第一种方式在任何地方都可以使用,第二种方式在Fragment等地方不适用,但是在Activity中使用起来比第一种方式要方便很多。

方法引用

 事件可以直接绑定到处理脚本方法,类似于为Activity中的方法指定android:onClick的方式。与View OnClick特性相比,一个主要优点是表达式在编译时进行处理,因此,如果该方法不存在或者其签名不正确,则会收到编译时错误。

 方法引用和监听器绑定之间的主要区别是实际监听器实现是在绑定数据时创建的,而不是在事件触发时创建的,如果希望在监听器发生时对表达式求值,则应使用监听器绑定。

 要将事件分配给其处理脚本,可以使用常规绑定表达式,并要以其调用的方法名称作为值,如:

//创建类
class ClickHandler {

    fun click(view: View){
        if(view.id == R.id.tv_content10){
            Log.e("TAG","View被点击")
        }
    }
}

//在<data>标签下定义
<variable
    name="clickHandler"
    type="com.project.databinding_moudle.utils.ClickHandler"
    />

//在View中使用
<TextView
    android:id="@+id/tv_content10"
    style="@style/match_width"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toBottomOf="@id/tv_content9"
    android:padding="@{largePadding ? @dimen/large_padding : @dimen/small_padding}"
    android:gravity="center"
    android:background="@android:color/black"
    android:textColor="@android:color/white"
    android:text="@{@string/name_and_phone(user.name,user.phone)}"
    android:layout_marginTop="20dp"
    android:onClick="@{clickHandler::click}"
    />

 点击之后会出现如下的Log:

E/TAG: View被点击

 注意:表达式中的方法签名必须与监听器对象中的方法签名完全一致

监听器绑定

 监听器绑定是在事件发生时运行的绑定表达式,它们类似于方法引用,但允许运行任意数据表达式。

 在方法引用中,方法的参数必须与事件监听器的参数匹配,在监听器绑定中,返回值必须和监听器的预期返回值匹配(预期返回值无效除外),例如:

class Presenter {
    fun onSaveClick(task: Runnable){
        val thread: Thread = Thread(task)
        thread.start()
    }
}

 有了上面的类,然后就可以将点击事件绑定到onSaveClick()方法:

<variable
    name="task"
    type="Runnable"
    />
<variable
    name="presenter"
    type="com.project.databinding_moudle.utils.Presenter"
    />
    
<Button
    android:id="@+id/btn_content11"
    style="@style/match_width"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toBottomOf="@id/tv_content10"
    android:text="保存用户数据"
    android:layout_marginTop="20dp"
    android:onClick="@{() -> presenter.onSaveClick(task)}"
    />

 这样当我们运行上面的程序之后就可以保存用户数据了:

val task = Runnable {
    val sp: SharedPreferences = this.getSharedPreferences("test", Context.MODE_PRIVATE)
    val edit = sp.edit()
    edit.putString("name",user.name)
    edit.putInt("age",user.age)
    edit.putString("phone",user.phone)
    edit.apply()
    edit.commit()
}
val presenter: Presenter = Presenter()

mBinding.task = task
mBinding.presenter = presenter

 在表达式中使用回调时,数据绑定会自动为事件创建并注册必要的监听器。当视图触发事件时,数据绑定会对给定表达式求值,与常规绑定表达式一样,在对这些监听器表达式求值时,仍会获得数据绑定的Null值和线程安全。

在上面的例子中,我们在给Button设置监听器的表达式的时候,使用了android:onClick="@{() -> presenter.onSaveClick(task)}"这样的语法,可以看到,我们没有在表达式中传递任何值。这是因为监听器绑定提供两个监听器参数选项:可以忽略方法的所有参数,也可以命名所有参数。如果想命名参数,则可以在表达式中使用这些参数,例如:上面的表达式也可以写成下面的形式:

android:onClick = "@{(view) -> presenter.onSaveClick(task)}"

或者,想要在表达式中使用参数,则可以写成如下的形式:

class Presenter {
    fun onSaveClick(view: View, task: Runnable){
        val thread: Thread = Thread(task)
        thread.start()
    }
}

这样,在监听器中可以设置如下绑定:

android:onClick = "@{(view) -> presenter.onSaveClick(view,task)}"

另一个例子是这样的:

    fun onSaveClick(save: Boolean, task: Runnable){
        if(save){
            val thread = Thread(task)
            thread.start()
        }
    }

在上面的方法中接收一个Boolean和一个Runnable类型的参数,然后在布局文件中可以这样使用:

 <CheckBox
        style="@style/match_width"
        android:layout_marginTop="20dp"
        android:onCheckedChanged="@{(cb,checked) -> presenter.onSaveClick(checked,task)}"
        android:text="保存用户信息"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btn_content11" />

如果监听的事件返回值类型不是void类型,则表达式也必须返回相同类型的值,例如,要监听长按事件,表达式应该返回一个布尔值。

fun onLongClickSave(view: View,task: Runnable): Boolean{
    val thread: Thread = Thread(task)
    thread.start()
    return true
}

在布局文件中使用:

<Button
    style="@style/match_width"
    app:layout_constraintTop_toBottomOf="@id/cb_content11"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    android:text="长按保存用户信息"
    android:onLongClick="@{(view) -> presenter.onLongClickSave(view,task)}"
    />

同时,如果需要将表达式和谓词(例如三元运算符)结合使用,则可以使用void作为符号:

android:onClick="@{(view) -> view.isVisible() ?  presenter.onLongClickSave(view,task) : void}"
需要注意的是:监听器表达式功能非常强大,可以使代码非常易于阅读。另一方面,包含复杂表达式的监听器会使得布局难以阅读和维护,这些表达式应该像可用数据从界面传递到回调方法一样简单。应该在从监听器表达式调用的回调方法中实现业务逻辑。

导入、变量和包含

这几个功能在上面学习其他语法的时候都学习过了,这里做一个简单的总结。

导入

就是在<data>...</data>标签中导入我们需要使用的类,比如下面的导入android.view.View类:

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

当类名有冲突的时候,可以将其中一个类指定为其它名称,也就是别名,然后使用的时候直接使用别名即可:

<import type="android.view.View" 
    alias="MyView"
    />

使用:

android:visibility="@{user.age > 10 ? MyView.GONE : MyView.VISIBLE}"

注意:如果我们指定了一个别名,那么就不能再使用它原来的名字了。

变量

变量就是我们再布局文件的表达式中需要使用的数据,也是定义在<data>...</data>标签下:

<data>
    <variable 
        name="user" 
        type="User"/>
</data>

像这样就定义了一个User类型的user变量。

表达式中仍然支持强制类型转换。

包含

当我们使用<include />标签包含另一个布局文件的时候,在另一个布局文件中可能也会有定义的一些变量,此时可以使用bind:来对变量进行绑定:

<include
    android:id="@+id/layout_title"
    layout="@layout/base_layout_header"
    bind:title="@{@string/data_binding_test1}" />

base_layout_header这个布局文件中我们定义了一个String类型的title,在当前布局文件中就可以使用bind来对title的值进行绑定。