本篇文章以这个示例BasicSample来研究学习Android DataBinding的基本用法。
开始使用
首先请在应用模块的 build.gradle 文件中添加 dataBinding 元素,如下所示
android {
...
dataBinding {
enabled = true
}
}
然后将常规的布局文件转化为数据绑定布局文件
<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>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
- 使用
<layout>
标签包裹常规的布局文件 - 添加
<data>
标签,在<data>
标签内添加布局变量和布局表达式(不是必须的)
可以使用Android Studio的快捷功能将普通布局转化为数据绑定布局
将鼠标悬停在布局文件的根元素上,点击左侧出现的黄色小灯,然后选择Convert to Data binding layout
。
BasicSample如下所示
本篇文章主要学习一下几点
- 布局变量和布局表达式
- 观察能力,通过可观察的变量,LiveData 和可观察的类实现观察能力
- 绑定适配器,绑定方法和绑定转换器
- 和ViewModels无缝结合
布局变量和布局表达式
使用布局变量和布局表达式可以少写很多样板代码和重复代码。布局变量和布局表达式将一些UI操作从activities and fragments移到XML布局文件中。
例如,我们要给一个TextView动态设置text,我们会在activity中这样写
TextView textView = findViewById(R.id.name);
textView.setText(user.name);
但是使用数据绑定,我们可以直接在XML布局文件中直接将布局变量
赋值给TextView的text
属性
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.name}" />
具体实现
首先定义我们用到的数据类
data class ObservableFieldProfile(
val name: String,
val lastName: String,
val likes: ObservableInt
)
修改布局文件
<?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>
<!--注释1处-->
<variable
name="user"
type="com.example.android.databinding.basicsample.data.ObservableFieldProfile" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.android.databinding.basicsample.ui.BlogDemoActivity">
<TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.name}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
在注释1处,我们在data
标签内定义了user变量,类型是ObservableFieldProfile,然后我们给TextView的属性赋值为user.name
android:text="@{user.name}"
修改布局文件以后,我们点击一下Make project
,数据绑定框架会为我们生成一些用用的类。
然后我们还要修改Activity,如下所示
class BlogDemoActivity : AppCompatActivity() {
//定义我们的数据
private val fieldProfile = ObservableFieldProfile("Ada", "Lovelace",
ObservableInt(0))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//注释1处,将Activity和布局文件绑定
val binding: ActivityBlogDemoBinding = DataBindingUtil
.setContentView(this, R.layout.activity_blog_demo)
//注释2处,为布局变量user赋值
binding.user = fieldProfile
}
}
这个ActivityBlogDemoBinding
类,就是数据绑定框架帮我们生成的中间类,我们看见生成的类名和我们的布局文件名字是对应的。布局文件名是activity_blog_demo
,生成的绑定类名是ActivityBlogDemoBinding
。可以看到就是把布局文件名转成驼峰命名然后在后面加上Binding。
我们也可以自定义生成的绑定类名,如下所示:
<data class="ActivityMyBlogDemoBinding">
</data>
我们运行一下可以看到TextView的text
显示是Ada
。
观察能力
当数据改变的时候为了能自动实现UI更新,需要将可观察的对象和View的属性绑定。有三种机制可以实现这个目标:可观察的变量,LiveData 和可观察的类。
可观察的变量(Observable fields)
数据绑定框架提供了像ObservableInt,ObservableBoolean来替代原始数据类型,使其具备可观察能力。提供了ObservableField来替代引用数据类型,使其具备可观察能力。
我们在上面定义的ObservableFieldProfile类,它的likes
就是ObservableInt类型的。
我们将likes
绑定到TextView的text
属性,然后手动改变likes
,我们观察TextView的text
是否也跟着改变。
<TextView
android:id="@+id/tvLikes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{Integer.toString(user.likes)}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btnChangeLike"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Change like"
android:textAllCaps="false" />
这里有一点要注意,
android:text="@{Integer.toString(user.likes)}"
因为likes
就是ObservableInt类型,我们使用的时候会自动转化成int类型,将likes
赋值给text属性的时候,需要将likes
转化成字符串类型。这里我们可以看到,我们可以直接在布局文件中不需要导入就可以使用一些常见的类,Integer,String,等等,具体哪些类有待完善。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//注释1处,将Activity和布局文件绑定
val binding: ActivityBlogDemoBinding = DataBindingUtil
.setContentView(this, R.layout.activity_blog_demo)
binding.btnChangeLike.setOnClickListener {
//增加likes
fieldProfile.likes.set(fieldProfile.likes.get() + 1)
}
}
我们点击按钮发现,likes增加的时候,TextView的text
也会跟着变。
LiveData
LiveData是Android Architecture Components 中的一个可观察者具有生命周期感知能力。和可观察的变量相比,LiveData的优势是支持 Transformations并且能和其他组件和库一起配合使用,例如Room和WorkManager。
我们定义一个类ProfileLiveDataViewModel
。ViewModel是一个用来为Activity或者Fragment准备和管理数据的类。ViewModel可以通过LiveData来提供数据。关于ViewModel和LiveData可以参考ViewModel和LiveData 使用。
class ProfileLiveDataViewModel : ViewModel() {
private val _likes = MutableLiveData(0)
val likes: LiveData<Int> = _likes //暴露一个不可更改的LiveData
//改变_likes
fun onLike() {
_likes.value = (_likes.value ?: 0) + 1
}
}
看看布局文件的改变,只保留了关键信息
<data>
<!--注释1处-->
<variable
name="viewmodel"
type="com.example.android.databinding.basicsample.data.ProfileLiveDataViewModel"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.android.databinding.basicsample.BlogDemo2Activity">
<TextView
android:id="@+id/tvLikes"
android:text="@{Integer.toString(viewmodel.likes)}"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
声明了布局变量viewmodel,类型是ProfileLiveDataViewModel。将viewmodel的likes赋值给TextView的text
属性。
Activity的改变
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityBlogDemo2Binding>(this, R.layout.activity_blog_demo2)
//获取viewModel对象
val viewModel = ViewModelProvider(this).get(ProfileLiveDataViewModel::class.java)
// 赋值给布局变量
binding.viewmodel = viewModel
//注释1处,
//给binding设置生命周期持有者
//LiveData需要生命周期持有者,因为LiveData数据改变的时候只会通知生命周期处于活动状态的观察者
binding.lifecycleOwner = this
btnChangeLike.setOnClickListener {
viewModel.onLike()
}
}
//...
获取viewmodel赋值给布局变量,注释1处,我们要给binding设置生命周期持有者。LiveData需要生命周期持有者,因为LiveData数据改变的时候只会通知生命周期处于活动状态的观察者。
点击btnChangeLike也是可以改变TextView的text。
可观察的类(Observable classes)
为了更加灵活可控,你可以实现一个完整的可观察的类来决定何时更新哪些变量。这允许你创建变量之间的依赖关系并且只更新部分UI。
/**
* A ViewModel that is also an Observable, to be used with Data Binding.
*/
open class ObservableViewModel : ViewModel(), Observable {
//属性改变注册器
private val callbacks: PropertyChangeRegistry = PropertyChangeRegistry()
//添加观察者
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
callbacks.add(callback)
}
//移除观察者
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
callbacks.remove(callback)
}
/**
* 通知观察者,当前对象的所有属性发生了该变。
*/
fun notifyChange() {
callbacks.notifyCallbacks(this, 0, null)
}
/**
* 通知观察者指定的属性发生了改变。
* 改变的属性的getter方法应该使用Bindable来注解,用于在`BR`类中生成一个field。
*
* @param fieldId 为可绑定的属性在BR类中生成的field
*/
fun notifyPropertyChanged(fieldId: Int) {
callbacks.notifyCallbacks(this, fieldId, null)
}
}
我们自定义了一个ObservableViewModel类并且继承了Observable。所以ObservableViewModel是一个可观察者。我们还定义了添加、移除和通知观察者的方法。
要注意的是:改变的属性的getter方法应该使用Bindable来注解,用于在BR
类中生成一个field
,如下所示。
//继承ObservableViewModel类
class ProfileObservableViewModel : ObservableViewModel() {
val likes = ObservableInt(0)
fun onLike() {
likes.increment()
notifyPropertyChanged(BR.popularity)
}
//注释1处
@Bindable
fun getPopularity(): Popularity {
return likes.get().let {
when {
it > 9 -> Popularity.STAR
it > 4 -> Popularity.POPULAR
else -> Popularity.NORMAL
}
}
}
}
//流行度
enum class Popularity {
NORMAL,//正常
POPULAR,//流行
STAR//明星
}
//定义ObservableInt的扩展函数
private fun ObservableInt.increment() {
set(get() + 1)
}
在注释1处,使用Bindable注解getPopularity方法。
@Bindable
fun getPopularity(): Popularity {
}
点击Make project
,可以在生成的BR类中看到popularity的field
。
public class BR {
//...
public static final int popularity = 2;
}
在上面的例子中,当onLike
方法被调用的时候,likes
会增加并且popularity
属性也会收到数据改变的通知(popularity的值依赖likes)。getPopularity
方法会被数据绑定框架调用,返回一个可能发生了变化的新值。
Bindable
注解应该被应用到Observable
类中的所有getter方法上。Bindable会在BR类中生成一个field
来标识Observable
类中发生改变的属性。
绑定适配器,绑定方法和绑定转换器
绑定适配器(Binding adapters)
绑定适配器可以用来自定义布局属性。例如你可以为ProgressBar定义app:progressTint
属性,根据外部的值来改变进度条的颜色。
@BindingAdapter("app:progressTint")
@JvmStatic fun tintPopularity(view: ProgressBar, popularity: Popularity) {
val color = getAssociatedColor(popularity, view.context)
view.progressTintList = ColorStateList.valueOf(color)
}
//根据流行度返回不同的颜色
private fun getAssociatedColor(popularity: Popularity, context: Context): Int {
return when (popularity) {
Popularity.NORMAL -> context.theme.obtainStyledAttributes(
intArrayOf(android.R.attr.colorForeground)).getColor(0, 0x000000)
Popularity.POPULAR -> ContextCompat.getColor(context, R.color.popular)
Popularity.STAR -> ContextCompat.getColor(context, R.color.star)
}
}
在布局文件中使用
<ProgressBar
app:progressTint="@{viewmodel.popularity}" />
使用绑定适配器可以将在activity中的UI调用移到静态方法中,利于封装。
你也可以在绑定适配器中使用多个属性。
@BindingAdapter(value = ["app:progressScaled", "android:max"], requireAll = true)
@JvmStatic
fun setProgress(progressBar: ProgressBar, likes: Int, max: Int) {
progressBar.progress = (likes * max / 5).coerceAtMost(max)
}
在这个绑定适配器方法中,要求传入两个参数,likes
,max
,对应布局文件中的"app:progressScaled"
属性和"android:max"
属性。
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:max="@{100}"
app:hideIfZero="@{viewmodel.likes}"
app:progressScaled="@{viewmodel.likes}"
app:progressTint="@{viewmodel.popularity}"
tools:progressBackgroundTint="@android:color/darker_gray" />
绑定方法和绑定转换器(Binding methods and binding converters)
当绑定适配器方法很简单的时候,你可以使用绑定方法和绑定转换器来减少代码。你可以在 official guide中查阅细节。
例如,当一个属性值需要传递给一个适配器方法:
@BindingAdapter("app:srcCompat")
@JvmStatic fun srcCompat(view: ImageView, @DrawableRes drawableId: Int) {
view.setImageResource(drawable)
}
上面的适配器方法可以使用绑定方法替代。绑定方法可以被添加到工程中的任意的类中。
@BindingMethods(
BindingMethod(type = ImageView::class,
attribute = "app:srcCompat",
method = "setImageResource"))
class MyBindingMethods
我们将这个绑定方法添加到MyBindingMethods上。BindingMethod注解的三个属性我们也留意一下:
- type = ImageView::class,属性关联的View类
- attribute = "app:srcCompat",属性名称,所有的android属性使用
android:
命名空间,应用自定义属性不用使用命名空间。 - method = "setImageResource",被数据绑定框架调用,用来设置属性值的方法。
绑定转换器(使用的时候要小心)
在这个例子中,我们展示了一个View依赖一个数字是否为0来决定View是否可见。有很多方法来实现这个功能。本例会展示两种实现方式。
我们的目标是根据likes
的值来控制View的显示或者隐藏。
android:visibility="@{viewmodel.likes}"
这种方式是不能正常工作的。likes
是一个整数,View的visibility
属性也是一个整数 (VISIBLE, GONE and INVISIBLE are 0, 4 and 8 respectively)。所以直接这样使用的话, 可以编译运行,但是结果肯定不是我们想要的。
一个可能的解决方式如下:
android:visibility="@{viewmodel.likes == 0 ? View.GONE : View.VISIBLE}"
布局表达式,增加复杂度不利于阅读和维护,不推荐使用。
第一种实现方式,就是使用绑定转换器(不推荐的方法)
object ConverterUtil {
@JvmStatic fun isZero(number: Int): Boolean {
return number == 0
}
}
我们首先在ConverterUtil类中定义一个isZero
方法,然后在布局文件中导入ConverterUtil类
<data>
<import type="com.example.android.databinding.basicsample.util.ConverterUtil" />
...
</data>
//使用
android:visibility="@{ConverterUtil.isZero(viewmodel.likes)}"
这样就行了?显然不可以,ConverterUtil的isZero方法返回值是一个boolean类型。而android:visibility接收一个Integer类型。所以我们还需要定义一个绑定转换器方法,将这个boolean类型值转化为Integer类型。如下所示:
object BindingConverters{
//BindingConversion使用注解
@BindingConversion
@JvmStatic fun booleanToVisibility(isNotVisible: Boolean): Int {
return if (isNotVisible) View.GONE else View.VISIBLE
}
}
android:visibility
接收了一个boolean类型参数以后,就会去找是否有可以把boolean类型值转化为Integer类型的绑定方法,如果找到了,就会进行转换。在编译期间,编译器会为我们做检查,如果找不到这样的方法就会报错,编译不过。
注意:这种转换是不安全的,因为我们无法将其限制在我们这一种场景下。如果一个View属性接收Integer类型,然后我们给该View传递了一个boolean类型的值,那么它会将该boolean值转化为View可见性的Integer值。在这个例子中就是View.GONE(8)或者View.VISIBLE(0)。
推荐的方法,使用绑定适配器
@BindingAdapter("app:hideIfZero") // Recommended solution
@JvmStatic fun hideIfZero(view: View, number: Int) {
view.visibility = if (number == 0) View.GONE else View.VISIBLE
}
app:hideIfZero="@{viewmodel.likes}"
这种通过自定义一个新的属性的方式,可以避免被意外使用。
根据经验,使用绑定适配器创建自己的自定义属性比在布局表达式中增加逻辑更好,也比绑定转换器更安全,推荐使用。
和ViewModels无缝结合
上面在说LiveData的时候提到了,这里就不说了。
完整代码请参考官方示例BasicSample