前言
DataBinding
我是没有用过的,谷歌推出它是应该是很早的事情了,主要是配合MVVM
这种构建模式使用的一个工具。2019年第一次使用MVVM
架构模式开发应用时也在犹豫要不要用DataBinding
,但最后还是没有用。主要的原因有几点,一是当时的同事不怎么推荐,觉得在xml
写太多代码比较凌乱,又不方便调试(在细看DataBinding
后知道是可以调试的,但调试相对以往的操作来说也的确相对麻烦一点),另外大家也不熟,所以就这样不了了之。对于身边一些开发的朋友,其实也没听说过谁在用DataBinding
,也没有听过谁推荐。网上查了一下,倒是有很多论证DataBinding
好处的文章,也对一些质疑进行回击。小弟认为DataBinding
作为官方推出了的一个这么久且还没放弃的组件,它一定是有的优势,借此重温Jetpack的机会必须要对它有一次全面的了解。
简介
官方的描述
- 数据绑定库是一种支持库,借助该库,您可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。(developer.android.com/topic/libra…)
DataBinding
可以实现数据的双向绑定,它是MVVM
构建模式的一种实现,但非必须,可降低布局和逻辑的耦合性。DataBinding
的优势主要有:
- 双向数据绑定:当数据内容有变化的时候,
DataBinding
会自动更新UI,不再需要人工通过View的id进行绑定,同时当UI的数据发生改变是,model中的数据同样会同步发生改变。 - 精简了代码量:当使用
DataBinding
后,findViewById
、setOnClickListener()
这种代码就不用再写了,Activity或者Fragment会简洁不少; - 绑定数据空安全:在xml实现绑定数据的操作是空安全的,空安全就是它会自己判断空指针,所以大大减少了数据绑定带来的空指针问题。
DataBinding的基本使用
我们通过一个模拟加载数据的小案例来捋一下DataBinding
的使用流程,后面再说一些使用细节诸如在xml
中语法就清晰得多了。
gradle文件修改
在App层级的build.gradle中android节点下添加以下代码就可以使用DataBinding
了:
android {
dataBinding {
enabled = true
}
}
设置布局
声明一个数据model:
@NoArgAnn
data class UserModel(
var name: String,
var sex: String
)
这里的注解@NoArgAnn
是自定义的,主要是为了data class
可以通过无参构造方法获得对象,使用了NoArg和AllOpen插件,如果没有使用过可以看看我的这篇文章:juejin.cn/post/706832… 。
布局:
要使用DataBinding
,布局中的根标记必要要以layout
开头,然后包含data
和普通布局的layout
。看下面MainActivity
布局:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<!--指定可在此布局使用的变量model 变量名字不可以有下划线,否则系统会找不到文件-->
<variable
name="userModel"
type="com.qisan.databinding.UserModel" />
<!--OnViewClickHandler 主要处理view点击事件回调后的处理-->
<variable
name="onViewClickHandler"
type="com.qisan.databinding.MainActivity.OnViewClickHandler" />
<!--声明一个基本数据类型变量 这里更多是我们平时在activity中的使用的全局标志位之类-->
<variable
name="isShow"
type="Boolean" />
<!--把View倒入进来,xml才知道-->
<import type="android.view.View" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center">
<!--include进来的布局也可以使用dataBinding-->
<include
app:isShow="@{isShow}"
layout="@layout/layout_loading" />
<!--布局中的表达式使用“@{}”语法给控件赋值。-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@{userModel.name}"
android:textColor="@color/black"
android:textSize="16sp"
android:textStyle="bold"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="4dp"
android:text="@{userModel.sex}"
android:textColor="@color/teal_700"
android:textSize="14sp"
android:textStyle="bold"/>
<!--() -> onViewClickHandler.userChangeClick()是指定点击之后执行的方法 如果你需要view的话也可这样:
(view) -> onViewClickHandler.userChangeClick(view)-->
<!--显示隐藏的做法:@{isShow ? View.GONE : View.VISIBLE}-->
<Button
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_margin="16dp"
android:background="@color/teal_700"
android:gravity="center"
android:onClick="@{() -> onViewClickHandler.userChangeClick()}"
android:text="加载数据"
android:textColor="@color/white"
android:textSize="16sp"
tools:ignore="HardcodedText"
android:visibility="@{isShow ? View.GONE : View.VISIBLE}"/>
</LinearLayout>
</layout>
这里要说一下include
进来布局layout_loading.xml
代码:
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="isShow"
type="Boolean" />
<import type="android.view.View" />
</data>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rl_loading"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="@{isShow ? View.VISIBLE : View.GONE,default=gone}">
<ProgressBar
android:id="@+id/progressBar2"
style="?android:attr/progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true" />
</RelativeLayout>
</layout>
这里一定要把data
内容声明一遍,不然isShow
这个值无法达到双向绑定的目的,而且isShow
在根布局那里通过app:isShow="@{isShow}"
来传递值。
Activity的处理:
Activity
中的处理主要是通过DataBindingUtil
绑定布局,并绑定在xml
声明的data。这里要注意的是,在xml
布局设置好了之后记得make
下项目,不然没有生成布局文件对应的绑定类。这里把MianActivity的代码贴出来:
class MainActivity : AppCompatActivity() {
private lateinit var userModel: UserModel
private lateinit var mainBinding: ActivityMainBinding
private var count: Int = 0
protected var isShow = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mainBinding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
//通过反射获取无参构造方法获得一个对象
userModel = UserModel::class.java.newInstance()
mainBinding.onViewClickHandler = OnViewClickHandler()
}
inner class OnViewClickHandler {
fun userChangeClick() {
isShow = true
mainBinding.isShow = isShow
//模拟一下数据加载
Handler(Looper.getMainLooper()).postDelayed({
when (count) {
0 -> {
userModel.name = "小明"
userModel.sex = "男"
}
1 -> {
userModel.name = "李梅梅"
userModel.sex = "女"
}
else -> {
userModel.name = "张三"
userModel.sex = "男"
}
}
//数据绑定
mainBinding.userModel = userModel
isShow = false
mainBinding.isShow = isShow
if(count == 2) count=0 else count++
}, 2000)
}
}
}
这里把点击事件回调处理的OnViewClickHandler
类写成了一个内部类,这样是为了方便使用全局变量作为演示,一般开发中我们会单独作为一个类来处理,这样代码分离也会更好一点,这点在MVVM
架构中会有更好的体现。
案例演示效果:
在使用DataBinding
注意一下几点:
layout
用作布局的根节点,只能包裹一个View
标签,且不能包裹merge
标签;<data>
标签只能存在一个,而且它还有个属性class,我们可以自定义DataBinding
生成的类名及路径,但我们一般不需要这样做:
<!--自定义类名-->
<data class="CustomDataBinding">
</data>
<!--自定义生成路径以及类型-->
<data class="com.qisan.databinding.CustomDataBinding">
</data>
DataBinding的绑定表达式
@{}表达式中支持的运算操作符和关键字
在上面的使用案例中,我们看到layout_loading.xml
中对加载效果layout
隐藏显示是使用表达式来处理的:
android:visibility="@{isShow ? View.VISIBLE : View.GONE,default=gone}"
后面加了一个default=gone
可以默认隐藏,如果不加会默认显示出来。此外,xml
文件支持以下的运算操作符和关键字的表达式:
- 算术运算符
+ - / * %
- 字符串连接运算符
+
- 逻辑运算符
&& ||
- 二元运算符
& | ^
- 一元运算符
+ - ! ~
- 移位运算符
>> >>> <<
- 比较运算符
== > < >= <=
(请注意,<
需要转义为<
) instanceof
- 分组运算符
()
- 字面量运算符 - 字符、字符串、数字、
null
- 类型转换
- 方法调用
- 字段访问
- 数组访问
[]
- 三元运算符
?:
表达式不支持的关键字和操作:
- this
- super
- new
- 显式泛型调用
空合并运算符
上面这些操作符我们或多或少都见到过或用过,另外DataBinding
还新增了Null合并运算符(??),或者叫空合并运算符,我们看一下它的使用格式:
android:text="@{userModel.name ?? userModel.firstName}"
意思也很简单,就是左边的值不是null
,则空合并运算符选择左边的值,如果左边的值为 null
,则选择右边的值。
空安全
我们在前面说过DataBinding
的数据绑定是空安全的,这是因为它在生产类中检查值是否为ull,并避免出现空指针异常。看下面代码:
android:text="@{userModel.name}"
如果userModel
是null,则userModel.name
默认返回null,如果是Int类型的值,如userModel.age
,如果userModel
为null,则默认返回0。
视图引用
当前View(即控件)可以通过其他View的id引用其属性值,通过将id转换成驼峰式写法即可引用,请看下面代码:
<EditText
android:id="@+id/et_account"
android:layout_width="match_parent"
android:layout_height="48dp"
android:hint="请输入账号"
android:textColor="@color/teal_200"
android:inputType="text"
android:textColorHint="@color/purple_500"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{etAccount.text}"
android:layout_marginTop="16dp"
android:textColor="@color/teal_200"
android:textSize="16sp"
android:textStyle="bold"/>
这样当输入的值变化时,TextView的值也会跟着变化:
如果要在xml
直接通过etAccount.text
拿值传给view的监听回调函数会编译不通过,我们先找OnViewClickHandler
添加一个方法:
fun getEditTtext(editText: String){
Toast.makeText(this@MainActivity,editText,Toast.LENGTH_SHORT).show()
}
然后在Button
的onClick
中调用:
<Button
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_margin="16dp"
android:background="@color/teal_700"
android:gravity="center"
android:onClick="@{() -> onViewClickHandler.getEditTtext(etAccount.text)}"
android:text="加载数据"
android:textColor="@color/white"
android:textSize="16sp"
android:visibility="@{isShow ? View.GONE : View.VISIBLE}"/>
结果运行编译不通过,改成etAccount.text.toString()
也不行,在官方文档也没有找到解决方法,不知道是不是我写法的问题,如果有同学知道的话麻烦在评论区告诉我。这个方法不行,我们可以这样做,先在<data>
标签中添加一个变量:
<variable
name="inputValue"
type="String" />
然后在EditText
中对inputValue
赋值:
<EditText
android:id="@+id/et_account"
android:layout_width="match_parent"
android:layout_height="48dp"
android:hint="请输入账号"
android:textColor="@color/teal_200"
android:inputType="text"
android:textColorHint="@color/purple_500"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="@={inputValue}"/>
这样,inputValue
的值就是EditText
输入的值,Button
中的点击操作改成这样:
android:onClick="@{() -> onViewClickHandler.getEditTtext(inputValue)}"
运行效果:
集合
为方便起见,可使用 []
运算符访问常见集合,例如数组、列表、稀疏列表和映射。
<data>
<import type="android.util.SparseArray"/>
<import type="java.util.Map"/>
<import type="java.util.List"/>
<variable name="list" type="List<String>"/>
<variable name="sparse" type="SparseArray<String>"/>
<variable name="map" type="Map<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<String>
。您还可以使用 object.key
表示法在映射中引用值。例如,以上示例中的 @{map[key]}
可替换为 @{map.key}
。
字符串字面量
我们可以使用单引号括住特性值,这样就可以在表达式中使用双引号,如以下示例所示:
android:text='@{map["firstName"]}'
也可以使用双引号括住特性值。如果这样做,则还应使用反单引号 `
将字符串字面量括起来:
android:text="@{map[`firstName`]}"
资源引用
在xml
文件中可以通过表达式使用以下语法引用应用资源:
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
large
只是一个布尔值的判断哈。
我们也可以通过提供参数来评估格式字符串和复数形式:
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
我们也可以将属性引用和视图引用作为资源参数进行传递:
android:text="@{@string/example_resource(user.lastName, exampleText.text)}"
某些资源需要显式类型求值,如下图:
事件处理
通过数据绑定,我们可以编写从视图分派的表达式处理事件(例如,onClick()
方法)。事件特性名称由监听器方法的名称确定,但有一些例外情况。例如,View.OnClickListener
有一个onClick()
方法,所以该事件的特性为android:onClick
。这个从我们文章开头介绍的案例已经知道了,此外还有一些专门针对点击事件的事件处理方法,这些处理方法需要使用除android:onClick
以外的特性来避免冲突。您可以使用以下属性来避免这些类型的冲突:
我们可以通过可以使用方法引用 或 监听器绑定 来进行事件处理。
方法引用
//定义的方法引用
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>
方法引用的方式要注意的是:引用的方法,参数需要和监听器方法签名一致 (参数类型、个数),所以onClickFriend
方法的参数类型是View
.
监听器绑定
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>
从上面两段模板代码可以看出引用方法需要和监听器的参数一致,而监听器绑定更加灵活,可在运行时动态运行lambda表达式,参数无需一致。在我们文章开头展示的案例中就是采用的监听器绑定实现,区别的就是没有另外再写Task来执行逻辑,而是直接在方法体内实现了。
监听器如果有多个参数并且需要拿到点击事件的参数需要这样写:
class Presenter {
fun onCompletedChanged(task: Task, completed: Boolean){}
}
//监听CheckBox选中状态变化
<CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content"
android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />
这里有点要注意:
- 如果监听事件的返回类型不为Void,lambda表达式也要返回相同类型的值!
- 监听器表达式这种写法功能强大,可以使代码更易阅读,但不建议写太复杂的表达式,本末倒置,反而使得布局难以阅读和维护。
使用可观察数据对象
可观察性是指一个对象将其数据变化告知其他对象的能力。通过数据绑定库,您可以让对象、字段或集合变为可观察。任何对象都可用于数据绑定,但修改对象不会自动使界面更新。通过数据绑定,数据对象可在其数据发生更改时通知其他对象,即监听器。DataBinding
中可观察的数据对象有三种不同类型:字段、集合和对象,通过数据绑定,数据对象可在数据发生更改时通知其他对象,即监听器。
可观察字段
在创建实现Observable接口的类时要完成一些操作,但如果您的类只有少数几个属性,这样操作的意义不大。在这种情况下,您可以使用通用 Observable
类和以下特定于基元的类,将字段设为可观察字段:
- ObservableBoolean
- ObservableByte
- ObservableChar
- ObservableShort
- ObservableInt
- ObservableLong
- ObservableFloat
- ObservableDouble
- ObservableParcelable
可观察字段是具有单个字段的自包含可观察对象。原语版本避免在访问操作期间封箱和开箱。如需使用此机制,请采用 Java编程语言创建 public final
属性,或在Kotlin中创建只读属性,如以下示例所示:
class LoginInfo {
val firstName = ObservableField<String>()
val lastName = ObservableField<String>()
val age = ObservableInt()
}
然后通过get()
获取值,set("")
来设置值:
loginInfo.firstName.set("")
loginInfo.firstName.get()
在对应的Observable
类中的set
方法中都调用notifyChange()
方法更新值:
public void set(T value) {
if (value != mValue) {
mValue = value;
notifyChange();
}
}
可观察集合
某些应用使用动态结构来保存数据。可观察集合允许使用键访问这些结构。当键为引用类型(如 String
)时,ObservableArrayMap
类非常有用,如以下示例所示:
ObservableArrayMap<String, Any>().apply {
put("firstName", "Google")
put("lastName", "Inc.")
put("age", 17)
}
布局中通过字符串key找到值:
<data>
<import type="android.databinding.ObservableMap"/>
<variable name="user" type="ObservableMap<String, Object>"/>
</data>
…
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text="@{String.valueOf(1 + (Integer)user.age)}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
当键为整数时,使用ObservableArrayList
,如下所示:
ObservableArrayList<Any>().apply {
add("Google")
add("Inc.")
add(17)
}
// 布局中通过索引访问列表
<data>
<import type="android.databinding.ObservableList"/>
<variable name="user" type="ObservableList<Object>"/>
</data>
<TextView android:text="@{user[index]}" ... />
可观察对象
实现Observable
接口的类允许注册监听器,以便它们接收有关可观察对象的属性更改的通知。
Observable
接口具有添加和移除监听器的机制,但何时发送通知必须由您决定。为便于开发,数据绑定库提供了用于实现监听器注册机制的 BaseObservable
类。实现BaseObservable
的数据类负责在属性更改时发出通知。具体操作过程是向 getter 分配 Bindable
注释,然后在setter
中调用 notifyPropertyChanged()
方法,如以下示例所示:
class LoginInfo : 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)
}
}
数据绑定在模块包中生成一个名为 BR
的类,该类包含用于数据绑定的资源的ID。在编译期间,Bindable注释会在BR
类文件中生成一个条目。如果数据类的基类无法更改,Observable
接口可以使用PropertyChangeRegistry
对象实现,以便有效地注册和通知监听器。
这里我直接编译一直报错,BR文件是已经导进来了的,但说找不到BR.firstName
找不到,这里需要在app的build.gradle
加入Kotlin-apt插件:
apply plugin: 'kotlin-kapt'
kapt {
generateStubs = true
}
回到我们文章开头的案例也可以改成这样子,不用在修改数据之后在通过viewDataBinding再set一次,但data class
中如何使用实现set方法还不清楚如何解决,如果你有解决方案麻烦评论区告诉我哈。
生成的绑定类
数据绑定库可以生成用于访问布局的变量和视图的绑定类。生成的绑定类将布局变量与布局中的视图关联起来。绑定类的名称和包可以自定义。所有生成的绑定类都是从ViewDataBinding
类继承而来的。系统会为每个布局文件生成一个绑定类。默认情况下,类名称基于布局文件的名称,它会转换为Pascal
大小写形式并在末尾添加 Binding 后缀。以上布局文件名为activity_main.xml
,因此生成的对应类为ActivityMainBinding
。此类包含从布局属性(例如,user
变量)到布局视图的所有绑定,并且知道如何为绑定表达式指定值。
创建绑定对象
我们一般通过DataBindingUtil
来创建绑定对象,它里面有很多种绑定创建方法,上面的案例是其中的一种。
val binding: MyLayoutBinding = MyLayoutBinding.inflate(layoutInflater)
setContentView(binding.root)
//
val binding: MyLayoutBinding = MyLayoutBinding.inflate(getLayoutInflater(), viewGroup, false)
如果布局是使用其他机制扩充的,可单独绑定,如下所示:
val binding: MyLayoutBinding = MyLayoutBinding.bind(viewRoot)
有时,系统无法预先知道绑定类型。在这种情况下,可以使用DataBindingUtil
类创建绑定,如以下代码段所示:
val viewRoot = LayoutInflater.from(this).inflate(layoutId, parent, attachToParent)
val binding: ViewDataBinding? = DataBindingUtil.bind(viewRoot)
如果您要在 Fragment
、ListView
或 RecyclerView
适配器中使用数据绑定项,您可能更愿意使用绑定类或DataBindingUtil
类的inflate()
方法,如以下代码示例所示:
val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)
// or
val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)
带 ID 的视图
DataBinding
会对布局中拥有ID的每个View
在绑定类中创建不可变字段。例如,数据绑定库会根据以下布局创建TextView
类型的firstName
和lastName
字段:
<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>
该库一次性从视图层次结构中提取包含ID的视图。相较于针对布局中的每个视图调用findViewById()
方法,这种机制速度更快。如果没有数据绑定,则ID并不是必不可少的,但仍有一些情况必须能够从代码访问视图。
变量
DataBinding会为布局中声明的每个变量生成getter、setter方法。
ViewStub
占位置,惰性加载,当ViewStub被inflate或setVisible可见,它会从视图层次结构消失,如果想绑定里面的View,需要在监听 OnInflateListener,在此完成绑定.
即时绑定
当可变或可观察对象发生更改时,绑定会按照计划在下一帧之前发生更改。如果需要立即执行绑定,强制执行,可 executePendingBindings(),但要注意,此方法必须运行在UI线程。
高级绑定(Adapter使用绑定)
有时,系统并不知道特定的绑定类。例如,针对任意布局运行的RecyclerView.Adapter
不知道特定绑定类。在调用onBindViewHolder()
方法时,仍必须指定绑定值。
在以下示例中,RecyclerView
绑定到的所有布局都有item
变量。BindingHolder
象具有一个getBinding()
方法,这个方法返回ViewDataBinding
基类。
class BindingHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
lateinit var binding: ViewDataBinding
}
class MyAdapter(data: List<User>) : RecyclerView.Adapter<BindingHolder>() {
private var mData = data
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder {
// 绑定
val binding: ViewDataBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.item_layout,
parent,
false
)
val holder = BindingHolder(binding.root)
holder.binding = binding
return holder
}
override fun onBindViewHolder(holder: BindingHolder, position: Int) {
val user = mData[position]
holder.binding.setVariable(BR.mUser, user)
}
override fun getItemCount() = mData.size
}
绑定适配器
绑定适配器负责发出相应的框架调用来设置值。例如,设置属性值就像调用setText()
方法一样。再比如,设置事件监听器就像调用 setOnClickListener()
方法。
数据绑定库允许您通过使用适配器指定为设置值而调用的方法、提供您自己的绑定逻辑,以及指定返回对象的类型。
自动选择方法
属性搜索对应方法,不会考虑命名空间,只考虑属性名称和类型 ,如:
android:text="@{user.name}"
如果user.getName()的返回值为String,查找接受String参数的setText()方法,所以表达式返回正确的类型很重要,必要时你还可以根据需要进行类型转换。
指定自定义方法名称
一些属性具有名称不符的 setter 方法。在这些情况下,某个特性可能会使用BindingMethods
注释与 setter 相关联。注释与类一起使用,可以包含多个BindingMethod
注释,每个注释对应一个重命名的方法。绑定方法是可添加到应用中任何类的注释。在以下示例中,android:tint
属性与 setImageTintList(ColorStateList)
方法相关联,而不与setTint()
方法相关联:
@BindingMethods(value = [
BindingMethod(
type = android.widget.ImageView::class,
attribute = "android:tint",
method = "setImageTintList")])
大多数情况下,您无需在 Android 框架类中重命名 setter。特性已使用命名惯例实现,可自动查找匹配的方法。
提供自定义逻辑
一些属性需要自定义绑定逻辑。例如,android:paddingLeft
特性没有关联的setter
,而是提供了setPadding(left, top, right, bottom)
方法。使用BindingAdapter
注释的静态绑定适配器方法支持自定义特性 setter 的调用方式。
Android 框架类的特性已经创建了 BindingAdapter
注释。例如,以下示例展示了 paddingLeft
属性的绑定适配器:
@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, padding: Int) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom())
}
参数类型非常重要。第一个参数用于确定与特性关联的视图类型,第二个参数用于确定在给定特性的绑定表达式中接受的类型。
使用接收多个属性的适配器:
@BindingAdapter("imageUrl", "error")
fun loadImage(view: ImageView, url: String, error: Drawable) {
Picasso.get().load(url).error(error).into(view)
}
在布局中使用适配器,如以下示例所示。请注意,@drawable/venueError
引用应用中的资源。使用 @{}
将资源括起来可使其成为有效的绑定表达式:
<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />
如果ImageView同时使用了imageUrl、error,且前者是String,后者是Drawable,就会调用适配器。
如果我们要设置了任意属性就调用适配器,可以将适配器的 requireAll 设置为 false,示例如下:
@BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = false)
fun setImageUrl(imageView: ImageView, url: String?, placeHolder: Drawable?) {
if (url == null) {
imageView.setImageDrawable(placeholder);
} else {
MyImageLoader.loadInto(imageView, url, placeholder);
}
}
requireAll
设置为false,没填写的属性将为null,需要做好非空判断!- 上述写法命名空间可以随意,写
xxx:imageUrl
也是可以的,但如果定义了如android:imageUrl
就只能用这个命名空间; - 自定义的绑定适配器和默认数据绑定适配器冲突,会使用自定义的绑定适配器;
@BindingMethod
属性名和@BindingAdapter
定义的属性名相同会冲突报错。
对象转换
自动转换对象
当绑定表达式返回 Object
时,库会选择用于设置属性值的方法。Object
会转换为所选方法的参数类型。对于使用ObservableMap
类存储数据的应用,这种行为非常便捷,如以下示例所示:
<TextView
android:text='@{userMap["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
注意:您还可以使用
object.key
表示法引用映射中的值。例如,以上示例中的@{userMap["lastName"]}
可替换为@{userMap.lastName}
。
表达式中的 userMap
对象会返回一个值,该值会自动转换为用于设置 android:text
特性值的 setText(CharSequence)
方法中的参数类型。如果参数类型不明确,则必须在表达式中强制转换返回类型。
自定义转换
在某些情况下,需要在特定类型之间进行自定义转换。例如,视图的 android:background
特性需要 Drawable
,但指定的color
值是整数。以下示例展示了某个属性需要 Drawable
,但结果提供了一个整数:
<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
每当需要Drawable且返回整数时,int都应转换为ColorDrawable,可以使用 @BindingConversion 注解静态方法来完成这个转换。
@BindingConversion
fun convertColorToDrawable(color: Int) = ColorDrawable(color)
双向数据绑定
使用单向数据绑定时,您可以为特性设置值,并设置对该特性的变化作出反应的监听器:
<CheckBox
android:id="@+id/rememberMeCheckBox"
android:checked="@{viewmodel.rememberMe}"
android:onCheckedChanged="@{viewmodel.rememberMeChanged}"
/>
双向数据绑定为此过程提供了一种快捷方式:
<CheckBox
android:id="@+id/rememberMeCheckBox"
android:checked="@={viewmodel.rememberMe}"
/>
@={}
表示法(其中重要的是包含“=”符号)可接收属性的数据更改并同时监听用户更新。
为了对后台数据的变化作出反应,您可以将您的布局变量设置为Observable
(通常为BaseObservable
)的实现,并使用@Bindable
注释。
class LoginViewModel : BaseObservable {
// val data = ...
@Bindable
fun getRememberMe(): Boolean {
return data.rememberMe
}
fun setRememberMe(value: Boolean) {
// Avoids infinite loops. 避免死循环
if (data.rememberMe != value) {
data.rememberMe = value
// React to the change. 对变化做出反应
saveData()
// Notify observers of a new value. 更新数据
notifyPropertyChanged(BR.remember_me)
}
}
}
使用自定义特性的双向数据绑定
如果您我们希望结合使用双向数据绑定和自定义特性,则需要使用@InverseBindingAdapter
和 @InverseBindingMethod
注释。例如,如果要在名为MyView
的自定义视图中对 "time"
特性启用双向数据绑定,请完成以下步骤:
1、使用 @BindingAdapter
,对用来设置初始值并在值更改时进行更新的方法进行注释:
@BindingAdapter("time")
@JvmStatic fun setTime(view: MyView, newValue: Time) {
// Important to break potential infinite loops.
if (view.time != newValue) {
view.time = newValue
}
}
2、使用@InverseBindingAdapter
对从视图中读取值的方法进行注释:
@InverseBindingAdapter("time")
@JvmStatic fun getTime(view: MyView) : Time {
return view.getTime()
}
此时,数据绑定知道在数据发生更改时要执行的操作(调用使用@BindingAdapter
注释的方法)以及当 view 视特性发生更改时要调用的内容(调用InverseBindingListener
)。但是,它不知道特性何时或如何更改。
为此,我们需要在视图上设置监听器。这可以是与自定义视图相关联的自定义监听器,也可以是通用事件,例如失去焦点或文本更改。将@BindingAdapter
注释添加到设置监听器(用来监听属性更改)的方法中:
@BindingAdapter("app:timeAttrChanged")
@JvmStatic fun setListeners(
view: MyView,
attrChange: InverseBindingListener
) {
// Set a listener for click, focus, touch, etc.
}
该监听器包含一个InverseBindingListener
参数。您可以使用InverseBindingListener
告知数据绑定系统,特性已更改。然后可以开始调用使用 @InverseBindingAdapter
注释的方法,依此类推。
转换器
如果绑定到View
对象的变量需要设置格式、转换或更改后才能显示,则可以使用Converter
对象。
以显示日期的EditText
对象为例:
<EditText
android:id="@+id/birth_date"
android:text="@={Converter.dateToString(viewmodel.birthDate)}"
/>
viewmodel.birthDate
属性包含 Long
类型的值,因此需要使用转换器设置格式。
由于使用了双向表达式,因此还需要使用反向转换器,以告知库如何将用户提供的字符串转换回后备数据类型(在本例中为 Long
)。此过程是通过向其中一个转换器添加 @InverseMethod
注释并让此注释引用反向转换器来完成的。以下代码段显示了此配置的一个示例:
object Converter {
//注解修饰反向转换器。
@InverseMethod("stringToDate")
@JvmStatic fun dateToString(
view: EditText, oldValue: Long,
value: Long
): String {
// Converts long to String.
}
@JvmStatic fun stringToDate(
view: EditText, oldValue: String,
value: String
): Long {
// Converts String to long.
}
}
DataBinding
的基本使用就先介绍到这里了,如果后面使用上还有什么坑就细细道来。
总结
DataBinding
主要是往前端数据绑定的思想靠,它的确可以简化不少代码,最直观的就是在Activity
中少了很多findViewById
的代码,也可以绑定数据,不用再用每个控件去setter数据。但DataBinding
的确又很难用(郭霖大神也说很难用~),首先在布局中做比较多的数据赋值,或者是数据判断处理,这和传统开发会有很大的不一样,调试来说也比传统的调试要麻烦一些,这些肯定会让很多开发者头痛,小弟也是挺头痛。另外,我们还是要对View
诸如点击事件的操作方法逻辑,这一部分的代码并没有少的,只可以说做到了比较好的解耦。如果公司的开发人员都对DataBinding
比较熟悉,大家约定好实用规则,确实可以带来很大的开发便利。无论怎么说,DataBinding
还是非常优秀的库,能用还是得用。