Data Binding(数据绑定库)

1,411 阅读8分钟

Data Binding(数据绑定库)

Data Binding继承了View Binding,所以在Data Binding中不仅有视图变量,还包含数据变量。

配置

在应用app模块的build.gradle

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

布局

<?xml version="1.0" encoding="utf-8"?>
<!-- 根标签为layout -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 数据变量 -->
    <data>
        <variable name="user" type="com.example.User"/> <!-- 变量 -->
        <import type="android.view.View" /> <!-- 导入类 -->
        <import type="com.example.real.estate.View" alias="Vista"/> <!-- 导入类与别名 -->
    </data>
    <!-- 视图布局。如果不使用Data Binding,而是使用LayoutInflater,则会忽视以上的数据变量,直接扩充以下布局, -->
    <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>

绑定数据与视图

布局文件:example_layout.xml

生成的Data Binding类:ExampleLayoutBinding

在代码中绑定视图

两种方式

  • 直接使用生成类
ExampleLayoutBinding.inflate(getLayoutInflater())
ExampleLayoutBinding.inflate(layoutInflater, viewGroup, false)
  • 使用Data Binding工具类
// Activity
DataBindingUtil.setContentView(this, R.layout.example_layout)
// Fragment, RecyclerView
DataBindingUtil.inflate(layoutInflater, R.layout.example_layout, viewGroup, false)

Activity

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

    val binding: ExampleLayoutBinding = DataBindingUtil.setContentView(
                this, R.layout.example_layout)

    // 绑定变量
    binding.user = User("Test", "User")
}

或者

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

    val binding: ExampleLayoutBinding = ExampleLayoutBinding.inflate(getLayoutInflater())
    setContentView(binding.root)

    // 绑定变量
    binding.user = User("Test", "User")
}

Fragment

private var _binding: FragmentPersonBinding? = null
private val viewBinding get() = _binding!!

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    // 绑定视图
    _binding = FragmentPersonBinding.inflate(inflater, container, false)
    // 或者:
    // _binding = DataBindingUtil.inflate(layoutInflater, R.layout.example_layout, container, false)
    
    return viewBinding.root
}

override fun onDestroyView() {
    super.onDestroyView()
    _binding = null // 解绑
}

RecyclerView

// 创建ViewHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
	CommonViewHolderWithDataBinding(DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.example_layout, parent, false))
// 或者CommonViewHolderWithDataBinding(ExampleLayoutBinding.inflate(LayoutInflater.from(context), parent, false))

/**
 * 使用Data Binding的通用视图持有类
 * @param viewBinding 视图数据绑定类,需要转换为自定义布局文件对应的绑定类
 */
class CommonViewHolderWithDataBinding(val viewBinding: ViewDataBinding)
        : RecyclerView.ViewHolder(viewBinding.root)

在代码中访问视图

这是View Binding提供的功能。在布局文件中为某个视图设置ID,然后通过数据绑定类访问这个视图。

布局ID:@+id/example_view

变量名:exampleView

布局文件example_layout.xml

<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}"
       android:id="@+id/firstName"/>
           <TextView android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:text="@{user.lastName}"
      android:id="@+id/lastName"/>
       </LinearLayout>
    </layout>

代码

val dataBinding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.example_layout, viewGroup, false)
dataBinding.user = User()	// 绑定数据
dataBinding.firstName.text = "FirstName"  // 访问id为firstName的TextView视图
dataBinding.lastName.text = "LastName"  // 访问id为lastName的TextView视图

在视图中使用数据

属性引用

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

字符串字面量

android:text='@{map["firstName"]}'
<!-- 或 -->
android:text="@{map[`firstName`]}"

资源

android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
<!-- 对格式字符串求值 -->
android:text="@{@string/nameFormat(firstName, lastName)}"

事件处理

方法引用

包含事件方法的类

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}"/> <!-- 方法引用 -->
       </LinearLayout>
    </layout>
监听器绑定

即使用Lambda表达式。

包含事件方法的类

class Presenter {
    // 返回值与事件返回值相同,如长按事件返回Boolean
    fun onLongClick(view: View, task: Task): Boolean { }
}

