Jetpack--Data Binding
旧文填坑 Data Binding
资料来源:
https://developer.android.com/topic/libraries/data-binding
更新
1
2
320.02.19 初始化
20.02.23 初步刷完文档
20.04.28 添加几个坑
导语
- 重刷 Android,最近这两年 Android 变化还真大,迁移到了 Kotlin ,当初接触的 data binding 和架构组件都成了 jetpack 的一部分.
- 趁着这段时间刷刷 jetpack
水几篇博客. - 初始仅仅是速描官方文档,随时添加内容.
概览
- 初始上手 Android 时候最烦的就是
findViewById
每一个子 view 都要来一遍.处理监听事件也非常麻烦. - 即使导入了 MVP 的架构,view 层还是存在大量冗余代码,这几年 kotlin 解决了一部分,剩下的可以交给 jetpack 了.
- data binding 可以把可观察的数据和视图绑定,当数据变化时自动更新视图.但是视图部分逻辑在 data binding 中被放到了 xml 中,而且调试非常麻烦.
- 如果你只是需要废掉
findViewById
, 请尝试 视图绑定
基础
启用 data binding,在应用的 build.gradle 文件中添加 dataBinding
1
2
3
4
5
6android {
...
dataBinding {
enabled = true
}
}Android studio 的支持
- 语法突出显示
- 标记表达式语言语法错误
- XML 代码完成
- 包括导航(如导航到声明)和快速文档在内的引用
对于一个这样的类(kt 真简结)
1
data class User(val firstName: String, val lastName: String)
布局声明:
要在
<data>
之间声明.要声明变量名和类型
布局中通过
@{}
访问变量.并且可以直接访问属性.默认会检查 Null
例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<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}"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"/>
</LinearLayout>
</layout>
绑定数据
每个布局文件都会生成一个绑定类.默认情况下,绑定类的名称称基于布局文件的名称驼峰大小写 + Binding 后缀.例如布局文件名 activity_main.xml,绑定类ActivityMainBinding.
例:
1
2
3val binding: ActivityMainBinding = DataBindingUtil.setContentView(
this, R.layout.activity_main)
binding.user = User("Test", "User")Fragment ListView 或 RecyclerView 适配器中使用数据绑定.
1
2
3val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)
// or
val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)
所以使用 databinding 的流程就是:
- 设计可观察类
- 声明 xml 布局
- 生成加载 xml 的绑定类实例.
- 实例化数据类,绑定.剩下的显示数据等系统会搞定.
xml 访问
xml 中支持的表达式(非常不推荐在 xml 中堆积太多逻辑)
- 算术运算符 + - / * %
- 字符串连接运算符 +
- 逻辑运算符 && ||
- 二元运算符 & | ^
- 一元运算符 + - ! ~
- 移位运算符 >> >>> <<
- 比较运算符 == > < >= <=(请注意,< 需要转义为 <)
- instanceof
- 分组运算符 ()
- 字面量运算符 - 字符、字符串、数字、null
- 类型转换
- 方法调用
- 字段访问
- 数组访问 []
- 三元运算符 ?:
xml 不支持的表达式
- this
- super
- new
- 显式泛型调用
空合并运算符
??
:android:text="@{user.displayName ?? user.lastName}"
等价于android:text="@{user.displayName != null ? user.displayName : user.lastName}"
可使用 [] 运算符访问常见集合,例如数组 列表 稀疏列表和映射.有一点限制是
<
必须转义成<
才能在 xml 中识别字符串:
'@{map["firstName"]}'
或者android:text="@{map[```firstName```]}"
.
事件处理
databinding 可以在 xml 上分派事件.一般的 View.OnClickListener -> onClick()
- SearchView -> android:onSearchClick
- ZoomControls -> android:onZoomIn
- ZoomControls -> android:onZoomOut
处理事件可以有方法引用或者监听器绑定.
方法引用:大致是直接在 onClick() = 处理的方法.表达式中的方法名必须与监听器对象中的方法名完全一致.好处是编译器就处理.
1
2
3class MyHandlers {
fun onClickFriend(view: View) { ... }
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<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 表达式.要求 Gradle 2.0 版及更高版本.允许运行任意数据绑定表达式.
1
2
3class Presenter {
fun onSaveClick(task: Task){}
}1
2
3
4
5
6
7
8
9
10
11
12
<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>
...上面例子中也可以写成
android:onClick="@{(view) -> presenter.onSaveClick(task)}"
传入 View 参数:
fun onSaveClick(view: View, task: Task){}
和android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
多个参数:
fun onCompletedChanged(task: Task, completed: Boolean){}
和android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}"
监听的事件返回类型不是 void ,lambda 也必须返回对应的类型.
监听器表达式功能非常强大,具体的业务逻辑必须在对应方法中而不是堆积在 xml 中.
坑: 有一些绑定的属性,Android Studio 会提示不存在,但是实际上却又可用.包括并不限于
1
2
3
4"android:beforeTextChanged",
"android:onTextChanged",
"android:afterTextChanged",
"android:textAttrChanged
可观察对象
可观察性是指一个对象将其数据变化通知给其他对象的能力.数据变了通知 UI 更新.
从数据类型上看分为 对象 字段 集合.
可观察字段: 当只需要少数一些属性可观察时,不需要继承实现 Observable 接口.只需要把属性改为 ObservableXXX 类型即可.访问时使用 set/get 方法.(kt 中相当于直接访问)
- ObservableBoolean
- ObservableByte
- ObservableChar
- ObservableShort
- ObservableInt
- ObservableLong
- ObservableFloat
- ObservableDouble
- ObservableParcelable
可观察集合
- ObservableArrayMap 当键值为引用类型.
- ObservableArrayList 键值为整数
- 都可以直接在 xml 中直接访问.
可观察类
如果非要从头设计数据结构.我们可以继承并实现 Observable 接口.get 使用 @Bindable 修饰.set 则在值更新后调用 notifyPropertyChanged() 通知 UI 更新.(kt 中因为语法要绕一点)
数据绑定会在模块包中生成一个名为 BR 的类,该类包含用于数据绑定的资源的 ID.在编译期间,Bindable 注释会在 BR 类文件中生成一个条目,这样在 notifyPropertyChanged() 才会有 BR.x .所以如果 notifyPropertyChanged() 报错找不到 BR.x 先编译一下工程.
例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14class User : BaseObservable() {
var firstName: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.firstName)
}
var lastName: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.lastName)
}
}
生成绑定类
默认情况下,系统会为每个布局文件生成一个绑定类.类名称基于布局文件的名称驼峰写法 + Binding 后缀.例如 activity_main.xml 对应类为 ActivityMainBinding.绑定类包含从布局属性到布局视图的所有绑定,并且知道如何为绑定表达式指定值.
绑定类的 inflate() 方法.有两个重载方法.
1
2
3val binding: MyLayoutBinding = MyLayoutBinding.inflate(layoutInflater)
//增加 ViewGroup 参数
val binding: MyLayoutBinding = MyLayoutBinding.inflate(getLayoutInflater(), viewGroup, false)布局使用其他机制扩充的,单独绑定.
1
val binding: MyLayoutBinding = MyLayoutBinding.bind(viewRoot)
如无法预先知道绑定类型,可以使用 DataBindingUtil 类创建绑定类.
1
2val viewRoot = LayoutInflater.from(this).inflate(layoutId, parent, attachToParent)
val binding: ViewDataBinding? = DataBindingUtil.bind(viewRoot)使用 DataBindingUtil 类 inflate 方法,在 Fragment ListView 或 RecyclerView 常用.
1
val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)
自定义绑定类名
绑定类的默认规则上文已经提到了.有时候 xml 的名称驼峰写法 + Binding.默认位置在 模块包.databinding 下.
调整 data 元素的 class 特性,可以自定义名称和路径.
自定义类名,生成 ContactItem 绑定类.
1
2
3<data class="ContactItem">
…
</data>自定义路径,在类名前添加句点和前缀,示例即在模块包生成.
1
2
3<data class=".ContactItem">
…
</data>也可以使用完整软件包名称来生成绑定类.
1
2
3<data class="com.example.ContactItem">
…
</data>
自定义 set() 方法
在 xml 中直接绑定调用函数是很爽,但是总有一些是默认方法覆盖不到的.例如传入 url 利用 Glide 设置 ImageView 显示,ImageView 就不可能有对应的方法.这个时候就需要我们自定义逻辑.
自定义的 set() 适配器方法
必须是 BindingAdapter 注释的静态绑定适配器方法.(kt 中沒有静态方法,所以如果是在普通类中需要 @JvmStatic 修饰).
注释没有声作用域的默认调用是在
app:xx
下.如@BindingAdapter("android:paddingLeft")
,调用时就是app:imageUrl="@{venue.imageUrl}"
.例
1
2
3
4
5
6
7
8
9
10
11
12
13
fun LoadIcon(imageView: ImageView, appInfo: AppInfo) {
val options = RequestOptions()
.error(R.drawable.sym_def_app_icon)
.placeholder(R.drawable.sym_def_app_icon)
val uri = Uri.parse("android.resource://" + appInfo.packageName + "/" + appInfo.icon)
Glide.with(imageView.context)
.applyDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
.load(uri)
.apply(options)
.into(imageView)
}
在 xml 中直接调用
app:loadIcon="@{appInfo}"
双向数据绑定
databinding 不仅提供了数据到 UI 的绑定,同时还可以反向绑定.即 UI 改变时数据也跟着变动.例如一个 bool 类型数据绑定到了一个 Button 上,我们声明了双向的数据绑定,当 Button 的状态改变时,对应数据的值也会改变.
实现双向绑定非常简单,在 xml 使用变量时由
@
改为@=
即可.双向数据绑定对数据类型有要求,如果是系统提供的 ObservableXXX ,无需修改类中已经实现了,但是我们继承并实现 Observable 接口的类,必须实现 setter 方法,且调用 notifyPropertyChanged() 通知 UI.
转换器
当需要双向绑定的数据类型不匹配时,可以自定义 Converter 对象,完成对应的数据转换.
例
1
2
3
4<EditText
android:id="@+id/birth_date"
android:text="@={Converter.dateToString(birthDate)}"
/>例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16object Converter {
fun dateToString(
view: EditText, oldValue: Long,
value: Long
): String {
// Converts long to String.
}
fun stringToDate(
view: EditText, oldValue: String,
value: String
): Long {
// Converts String to long.
}
}
还可以将双向绑定和自定义方法结合起来,详情见 使用自定义特性的双向数据绑定.
与 RecyclerView 连用
详情见 Jetpack-当 RecycleView 遇到 Databinding
简单描述一下,不再上代码了.参考了 借助 android databinding 框架,逃离 adapter 和 viewholder 的噩梦 (1)
最主要的工作是要实现一个自定义的 Adapter 继承自 RecyclerView.Adapter.实现 3 个主要的方法.
- public ViewHolder onCreateViewHolder(ViewGroup parent,int viewType)//创建返回ViewHolder实例
- public void onBindViewHolder(ViewHolder holder,int pisition)//数据与界面绑定
- public int getItemCount() // 返回数据的数量
我们先来看看 RecyclerView.Adapter 创建一个新 Item 时简单的工作流程.
- 调用 onCreateViewHolder 创建一个 ViewHolder 持有 View.
- 创建布局时,调用 onBindViewHolder 将 item 的数据显示到 View 上.
对比 databinding 的流程.与 RecyclerView 配合有以下的想法.
- onCreateViewHolder 不再返回 ViewHolder 了,改为返回一个根据 xml 布局生成的绑定类.
- onBindViewHolder 不再是一个个具体的字段设定.改为将数据和绑定类绑定.具体字段的映射到 UI 已经定义在了绑定类中.
问题
- 不同的 xml 的绑定类默认都是不同类型的,一个 item 对应一个绑定类工作量太大.
- 每个 item 对应的数据类都不相同.无法用简单的代码实现绑定.
第一个问题:
- 在 onCreateViewHolder 每个具体的 item 获取到 xml.利用 DataBindingUtil 类的 inflate 生成绑定类.
- 绑定类的类型声明为 ViewDataBinding,ViewDataBinding 是所有绑定类的基类.
- item 要实现一个 getType() 返回 xml 的接口,同时重写 getItemViewType 根据 position 调用 getType() 返回 xml 布局.
第二个问题
- 针对任意布局运行的 Adapter 并不知道特定绑定类,但调用 onBindViewHolder() 时,仍必须指定绑定值.
- 需要用到 databinding 的动态变量.setVariable() 方法.
- 例:
mbinding.setVariable(BR.item, item)
,这样有个要求 xml 声明变量时都必须声明为item
与架构组件配合
LiveData
LiveData 可以感知组件的生命周期,有一堆的好处,不再叙述😂.
Android Studio 3.1 以后可以使用 LiveData 替换可观察字段,即上文那些 ObservableXXX.
因为 LiveData 对组件生命周期感知,需要指定生命周期的所有者.
例:
1
2
3
4
5
6
7
8
9class ViewModelActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Inflate view and obtain an instance of the binding class.
val binding: UserBinding = DataBindingUtil.setContentView(this, R.layout.user)
// 生命周期的所有者
binding.setLifecycleOwner(this)
}
}
坑: Livedate 下kotlin 的 boolean 类型不支持双向绑定通知.
- 例如,根据 boolean 变量的值显示或者隐藏 View.
android:checked="@={flag}"
android:visibility="@{flag ? View.VISIBLE : View.GONE}"
- kotlin 的 Boolean 类型,当 flag 值变化时,View 没有任何变化.但是当 flag 是 java 类型的 Boolean 时,view 可以正常显示隐藏.
- 原因未知,目前暂时以 ObservableBoolean 代替,可以正常使用…
- 例如,根据 boolean 变量的值显示或者隐藏 View.
ViewModel
- 与 ViewModel 的配合有点奇怪.等刷完 jetpack 的 ViewModel 后再回头刷.