数据绑定库是一种支持库,借助该库,您可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。优势:
View
与数据源直接绑定,声明式绑定;findViewById()
方法,去除了大量模版化代码;kotlin-android-extensions
(已废弃)相比,控件使用更加安全;在应用模块的 build.gradle
文件中添加 dataBinding
元素,如以下示例所示:
android {
...
dataBinding {
enabled = true
}
}
在layout的activity_main.xml布局文件中,使用layout标签;
<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
;
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
//设置根布局
setContentView(binding.root)
//获取View控件并赋值
binding.userName.text = "小明"
binding.userAge.text = "20岁"
}
}
上面的代码中生成了绑定的类ActivityMainBinding
,系统会为每个布局文件生成一个绑定类。默认情况下,类名称基于布局文件的名称,它会转换为 Pascal
大小写形式并在末尾添加 Binding
后缀。以上布局文件名为 activity_main.xml
,因此生成的对应类为 ActivityMainBinding
。此类包含从布局属性(例如,user
变量)到布局视图的所有绑定,并且知道如何为绑定表达式指定值。
获取绑定的类主要有两种方式
// ListItemBinding绑定类调用inflate方法
val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)
// 或者DataBindingUtil调用inflate方法,同时把布局文件Id,与viewGroup传入
val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)
表达式语言与托管代码中的表达式非常相似。您可以在表达式语言中使用以下运算符和关键字:
+ - / * %
+
&& ||
& | ^
+ - ! ~
>> >>> <<
== > < >= <=
(请注意,<
需要转义为 <
)instanceof
()
null
[]
?:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
以下的运算符是不支持的:
this
super
new
为方便起见,可使用 []
运算符访问常见集合,例如数组、列表、稀疏列表和映射。
<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
形式,而是必须写成 List
。
事件可以直接绑定到处理脚本方法,类似于为 Activity 中的方法指定 android:onClick
的方式。与 View
onClick
特性相比,一个主要优点是表达式在编译时进行处理,因此,如果该方法不存在或其签名不正确,则会收到编译时错误。
方法引用和监听器绑定之间的主要区别在于实际监听器实现是在绑定数据时创建的,而不是在事件触发时创建的。如果您希望在事件发生时对表达式求值,则应使用监听器绑定。
要将事件分配给其处理脚本,请使用常规绑定表达式,并以要调用的方法名称作为值。例如,请考虑以下布局数据对象示例:
class MyHandlers {
fun onClickFriend(view: View) { ... }
}
绑定表达式可将视图的点击监听器分配给 onClickFriend()
方法,如下所示:
<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>
数据绑定库提供了诸如导入、变量和包含等功能。通过导入功能,您可以轻松地在布局文件中引用类。通过变量功能,您可以描述可在绑定表达式中使用的属性。通过包含功能,您可以在整个应用中重复使用复杂的布局。
导入
通过导入功能,您可以轻松地在布局文件中引用类,就像在托管代码中一样。您可以在 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
变量:
<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
元素的直接子元素。
class User {
var name: String = ""
var age: Int = 0
}
修改activity_main.xml
,增加data
与variable
标签;
<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>
系统会为每个布局文件生成一个绑定类。默认情况下,类名称基于布局文件的名称,它会转换为 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)
//直接设置Data
binding.user = User().apply {
name = "小红"
age = 18
}
}
}
可观察性是指一个对象将其数据变化告知其他对象的能力。通过数据绑定库,您可以让对象、字段或集合变为可观察。
任何 plain-old
对象都可用于数据绑定,但修改对象不会自动使界面更新。通过数据绑定,数据对象可在其数据发生更改时通知其他对象,即监听器。可观察类有三种不同类型:对象、字段和集合。
当其中一个可观察数据对象绑定到界面并且该数据对象的属性发生更改时,界面会自动更新。
可观察字段
在创建实现 Observable
接口的类时要完成一些操作,但如果您的类只有少数几个属性,这样操作的意义不大。在这种情况下,您可以使用通用 Observable
类和以下特定于基元的类,将字段设为可观察字段:
ObservableBoolean
ObservableByte
ObservableChar
ObservableShort
ObservableInt
ObservableLong
ObservableFloat
ObservableDouble
ObservableParcelable
可观察集合
某些应用使用动态结构来保存数据。可观察集合允许使用键访问这些结构。当键为引用类型(如 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" />
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)
}
}
上面讲了数据绑定与可观察数据对象,那View控件变化时,我们能不能直接获取到数据呢?@={}
表示法(其中重要的是包含“=”符号)可接收属性的数据更改并同时监听用户更新。
修改布局
<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()
//searchBtn调用的方法
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)
//获取ViewModel
mViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
//将ViewModel传递到View中
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
同样可以实现;
绑定适配器负责发出相应的框架调用来设置值。例如,设置属性值就像调用 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方法
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)
)
//setImageUrl方法
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中写过多的代码,后期维护成本高;绑定适配器实用性也不是很大;