1. 什么是DataBinding
数据绑定库是一种支持库,借助该库,您可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。优势:
View 与数据源直接绑定,声明式绑定;- 在寻找View控件时,可以省去
findViewById() 方法,去除了大量模版化代码; - 与插件
kotlin-android-extensions (已废弃)相比,控件使用更加安全;
2. 使用入门
2.1 工程配置
在应用模块的 build.gradle 文件中添加 dataBinding 元素,如以下示例所示:
android {
...
dataBinding {
enabled = true
}
}
2.2 绑定布局
在layout的activity_main.xml布局文件中,使用layout标签;
<?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">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/user_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/user_age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/user_name" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
同时点击Android Studio 中的Build ->Make Project ;
2.3 获取绑定类,并进行相关操作
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.userName.text = "小明"
binding.userAge.text = "20岁"
}
}
上面的代码中生成了绑定的类ActivityMainBinding ,系统会为每个布局文件生成一个绑定类。默认情况下,类名称基于布局文件的名称,它会转换为 Pascal 大小写形式并在末尾添加 Binding 后缀。以上布局文件名为 activity_main.xml ,因此生成的对应类为 ActivityMainBinding 。此类包含从布局属性(例如,user 变量)到布局视图的所有绑定,并且知道如何为绑定表达式指定值。
获取绑定的类主要有两种方式
val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)
val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)
3. 绑定表达式
3.1 表达式语言
表达式语言与托管代码中的表达式非常相似。您可以在表达式语言中使用以下运算符和关键字:
- 算术运算符
+ - / * % - 字符串连接运算符
+ - 逻辑运算符
&& || - 二元运算符
& | ^ - 一元运算符
+ - ! ~ - 移位运算符
>> >>> << - 比较运算符
== > < >= <= (请注意,< 需要转义为 < ) instanceof - 分组运算符
() - 字面量运算符 - 字符、字符串、数字、
null - 类型转换
- 方法调用
- 字段访问
- 数组访问
[] - 三元运算符
?:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
以下的运算符是不支持的:
3.2 集合使用
为方便起见,可使用 [] 运算符访问常见集合,例如数组、列表、稀疏列表和映射。
<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> 。
3.3 方法引用
事件可以直接绑定到处理脚本方法,类似于为 Activity 中的方法指定 android:onClick 的方式。与 View onClick 特性相比,一个主要优点是表达式在编译时进行处理,因此,如果该方法不存在或其签名不正确,则会收到编译时错误。
方法引用和监听器绑定之间的主要区别在于实际监听器实现是在绑定数据时创建的,而不是在事件触发时创建的。如果您希望在事件发生时对表达式求值,则应使用监听器绑定。
要将事件分配给其处理脚本,请使用常规绑定表达式,并以要调用的方法名称作为值。例如,请考虑以下布局数据对象示例:
class MyHandlers {
fun onClickFriend(view: View) { ... }
}
绑定表达式可将视图的点击监听器分配给 onClickFriend() 方法,如下所示:
<?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>
3.4 导入、变量和包含
数据绑定库提供了诸如导入、变量和包含等功能。通过导入功能,您可以轻松地在布局文件中引用类。通过变量功能,您可以描述可在绑定表达式中使用的属性。通过包含功能,您可以在整个应用中重复使用复杂的布局。
-
导入 通过导入功能,您可以轻松地在布局文件中引用类,就像在托管代码中一样。您可以在 data 元素使用多个 import 元素,也可以不使用。以下代码示例将 View 类导入到布局文件中: <data>
<import type="android.view.View"/>
</data>
-
变量 您可以在 data 元素中使用多个 variable 元素。每个 variable 元素都描述了一个可以在布局上设置、并将在布局文件中的绑定表达式中使用的属性。以下示例声明了 user 、image 和 note 变量: <data>
<import type="android.graphics.drawable.Drawable"/>
<variable name="user" type="com.example.User"/>
<variable name="image" type="Drawable"/>
<variable name="note" type="String"/>
</data>
-
包含 通过使用应用命名空间和特性中的变量名称,变量可以从包含的布局传递到被包含布局的绑定。以下示例展示了来自 name.xml 和 contact.xml 布局文件的被包含 user 变量: <?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}"/>
<include layout="@layout/contact"
bind:user="@{user}"/>
</LinearLayout>
</layout>
数据绑定不支持 include 作为 merge 元素的直接子元素。
4.数据绑定
4.1 数据对象,User为例
class User {
var name: String = ""
var age: Int = 0
}
4.2 布局中声明
修改activity_main.xml ,增加data 与variable 标签;
<?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>
<variable
name="user"
type="com.hjq.viewbinding.model.User" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/user_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{"名字:"+user.name}'
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/user_age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{"年龄:"+String.valueOf(user.age)+"岁"}'
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/user_name" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
4.3 绑定数据
系统会为每个布局文件生成一个绑定类。默认情况下,类名称基于布局文件的名称,它会转换为 Pascal 大小写形式并在末尾添加 Binding 后缀。以上布局文件名为 activity_main.xml ,因此生成的对应类为 ActivityMainBinding 。此类包含从布局属性(例如,user 变量)到布局视图的所有绑定,并且知道如何为绑定表达式指定值。建议的绑定创建方法是在扩充布局时创建,如以下示例所示:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.user = User().apply {
name = "小红"
age = 18
}
}
}
4.4 可观察数据对象
可观察性是指一个对象将其数据变化告知其他对象的能力。通过数据绑定库,您可以让对象、字段或集合变为可观察。
任何 plain-old 对象都可用于数据绑定,但修改对象不会自动使界面更新。通过数据绑定,数据对象可在其数据发生更改时通知其他对象,即监听器。可观察类有三种不同类型:对象、字段和集合。
当其中一个可观察数据对象绑定到界面并且该数据对象的属性发生更改时,界面会自动更新。
-
可观察字段 在创建实现 Observable 接口的类时要完成一些操作,但如果您的类只有少数几个属性,这样操作的意义不大。在这种情况下,您可以使用通用 Observable 类和以下特定于基元的类,将字段设为可观察字段:
-
可观察集合 某些应用使用动态结构来保存数据。可观察集合允许使用键访问这些结构。当键为引用类型(如 String )时,ObservableArrayMap 类非常有用,如以下示例所示: ObservableArrayMap<String, Any>().apply {
put("firstName", "Google")
put("lastName", "Inc.")
put("age", 17)
}
在布局中,可使用字符串键找到地图,如下所示: <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"/>
-
可观察对象 实现 Observable 接口的类允许注册监听器,以便它们接收有关可观察对象的属性更改的通知。 Observable 接口具有添加和移除监听器的机制,但何时发送通知必须由您决定。为便于开发,数据绑定库提供了用于实现监听器注册机制的 BaseObservable 类。实现 BaseObservable 的数据类负责在属性更改时发出通知。具体操作过程是向 getter 分配 Bindable 注释,然后在 setter 中调用 notifyPropertyChanged() 方法,如以下示例所示: 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)
}
}
4.5 双向数据绑定 上面讲了数据绑定与可观察数据对象,那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>
<variable
name="viewModel"
type="com.hjq.viewbinding.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/user_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{"名字:"+viewModel.user.name}'
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/user_age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{"年龄:"+String.valueOf(viewModel.user.age)+"岁"}'
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/user_name" />
<EditText
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@={viewModel.searchTextField}"
app:layout_constraintEnd_toStartOf="@+id/searchBtn"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/user_age" />
<Button
android:id="@+id/searchBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> viewModel.searchText()}"
android:text="搜索"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/user_age" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
-
MainViewModel 代码如下 class MainViewModel : ViewModel() {
val searchTextField = ObservableField<String>("")
val searchTextLiveData = MutableLiveData<String>("")
var imageUrl = ""
var user = User()
fun searchText() {
val searchText = searchTextField.get()
if (TextUtils.isEmpty(searchText)) {
ToastUtils.showShort("请输入要搜索的内容")
return
}
ToastUtils.showShort("你输入的内容是:${searchText}")
}
}
-
MainActivity 中的代码
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var mViewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
mViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
binding.viewModel = mViewModel
initData()
mViewModel.searchTextField.addOnPropertyChangedCallback(object :
Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable, propertyId: Int) {
val changeText = mViewModel.searchTextField.get()
LogUtils.d("Field数据发生了改变:${changeText}")
}
})
mViewModel.searchTextLiveData.observe(this, { text ->
LogUtils.d("LiveData数据发生了改变:${text}")
})
}
private fun initData() {
mViewModel.user = User().apply {
name = "小明"
age = 18
}
}
}
上面我们就用ObservableField 和@={} 表示法,实现了数据的双向绑定; LiveData 同样可以实现;
5. 绑定适配器
绑定适配器负责发出相应的框架调用来设置值。例如,设置属性值就像调用 setText() 方法一样。再比如,设置事件监听器就像调用 setOnClickListener() 方法。使用BindingAdapter 注解;
数据绑定库允许您通过使用适配器指定为设置值而调用的方法、提供您自己的绑定逻辑,以及指定返回对象的类型。
比如我们要加载一张网络图片,我们可以直接修改布局
<ImageView
android:id="@+id/user_image"
android:layout_width="200dp"
android:layout_height="200dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/searchBtn"
app:loadError="@{@drawable/ic_launcher_background}"
app:loadImageViewByUrl="@{viewModel.imageUrl}" />
定义BindingAdapters 类,将app:loadError ,app:loadImageViewByUrl ,与View控件,传入到setImageUrl 方法中:
object BindingAdapters {
@BindingAdapter(value = ["app:loadImageViewByUrl", "app:loadError"], requireAll = false)
@JvmStatic
fun setImageUrl(imageView: ImageView, url: String?, placeHolder: Drawable?) {
if (url == null) {
imageView.setImageDrawable(placeHolder);
} else {
Glide.with(imageView).load(url).error(placeHolder).into(imageView)
}
}
}
代码中传入一个网络图片的地址
mViewModel.imageUrl = "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.jj20.com%2Fup%2Fallimg%2F1115%2F102621102550%2F211026102550-7-1200.jpg&refer=http%3A%2F%2Fimg.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1654605476&t=3d8c3d2843a558de05bf01196cc8901f"
总结,绑定适配器就是在View 布局中指定相关的参数,实现一个静态方法而已;
上面的代码与下面的代码效果是一样:
//定义ImageView
<ImageView
android:id="@+id/user_image"
android:layout_width="200dp"
android:layout_height="200dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/searchBtn"
/>
setImageUrl(
imageView = binding.userImage,
url = "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.jj20.com%2Fup%2Fallimg%2F1115%2F102621102550%2F211026102550-7-1200.jpg&refer=http%3A%2F%2Fimg.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1654605476&t=3d8c3d2843a558de05bf01196cc8901f",
placeHolder = resources.getDrawable(R.drawable.ic_launcher_background)
)
private fun setImageUrl(imageView: ImageView, url: String?, placeHolder: Drawable?) {
if (url == null) {
imageView.setImageDrawable(placeHolder);
} else {
Glide.with(imageView).load(url).error(placeHolder).into(imageView)
}
}
总结
- 综上,
DataBinding 可以进行数据绑定,可以直接调用代码的方法,还可以自己绑定适配器,减少了大量findViewById 的方法等好处;但不建议在View中写过多的代码,后期维护成本高;绑定适配器实用性也不是很大; - 代码示例
|