Jetpack DataBinding
Jetpck 才是真的豪华全家桶
引言
- DataBinding 实现了视图与数据的双向绑定,代码简洁,Activity 中代码不再因此膨胀。
- DataBinding 带来了软件架构的新模式 MVVM 。
- 如果使用 DataBinding 的主要目的是取代 findViewById() 调用,请考虑改用 ViewDataBinding。
- 灵活强大的框架,会带来学习成本的提升,深度掌握它,做工具的主人。
整体预览
Jetpack DataBinding 概览图用图说话
文章较长,考虑到心急的宝宝们看不到最后,所以总结性的图,放到最前面吧!
1. DataBinding 数据流向 生成绑定类:
2. DataBinding 数据流向 绑定类相互关系:
3. DataBinding 数据流向 普通-数据对象:
4. DataBinding 数据流向 可观察-数据对象:
5. DataBinding 数据流向 双向数据绑定:
6. DataBinding 数据流向 横向对比:
1. 语法说明
1.1 环境配置
模块启用
//build.gradle
android {
buildFeatures {
dataBinding true
}
}
1.2 布局文件格式
1.2.1 数据绑定布局文件
以根标记 layout 开头,后跟 data 元素和 view 根元素。绑定视图的根其实是View根元素。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
//data 中的 user 变量描述了可在此布局中使用的属性。
<variable
name="user"
type="com.kejiyuanren.jetpack.databinding.ViewModel" />
</data>
//布局中的表达式使用“@{}”语法写入特性属性中。
//在这里,TextView文本被设置为 user 变量的 userName 属性
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".databinding.DataBindingActivity">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="@{user.userName}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Tips:布局表达式应保持精简,因为它们无法进行单元测试,并且拥有的 IDE 支持也有限。
1.2.2 数据对象
定义一个最简单的数据对象。
data class ViewModel(val userName:String)
1.3 生成绑定类
生成的绑定类将布局变量与布局中的视图关联起来。所有生成的绑定类都是从 ViewDataBinding
类继承而来的。系统会为每个布局文件生成一个绑定类(绑定类名称规则:布局文件名为 activity_main.xml,因此生成的对应类为 ActivityMainBinding)。
1.3.1 创建绑定对象
创建绑定类的方式有很多种。
class DataBindingActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//普通实现
// setContentView(R.layout.activity_data_binding2)
//绑定实现 1
val binding: ActivityDataBinding2Binding = DataBindingUtil.setContentView(
this,
R.layout.activity_data_binding2
)
binding.user = ViewModel("kejiyuanren - 1")
//绑定实现2(其实binding类的生成,主要就是 DataBindingUtil类 和 xxxBinding类 中的实现)
// val binding2: ActivityDataBinding2Binding =
// ActivityDataBinding2Binding.inflate(layoutInflater)
// setContentView(binding2.root)
// binding2.user = ViewModel("keyijiyuanren-2")
}
}
Tips:绑定类创建汇总(xxxBinding
最终依然会调到 DataBindingUtil
)。
1.3.2 带 ID 的视图
数据绑定库会针对布局中具有 ID 的每个视图在绑定类中创建不可变字段。相较于针对布局中的每个视图调用 findViewById()
方法,这种机制速度更快。如果没有数据绑定,则 ID 并不是必不可少的,但仍有一些情况必须能够从代码访问视图。
1.3.3 变量
数据绑定库为布局中声明的每个变量生成访问器方法( setter 和 getter )。
//xml code
<layout ……>
<data>
<variable
name="star"
type="int" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
……>
<TextView
……
android:text="@{String.valueOf(star)}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
//java code
binding.setStar(5)
1.3.4 ViewStub
ViewStub是可用于延迟加载视图的组件。在DataBinding中的用法如下:
//xml code -> activity.xml
<layout ……>
<androidx.constraintlayout.widget.ConstraintLayout
……>
<ViewStub
android:layout="@layout/view_stub_tip"
…… />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
//xml code -> view_stub_tip.xml
<layout ……>
<data>
<variable
name="tipModel"
type="com.kejiyuanren.jetpack.databinding.TipViewModel" />
</data>
<TextView
android:text="@{tipModel.tipInfo}"
……/>
</layout>
//java code
fun viewStubAdd() {
//视图扩展监听器, 获取已经生成的绑定类,并执行更新操作
binding.viewStubExpand.setOnInflateListener { _, inflated ->
//OK : 方式 1,直接获取了view的binding缓存(因为binding已经生成过了,在ViewStubProxy中)
val vb1: ViewStubTipBinding? = DataBindingUtil.bind(inflated)
//OK : 方式 2,直接获取 ViewStubProxy中的 binding缓存
// val vb2 = binding.viewStubExpand.binding as ViewStubTipBinding
//ERROR : 方式 3, 直接通过扩展layout的绑定类生成,因为在ViewStubProxy中已经创建过了
//创建过的binding类会清空view对应的tag, 所以会报错(view must have a tag)
//这种机制也保证了,binding类的单例特性
// val vb3 = ViewStubTipBinding.bind(inflated)
vb1?.tipModel = TipViewModel("666")
}
//延迟5s触发加载扩展视图
binding.root.postDelayed({
val vs: ViewStub? = binding.viewStubExpand.viewStub
vs?.inflate()
}, 5000)
}
1.3.5 动态变量
有时,系统并不知道特定的绑定类。例如,针对任意布局运行的 RecyclerView.Adapter
不知道特定绑定类。在调用 onBindViewHolder()
方法时,仍必须指定绑定值。比如:RecyclerView 绑定到的所有布局都有 itemModel 变量。
//xml code
<layout ……>
<data>
<variable
name="itemModel"
type="com.kejiyuanren.jetpack.databinding.ItemViewModel" />
</data>
<TextView
android:text="@{itemModel.name}"
…… />
</layout>
//java code
import com.kejiyuanren.jetpack.BR //记得要引入 BR,不然会报错(Unresolved reference: BR)
class RvAdapter(private val mData: List<ItemViewModel>) :
RecyclerView.Adapter<RvAdapter.RvViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RvViewHolder {
val db = ItemDbRvTextBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return RvViewHolder(db)
}
override fun getItemCount(): Int {
return mData.size
}
override fun onBindViewHolder(holder: RvViewHolder, position: Int) {
holder.vb.setVariable(BR.itemModel, mData[position])
//当可变或可观察对象发生更改时,绑定会按照计划在下一帧之前发生更改。
//但有时必须立即执行绑定。要强制执行,请使用 executePendingBindings()` 方法。
holder.vb.executePendingBindings()
}
class RvViewHolder(binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) {
val vb = binding
}
}
1.3.6 自定义绑定类名称
默认情况下,绑定类是根据布局文件的名称生成的。通过调整 data 元素的 class 特性,绑定类可重命名或放置在不同的包中。
//xml code
<layout ……>
<data class="KeJiYuanRen">
</data>
</layout>
//java code
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//绑定类已经变为了自定义,不再是默认的命名规则类
val binding: KeJiYuanRen = DataBindingUtil.setContentView(
this,
R.layout.activity_data_binding2
)
}
1.4 绑定适配器
绑定适配器负责发出相应的框架调用来设置值。
1.4.1 自动选择方法
您可以使用数据绑定为任何 setter 创建特性。以 android:text="@{user.name}" 表达式为例,库会查找接受 user.getName() 所返回类型的 setText(arg) 方法。如果 user.getName() 的返回类型为 String,则库会查找接受 String 参数的 setText() 方法。
1.4.2 指定自定义方法名称
一些属性具有名称不符的 setter 方法(比如TextView中就没有setText(arg : Int)
)。那么就可以自定义方法名称,使用 BindingMethods
注释与 setter 相关联。注释与类一起使用,可以包含多个 BindingMethod
注释,每个注释对应一个重命名的方法。
//xml code
<layout ……>
<data>
<variable
name="star"
type="int" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
……>
<com.kejiyuanren.jetpack.databinding.Number
app:number="@{star}"
……/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
//java code
@BindingMethods( //自定义方法名称集合,可以包含多个BindingMethod
value = [ //这是个数组
BindingMethod(
type = TextView::class, //要操作的属性属于哪个类
attribute = "number", //xml属性,使用(app:number=“20”)
method = "setTextNumber" //指定xml属性对应的set方法,参数类型要对应
)]
)
class Number @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : TextView(context, attrs, defStyleAttr) {
fun setTextNumber(number : Int) {
text = number.toString()
}
}
1.4.3 提供自定义逻辑
一些属性需要自定义绑定逻辑。参数类型非常重要。第一个参数用于确定与特性关联的视图类型,第二个参数用于确定在给定特性的绑定表达式中接受的类型。
//xml code
<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />
//java code
//如果不需要同时满足,则requireAll设置为false
@BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = true)
fun loadImage(view: ImageView, url: String, error: Drawable) {
Picasso.get().load(url).error(error).into(view)
}
1.4.4 自动转换对象
当绑定表达式返回 Object
时,库会选择用于设置属性值的方法。如果参数类型不明确,则必须在表达式中强制转换返回类型。
1.4.5 自定义转换
在某些情况下,需要在特定类型之间进行自定义转换。转换器是全局的,请谨慎使用。
//xml code
<layout ……>
<data>
<variable
name="show"
type="boolean" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
……>
<TextView
android:visibility="@{show}"
……/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
//java code
object DbAdapter {
@BindingConversion
@JvmStatic
fun setShowView(show: Boolean): Int {
return if (show) {
View.VISIBLE
} else {
View.GONE
}
}
}
2. 扩展语法
2.1 布局和绑定表达式
2.1.1 表达式语言
表达式语言与托管代码中的表达式非常相似。
2.1.1.1 Null 合并运算符
//传统方式
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
//Null 合并运算符方式
android:text="@{user.displayName ?? user.lastName}"
2.1.1.2 避免出现 Null 指针异常
生成的数据绑定代码会自动检查有没有 null 值并避免出现 Null 指针异常。例如,在表达式 @{user.name} 中,如果 user 为 Null,则为 user.name 分配默认值 null。如果引用 user.age,其中 age 的类型为 int,则数据绑定使用默认值 0。
2.1.1.3 视图引用
表达式可以通过语法(绑定类将 ID 转换为驼峰式大小写),按 ID 引用布局中的其他视图。
//TextView 视图引用同一布局中的 EditText 视图
<EditText
android:id="@+id/example_text"
android:layout_height="wrap_content"
android:layout_width="match_parent"/>
<TextView
android:id="@+id/example_output"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{exampleText.text}"/>
2.1.1.4 集合
为方便起见,可使用 [] 运算符访问常见集合,例如数组、列表、稀疏列表和映射。
<data>
<import type="java.util.List"/>
<variable name="list" type="List<String>"/>
<variable name="index" type="int"/>
</data>
…
android:text="@{list[index]}"
2.1.1.5 字符串字面量
//可以使用单引号括住特性值,这样就可以在表达式中使用双引号
android:text='@{map["firstName"]}'
//也可以使用双引号括住特性值。如果这样做,则还应使用反单引号 ` 将字符串字面量括起来
android:text="@{map[`firstName`]}"
2.1.1.6 资源
//简单
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
//提供参数来评估格式字符串和复数形式
android:text="@{@string/nameFormat(firstName, lastName)}"
2.1.2 事件处理
通过数据绑定,您可以编写从视图分派的表达式处理事件(例如,onClick()
方法)。
2.1.2.1 方法引用
//xml code
<layout ……>
<data>
<variable
name="click"
type="com.kejiyuanren.jetpack.databinding.ClickHandler" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
……>
<TextView
android:onClick="@{click::onBtnClick}"
……/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
//java code
class ClickHandler {
fun onBtnClick(view: View) {
Log.d(TAG, "onBtnClick: ")
}
}
2.1.2.2 监听器绑定
监听器绑定是在事件发生时运行的绑定表达式。它们类似于方法引用,但允许运行任意数据绑定表达式。
//总有一款适合你
android:onClick="@{() -> presenter.onSaveClick(task)}"
android:onClick="@{(view) -> presenter.onSaveClick(task)}"
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
2.1.3 导入、变量和包含
2.1.3.1 导入
通过导入功能,您可以轻松地在布局文件中引用类,就像在托管代码中一样。可以在 data 元素使用多个 import 元素,也可以不使用。
<data>
<import type="com.kejiyuanren.jetpack.databinding.View"
alias="UnitView"/> //类型别名,防止与下面的冲突
<import type="android.view.View"/> //引入View
</data>
<TextView
android:text="@{user.lastName}"
android:text="@{((User)(user.connection)).lastName}" //类型强转
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/> //使用View
2.1.3.2 变量
您可以在 data 元素中使用多个 variable 元素。
2.1.3.3 包含
通过使用应用命名空间和特性中的变量名称,变量可以从包含的布局传递到被包含布局的绑定。注意:数据绑定不支持 include 作为 merge 元素的直接子元素。
//activity.xml
<layout ……>
<data>
<variable
name="user"
type="com.kejiyuanren.jetpack.databinding.ViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
……>
<TextView
android:text="@{user.userName}"
…… />
<include
layout="@layout/layout_title_append"
app:user="@{user}"
……/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
//layout_title_append.xml
<layout ……>
<data>
<variable
name="user"
type="com.kejiyuanren.jetpack.databinding.ViewModel" />
</data>
<TextView
android:text="@{user.userName.toUpperCase()}"
……>
</TextView>
</layout>
2.2 可观察的数据对象
可观察性是指一个对象将其数据变化告知其他对象的能力。通过数据绑定库,您可以让对象、字段或集合变为可观察。
2.2.1 可观察字段
在创建实现 Observable
接口的类时要完成一些操作,但如果您的类只有少数几个属性,这样操作的意义不大。字段设为可观察字段:ObservableXXX。
//数据模型
class ViewModel {
val userName = ObservableField<String>()
}
//观察更新
val userModel = ViewModel()
userModel.userName.set("keyijiyuanren-1")
binding.user = userModel
binding.click = ClickHandler(object : ClickListener{
override fun onClick() {
userModel.userName.set("replace = $count")
}
})
2.2.2 可观察对象
实现 Observable
接口的类允许注册监听器,以便它们接收有关可观察对象的属性更改的通知。
//数据模型
class ViewModel : BaseObservable() {
@get:Bindable
var userName: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.userName)
}
}
//观察更新
val userModel = ViewModel()
userModel.userName = "keyijiyuanren-1"
binding.user = userModel
binding.click = ClickHandler(object : ClickListener{
override fun onClick() {
userModel.userName = "replace = $count"
}
})
2.3 数据双向绑定
双向绑定可以在上面的 2.2.1 可观察字段
基础上,在xml中使用语法:@={} 表示法(其中重要的是包含“=”符号)可接收属性的数据更改并同时监听用户更新。
//xml code
<CheckBox
android:checked="@={checkModel.check}"
…… />
//java code
class CheckModel {
val check = ObservableBoolean(false)
}
2.3.1 使用自定义特性的双向数据绑定
&emsp;最常见的双向特性和更改监听器提供了双向数据绑定实现,可以将其用作应用的一部分。如果希望结合使用双向数据绑定和自定义特性,则需要使用 @InverseBindingAdapter
和 @InverseBindingMethod
注释。
下面实现了SeekBar的透明度与进度随动。
//xml code
<layout ……>
<data>
<variable
name="defineAlpha"
type="float " />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
……>
<SeekBar
app:okAlpha="@={defineAlpha}"
…… />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
//java code
//第三步:获取到已经更新了的defineAlpha数据,并更新 okAlpha 的 BindingAdapter方法
@BindingAdapter("okAlpha")
@JvmStatic
fun setOkAlpha(okBar: SeekBar, newValue: Float) {
okBar.alpha = newValue
}
//第二步:滑动触发 okAlpha 的 InverseBindingAdapter方法调用,获取属性okAlpha的值,更新defineAlpha的数据
@InverseBindingAdapter(attribute = "okAlpha")
@JvmStatic
fun getOkAlpha(okSeekBar: SeekBar): Float {
return okSeekBar.progress / 100f
}
@BindingAdapter("app:okAlphaAttrChanged")
@JvmStatic
fun setListeners(
okSeekBar: SeekBar,
attrChange: InverseBindingListener
) {
// Set a listener for click, focus, touch, etc.
okSeekBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
//第一步:监听滑动,会触发 okAlphaAttrChanged 的属性 okAlpha 的 InverseBindingAdapter方法
attrChange.onChange()
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
}
})
}
2.3.2 转换器
如果绑定到 View
对象的变量需要设置格式、转换或更改后才能显示,则可以使用 Converter
对象。较简单。
2.3.3 使用双向数据绑定的无限循环
使用双向数据绑定时,请注意不要引入无限循环。当用户更改特性时,系统会调用使用@InverseBindingAdapter
注释的方法,并且该值将分配给后备属性。继而调用使用 @BindingAdapter
注释的方法,从而触发对使用 @InverseBindingAdapter
注释的方法的另一个调用,依此类推。
因此,通过比较使用 @BindingAdapter
注释的方法中的新值和旧值,可以打破可能出现的无限循环。
2.3.4 双向特性
平台对部分控件提供对双向数据绑定的内置支持。比如 TextViewBindingAdapter。
2.4 布局绑定架构组件
数据绑定库可与架构组件(AndroidX 库包含的架构组件)无缝协作,进一步简化界面的开发。在后面的ViewModel和LiveData中再展开。
3. DataBinding文件说明
3.1 绑定类路径
3.1.1 JavaModel 视图层
3.1.2 JavaModel 数据层
路径:app/build/generated/source/kapt/buildTypes
3.1.3 Layout文件
3.2 绑定类作用
3.2.1 JavaModel 视图层
JavaModel 视图层(抽象类),继承自 ViewDataBinding,功能类似ViewBinding。参考 Jetpack ViewBinding。
3.2.2 JavaModel 数据层
JavaModel 数据层,继承自 视图层,添加的数据绑定的功能。
- BR:绑定属性的字段。
- DataBindingComponent:绑定类作用域。
- DataBinderMapperImpl:
- androidx.databinding 路径下:全局缓存添加器。
- com.xx.xx(包名)路径下:全局缓存器。
- XxxBindingImpl:绑定类的字段更新策略,绑定类的生成单例策略等。
3.2.3 Layout文件
用于生成JavaModel(绑定类)。参考 Jetpack ViewBinding。
4. DataBinding原理分析
4.1 布局绑定类生成
4.2 数据绑定类生成
4.3 绑定功能的数据流向
直接来个最全的吧。双向绑定数据流向:更新数据刷新视图 vs 更新视图刷新数据。
例子代码:
//xml code
<layout ……>
<data>
<variable
name="checkModel"
type="com.kejiyuanren.jetpack.databinding.CheckModel " />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
……>
<CheckBox
android:checked="@={checkModel.check}"
…… />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
//java code
class CheckModel {
val check = ObservableBoolean(false)
}
//java 更新操作
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityDataBinding2Binding = DataBindingUtil.setContentView(
this,
R.layout.activity_data_binding2
)
//设置java模型
var checkModel = CheckModel()
binding.checkModel = checkModel
binding.click = ClickHandler(object : ClickListener{
override fun onClick() {
//辅助button,点击,执行反向选择设置
checkModel.check.set(!checkModel.check.get())
}
})
}
流程图示:
Jetpack DataBinding 数据流向 绑定数据更新
5.小结
DataBinding 催生了 MVVM,代码简洁松耦合。JetPack-Compose 才是大Boss,比DB还DB。不过目前还没有稳定版,期待……