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方法。如TextView
的android: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。即:
user.lastName = "NewLastName"
=>user.setLastName("NewLastName")
notifyPropertyChanged(BR.lastName)
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()
}
}
其中
InverseBindingListener
是androidx.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
这个类。