在布局中书写Lamdba表达式

<?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)}"
                    android:onLongClick="@{(view) -> presenter.onLongClick(view, task)}"
                    />
        </LinearLayout>
    </layout>

Lambda表达式的参数列表有两种选择

  • 空:忽略回调方法(如onLongClick)的所有参数
  • 所有:命名所有参数并使用

<data>标签的子元素

导入import
<data>
    <import type="android.view.View" /> <!-- 导入类 -->
    <import type="com.example.real.estate.View" alias="Vista"/> <!-- 导入类与别名 -->
</data>

使用

android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"
android:xxxAttr="@{user.isChild ? Vista.VAR1 : Vista.VAR2}" <!--使用别名-->
变量variable
<data>
    <import type="android.graphics.drawable.Drawable"/>
    <variable name="user" type="com.example.User"/>
    <variable name="image" type="Drawable"/>
</data>
包含include

向布局文件中包含的子布局文件传递变量值

<?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}"/>	<!-- 传递user变量 -->
           <include layout="@layout/contact"
               bind:user="@{user}"/>
       </LinearLayout>
    </layout>

绑定适配器

设置属性值

视图调用属性关联的方法为属性设置值。

自动选择方法

针对布局中控件的原生属性(android:、官方app:),系统会自动寻找该属性对应的setter方法。如TextViewandroid:text=@{user.name}属性表达式,则系统会如下自动调用方法:

TextView.text = user.name
// Java为:TextView.setText(user.getName())

指定自定义方法名称

视图属性与(官方库)视图方法名称不直接对应时。参考androidx.databinding.adapters.ViewBindingAdapter类。

当然这部分也不需要我们关心。

提供自定义逻辑

为属性绑定自定义方法。这是我们要关心的。

可以在项目中的任意类里创建带BindingAdapter注解的方法。

  • Java:必须为静态方法
  • Kotlin:
    • .kt文件中声明一个函数
    • 声明为扩展函数
一个属性

方法代码示例:

@BindingAdapter("android:customAttr") // 声明属性名
fun setCustomAttr(view: View, attr: Int) {
    // view:第一个参数用于确定与属性关联的视图类型,必须有
    // attr:对应的属性的绑定表达式的返回值类型
    ...
}

在布局中使用:

<View android:customAttr="@{user.age}" /> <!-- user.age是Int型 -->
多个属性
  • 需要全部属性

表示需要在布局中声明所有的属性,并绑定表达式。

方法代码示例:

@BindingAdapter("imageUrl", "error")
fun loadImage(view: ImageView, url: String, error: Drawable) {
    Picasso.get().load(url).error(error).into(view)
}

布局代码:

<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />

数据绑定库在匹配时会忽略自定义命名空间,所以imageUrl即对应app:imageUrl

  • 需要部分属性

表示只要布局中有一个属性匹配,则可以调用方法。将注解中的requireAll设为false

方法代码示例:

@BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = false) // requireAll为false即表示不需要全部属性都声明
fun setImageUrl(imageView: ImageView, url: String?, placeHolder: Drawable?) {
    if (url == null) {
        imageView.setImageDrawable(placeholder);
    } else {
        MyImageLoader.loadInto(imageView, url, placeholder);
    }
}

布局代码:

<ImageView app:imageUrl="@{venue.imageUrl}" />	<!-- 使用上述方法 -->
<ImageView app:placeholder="@{@drawable/placeholder}" /> <!-- 使用上述方法 -->
类型转换
  • 自动对象转换
  • 自定义转换
<View
    android:background="@{isError ? @color/red : @color/white}"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

需要将@color/red转换为Drawable,使用带BindingConversion注解的方法。

@BindingConversion
fun convertColorToDrawable(color: Int) = ColorDrawable(color)

可观察对象(Observable)

上面一小节讲了系统是怎么将数据映射到视图属性上。那么就有一个问题,当数据发生改变时,怎么通知视图更新。具体内容参考官方文档使用可观察对象。我总结一下使用步骤。

  • 自定义一个类User,继承自BaseObservable

User类有两个属性:firstName, lastName。

  • User类的属性的getter方法添加Bindable注解
  • User类的属性的setter方法中发出通知,通知观察者(也就是视图)数据改变

于是得到以下代码:

class User : BaseObservable() {
    @get:Bindable
    var firstName: String = ""
    	set(value) {
            field = value
            notifyPropertyChanged(BR.firstName) // 然后视图会调用设置属性值的方法
        }

    @get:Bindable
    var lastName: String = ""
    	set(value) {
            field = value
            notifyPropertyChanged(BR.lastName)
        }
}

这是提醒一下,以上的过程中出现了两个setter方法:

  • BindingAdatper注解的setter方法——记为“M1”
  • 数据类的setter方法——记为“M2”

这两个方法的调用者是不一样,在项目代码中调用M2,然后M2通知视图数据改变,视图中对应的属性再调用M1。即:

  1. user.lastName = "NewLastName" => user.setLastName("NewLastName")
  2. notifyPropertyChanged(BR.lastName)
  3. android:text="@{user.lastName}" => @BindingAdapter setText(user.getLastName())

理解这个很重要。

双向绑定

针对自定义属性的双向绑定。

示例:

  • 视图:MyView
  • 自定义属性:time
  • 合成事件属性:timeAttrChanged

正向绑定(数据值 -> 视图)

使用BindingAdapter注解。当使用绑定表达式或数据发生改变时,调用BindingAdapter注解的方法更新视图。

@BindingAdapter("time")
@JvmStatic fun setTime(view: MyView, newValue: Time) {
    // 必须与旧值比较,避免无限循环产生(值改变->视图更新->值改变->视图更新->...)
    if (view.time != newValue) {
        view.time = newValue
    }
}

反向绑定(视图 -> 数据值)

使用InverseBindingAdapter注解。从视图中获取对应属性的值。就是说你想在代码中获得time的值时 ,系统会调用这个函数。

@InverseBindingAdapter("time")
@JvmStatic fun getTime(view: MyView) : Time {
    return view.getTime()
}

在代码中获得time属性

val time = myView.time
// 等价于:
// val time = getTime(myView)

监听器

如果你想在自定义属性改变时做一些事情,你可以参考以下的方法。

当某个属性支持双向绑定时,数据绑定系统会为该属性生成一个“合成事件属性”,名称为属性名加AttrChanged后缀。然后你可以使用该属性名在布局文件中绑定表达式。同时,你需要写一个BindingAdatper函数,用来监听属性改变。具体步骤如下:

  • 书写一个BindingAdapter函数
// 这个函数是事件属性timeAttrChanged的正向绑定函数
@BindingAdapter("app:timeAttrChanged")
@JvmStatic fun setListeners(
    view: MyView,
    attrChange: androidx.databinding.InverseBindingListener
) {
    myView.setOnClickListener { // 假设点击后,time属性改变,调用attrChange监听器通知变化
        attrChange.onChange()
    }
}

其中InverseBindingListenerandroidx.databinding包下的一个接口:

public interface InverseBindingListener {
    /**
     * Notifies the data binding system that the attribute value has changed.
     */
    void onChange();
}
  • 在布局文件中设置属性
<MyView app:timeAttrChanged="@{() -> doSomething()}" />

小结

以上内容大家可能看得有点晕,我再说两句。

正向绑定好理解,视图调用正向绑定的方法将数据填充到对应的属性上。至于如何通知视图数据改变,参考绑定适配器

反向绑定也好理解,就相当于一个getter函数,从视图中获得某个属性的值。

那自定义监听器呢,就是说我们希望能够监听视图中某个属性的变化。当这个属性值变化时,会调用与对应的事件属性(timeAttrChanged)绑定的表达式(如上面的@{() -> doSomething()})。这个调用发生在myView.setOnClickListener中。当然这里的点击监听只是举例,实际中很少用到,真正用到的监听器可以参考androidx.databinding.adapters.CalendarViewBindingAdapter这个类。

参考

Data Binding官方文档

在Kotlin中实现数据绑定框架中的BindingAdapter自定义属性