Android开发Jetpack组件DataBinding
Alpinist Wang 人气:0简介
DataBinding 是 Jetpack 组件之一,适用于 MVVM 模式开发,也是Google官方推荐使用的组件之一。使用DataBinding可以很容易的达到视图与逻辑分离,直接在布局中绑定数据,并且数据改变时自动更新UI,不再需要在业务代码中绑定数据。
本文结合Google官方提供的DataBinding使用示例讲解,Google官方sample地址:
https://github.com/googlesamples/android-databinding
其中包含两个工程,本文使用的BasicSample工程。
使用方式
1. build.gradle 中添加 kapt,并启用dataBinding
在app模块的build.gradle
文件中,添加:
apply plugin: 'kotlin-kapt' android { ... dataBinding { enabled true } }
其中,apply plugin: 'kotlin-kapt'
是为了在Kotlin中使用BindingAdapter注解。
2.修改布局文件,添加 layout 和 data 标签
使用DataBinding的布局文件与普通的布局文件有些不同,在布局外需要包裹一层<layout>
,并添加<data>
标签,如下所示:
<?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/> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
<data>
标签是用来添加数据的,初次使用,先保持为空即可。<data>
标签之后就是正常的布局代码,与普通的布局代码一模一样。
接下来,我们在activity_main布局中添加两个按钮:
<?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/> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:id="@+id/observable_fields_activity_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Observable Fields activity" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"/> <Button android:id="@+id/viewmodel_activity_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="ViewModel activity" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/observable_fields_activity_button"/> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
写好布局文件后rebuild project
,这时generatedJava文件夹下会自动生成databinding需要用到的文件。
3.使用 DataBindingUtil 绑定布局
由于使用了DataBinding布局,所以我们需要用DataBindingUtil绑定布局,而不是直接使用setContentView。
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 因为使用了dataBinding布局,所以这里需要用DataBindingUtil来设置布局 val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main) // 返回的binding拥有布局中所有的View,使用id引用即可 binding.observableFieldsActivityButton.setOnClickListener { startActivity(Intent(this, ObservableFieldActivity::class.java)) } binding.viewmodelActivityButton.setOnClickListener { startActivity(Intent(this, ViewModelActivity::class.java)) } } }
返回的Binding类型跟布局文件名有关,规则是布局文件名去掉下划线,改成驼峰命名,再添加Binding后缀。比如本例中使用的布局文件是activity_main.xml,所以返回的Binding类型就是ActivityMainBinding。
返回的binding中拥有布局中的所有View,使用id即可引用,这里我们给两个按钮添加了两个点击事件,点击跳转至另外两个Activity。这里我们先新建两个空的Activity,命名为ObservableFieldActivity和ViewModelActivity,后面我们会修改这两个Activity的内容。注意由于使用了DataBinding布局,所以Kotlin直接用id引用View的特性不可用了。
4.布局的 data 标签中添加数据变量,并使用其参数
<layout>
中的<data>
用来定义需要绑定的数据,我们先新建一个数据类ObservableFieldProfile:
data class ObservableFieldProfile( val name: String, val lastName: String )
这是一个表示个人信息的类,name表示名字,lastName表示姓氏。
在ObservableFieldActivity的布局文件中,使用此类定义一个user变量:
<?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> <import type="com.example.studydatabinding.data.ObservableFieldProfile"/> <variable name="user" type="ObservableFieldProfile"/> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.ObservableFieldActivity"> <TextView android:id="@+id/name_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginTop="8dp" android:text="Name" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"/> <TextView android:id="@+id/name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@{user.name}" app:layout_constraintStart_toStartOf="@id/name_label" app:layout_constraintTop_toBottomOf="@id/name_label"/> <TextView android:id="@+id/lastname_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Last Name" app:layout_constraintStart_toStartOf="@id/name" app:layout_constraintTop_toBottomOf="@id/name"/> <TextView android:id="@+id/lastname" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@{user.lastName}" app:layout_constraintStart_toStartOf="@id/lastname_label" app:layout_constraintTop_toBottomOf="@id/lastname_label"/> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
<data>
中的import标签用于导入使用的类,type表示类的完整路径,<variable>
标签用于定义变量,name表示变量名,type表示变量类型。在此例中,我们定义了一个类型为ObservableFieldProfile的user变量。
使用此变量的方式也很简单,在布局文件中,使用@{user.name}
即可使用user变量的name参数,使用@{user.lastName}
即可使用user变量的lastName参数。可以看到,我们给两个TextView的android:text
属性设置了user的这两个参数。
同样的,写好布局文件后需要rebuild Project。
在ObservableFieldActivity中,使用DataBindingUtil设置布局,并且给返回的binding变量设置user参数,代码如下:
class ObservableFieldActivity : AppCompatActivity() { private val observableFieldProfile = ObservableFieldProfile("Alpinist", "Wang") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding: ActivityObservableFieldBinding = DataBindingUtil.setContentView(this, R.layout.activity_observable_field) binding.user = observableFieldProfile } }
使用binding.user给DataBinding布局中的user变量赋值。
此时,运行程序,显示如下:
可以看到,我们绑定的user变量已经成功显示出来了。
不过此时user变量变化时,UI还不能自动刷新。如果需要UI随着变量自动刷新,需要将String类型换成ObservableField<String>
类型。如果是int型变量,需要替换成ObservableField<Int>
类型,它等同于ObservableInt
类型,我们以ObservableInt
类型为例,在ObservableFieldProfile类中新增likes变量,:
data class ObservableFieldProfile( val name: String, val lastName: String, val likes: ObservableInt )
修改布局文件activity_observable_field.xml:
<?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"> ... <TextView android:id="@+id/likes_label" android:text="Like" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" android:layout_marginEnd="8dp" android:layout_marginTop="8dp" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView android:id="@+id/likes" app:layout_constraintTop_toBottomOf="@id/likes_label" app:layout_constraintStart_toStartOf="@id/likes_label" app:layout_constraintEnd_toEndOf="@id/likes_label" android:text="@{Integer.toString(user.likes)}" android:layout_marginTop="8dp" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <Button android:id="@+id/like_button" app:layout_constraintTop_toBottomOf="@id/likes" app:layout_constraintEnd_toEndOf="parent" android:layout_marginTop="8dp" android:layout_marginEnd="8dp" android:text="Like" android:onClick="onLike" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
我们在布局中添加了一个按钮,点击一次触发onLike方法,还添加了一个显示likes数量的TextView,由于likes是一个Int型变量,所以我们用Integer.toString()方法将其转换为了String。
修改ObservableFieldActivity:
class ObservableFieldActivity : AppCompatActivity() { private val observableFieldProfile = ObservableFieldProfile("Alpinist", "Wang", ObservableInt(0)) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding: ActivityObservableFieldBinding = DataBindingUtil.setContentView(this, R.layout.activity_observable_field) binding.user = observableFieldProfile } fun onLike(view: View) { observableFieldProfile.likes.increment() } } fun ObservableInt.increment() { set(get() + 1) }
我们在此Activity中添加了onLike()方法,前文已说到,点击按钮一次便会触发一次onLike方法,在此方法中,我们将user的likes变量增加1。其中拓展的increment函数主要是为了方便调用。
此时,运行程序,效果如下:
为了让效果更加酷炫一点,顺便复习一下上面学到的内容,我们在布局中添加一个ImageView和一个ProgressBar:
<?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> <import type="com.example.studydatabinding.data.ObservableFieldProfile"/> <import type="android.view.View"/> <variable name="user" type="ObservableFieldProfile"/> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.ObservableFieldActivity"> <TextView android:id="@+id/name_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginTop="8dp" android:text="Name" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"/> <TextView android:id="@+id/name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@{user.name}" app:layout_constraintStart_toStartOf="@id/name_label" app:layout_constraintTop_toBottomOf="@id/name_label"/> <TextView android:id="@+id/lastname_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Last Name" app:layout_constraintStart_toStartOf="@id/name" app:layout_constraintTop_toBottomOf="@id/name"/> <TextView android:id="@+id/lastname" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@{user.lastName}" app:layout_constraintStart_toStartOf="@id/lastname_label" app:layout_constraintTop_toBottomOf="@id/lastname_label"/> <ImageView android:minHeight="48dp" android:minWidth="48dp" android:id="@+id/imageView" app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="24dp" android:layout_marginTop="24dp" android:tint="@{user.likes > 9 ? @android:color/holo_red_light : @android:color/black}" android:src="@{user.likes < 4 ? @drawable/ic_person_black_96dp : @drawable/ic_whatshot_black_96dp}" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView android:text="Like" app:layout_constraintEnd_toEndOf="@id/imageView" app:layout_constraintTop_toBottomOf="@id/imageView" app:layout_constraintStart_toStartOf="@id/imageView" android:layout_marginEnd="8dp" android:layout_marginTop="8dp" android:id="@+id/likes_label" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView android:id="@+id/likes" app:layout_constraintTop_toBottomOf="@id/likes_label" app:layout_constraintStart_toStartOf="@id/likes_label" app:layout_constraintEnd_toEndOf="@id/likes_label" android:text="@{Integer.toString(user.likes)}" android:layout_marginTop="8dp" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <Button android:id="@+id/like_button" app:layout_constraintTop_toBottomOf="@id/likes" app:layout_constraintEnd_toEndOf="@id/likes" app:layout_constraintStart_toStartOf="@id/likes" android:layout_marginTop="8dp" android:text="Like" android:onClick="onLike" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <ProgressBar android:layout_width="0dp" app:layout_constraintStart_toStartOf="@id/like_button" app:layout_constraintEnd_toEndOf="@id/like_button" app:layout_constraintTop_toBottomOf="@id/like_button" style="@style/Widget.AppCompat.ProgressBar.Horizontal" android:visibility="@{user.likes > 0 ? View.VISIBLE : View.GONE}" android:layout_marginTop="8dp" android:layout_height="wrap_content"/> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
其中的两张Vector图片代码如下:
ic_whatshot_black_96dp.xml:
<vector android:height="96dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="96dp" xmlns:android="http://schemas.android.com/apk/res/android"> <path android:fillColor="#FF000000" android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/> </vector>
ic_whatshot_black_96dp.xml:
<vector android:height="96dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="96dp" xmlns:android="http://schemas.android.com/apk/res/android"> <path android:fillColor="#FF000000" android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/> </vector>
主要添加的逻辑有:
- 如果user.likes大于9,ImageView显示红色,否则显示黑色。
- 如果user.likes小于4,ImageView显示ic_person_black_96dp图片,否则显示ic_whatshot_black_96dp图片。注意不能直接使用>、<符号,需要用转义字符>、<代替。
- 如果user.likes大于0,显示ProgressBar,否则隐藏ProgressBar
此时,运行代码,效果如下:
5.BindingAdapter的使用
BindingAdapter是用来拓展属性的,新建一个BindingAdapters类,添加以下代码:
object BindingAdapters { @BindingAdapter("hideIfZero") @JvmStatic fun hideIfZero(view: View, number: Int) { view.visibility = if (number == 0) View.GONE else View.VISIBLE } }
写好后,rebuild project
,然后就可以在布局文件中,对View使用hideIfZero
属性了。BindingAdapter中的参数表示属性名字,函数中的第一个参数表示View类型,后面的参数表示属性的值。
BindingAdapter中的属性名字可以添加命名空间前缀,例如可以写作app:hideIfZero
,不过这样的话在xml中就只能使用app:
前缀了。如果像上例中一样,在BindingAdapter中不添加命名空间前缀,那么在xml中可以直接使用hideIfZero
或者其他任何除android:
外的前缀,比如app:hideIfZero
,whatever:hideIfZero
等等。
在BindingAdapter中再添加几个扩展属性:
object BindingAdapters { @BindingAdapter("popularityIcon") @JvmStatic fun popularityIcon(view: ImageView, popularity: Popularity) { val color = getAssociateColor(popularity, view.context) ImageViewCompat.setImageTintList(view, ColorStateList.valueOf(color)) view.setImageDrawable(getDrawablePopularity(popularity, view.context)) } @BindingAdapter("progressTint") @JvmStatic fun tintPopularity(view: ProgressBar, popularity: Popularity) { val color = getAssociateColor(popularity, view.context) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { view.progressTintList = ColorStateList.valueOf(color) } } @BindingAdapter("progressScaled", "android:max") @JvmStatic fun setProgress(progressBar: ProgressBar, likes: Int, max: Int) { progressBar.progress = (likes * max / 5).coerceAtMost(max) } @BindingAdapter("hideIfZero") @JvmStatic fun hideIfZero(view: View, number: Int) { view.visibility = if (number == 0) View.GONE else View.VISIBLE } private fun getAssociateColor(popularity: Popularity, context: Context): Int { return when (popularity) { Popularity.POPULAR -> ContextCompat.getColor(context, R.color.popular) Popularity.STAR -> ContextCompat.getColor(context, R.color.star) else -> Color.BLACK } } private fun getDrawablePopularity(popularity: Popularity, context: Context): Drawable? { return ContextCompat.getDrawable( context, when (popularity) { Popularity.NORMAL -> R.drawable.ic_person_black_96dp else -> R.drawable.ic_whatshot_black_96dp } ) } }
从setProgress方法可以看出,如果需要获取xml中其他的属性值,在BindingAdapter中添加对应的属性名,再在函数中添加对应的参数即可。
其中的Popularity类如下:
enum class Popularity { NORMAL, POPULAR, STAR }
编辑activity_view_model,使用上面定义的自定义属性:
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" > <data> <import type="com.example.studydatabinding.data.ProfileLiveDataViewModel"/> <variable name="viewmodel" type="ProfileLiveDataViewModel"/> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.ViewModelActivity"> <TextView android:id="@+id/name_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginTop="8dp" android:text="@string/name_label" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"/> <TextView android:id="@+id/name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@{viewmodel.name}" app:layout_constraintStart_toStartOf="@id/name_label" app:layout_constraintTop_toBottomOf="@id/name_label"/> <TextView android:id="@+id/lastname_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/last_name_label" app:layout_constraintStart_toStartOf="@id/name" app:layout_constraintTop_toBottomOf="@id/name"/> <TextView android:id="@+id/lastname" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@{viewmodel.lastName}" app:layout_constraintStart_toStartOf="@id/lastname_label" app:layout_constraintTop_toBottomOf="@id/lastname_label"/> <ImageView android:id="@+id/imageView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:layout_marginEnd="24dp" android:contentDescription="@string/profile_avatar_cd" android:minWidth="48dp" android:minHeight="48dp" app:popularityIcon="@{viewmodel.popularity}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent"/> <TextView android:id="@+id/likes_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/likes" app:layout_constraintEnd_toEndOf="@id/imageView" app:layout_constraintStart_toStartOf="@id/imageView" app:layout_constraintTop_toBottomOf="@id/imageView"/> <TextView android:id="@+id/likes" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{Integer.toString(viewmodel.likes)}" app:layout_constraintEnd_toEndOf="@id/likes_label" app:layout_constraintStart_toStartOf="@id/likes_label" app:layout_constraintTop_toBottomOf="@id/likes_label"/> <Button android:id="@+id/like_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:onClick="@{() -> viewmodel.onLike()}" android:text="@string/like" app:layout_constraintEnd_toEndOf="@id/likes" app:layout_constraintStart_toStartOf="@id/likes" app:layout_constraintTop_toBottomOf="@id/likes"/> <ProgressBar android:id="@+id/progressBar" style="@style/Widget.AppCompat.ProgressBar.Horizontal" android:layout_width="0dp" app:progressTint="@{viewmodel.popularity}" app:progressScaled="@{viewmodel.likes}" android:max="@{100}" android:layout_height="wrap_content" android:layout_marginTop="8dp" app:hideIfZero="@{viewmodel.likes}" app:layout_constraintEnd_toEndOf="@id/like_button" app:layout_constraintStart_toStartOf="@id/like_button" app:layout_constraintTop_toBottomOf="@id/like_button"/> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
其中的ProfileLiveDataViewModel类如下:
class ProfileLiveDataViewModel : ViewModel() { private val _name = MutableLiveData("Alpinist") private val _lastName = MutableLiveData("Wang") private val _likes = MutableLiveData(0) val name: LiveData<String> = _name val lastName: LiveData<String> = _lastName val likes: LiveData<Int> = _likes val popularity: LiveData<Popularity> = Transformations.map(_likes) { when { it > 9 -> Popularity.STAR it > 4 -> Popularity.POPULAR else -> Popularity.NORMAL } } fun onLike(){ _likes.value = _likes.value?.inc() } }
LiveData也是jetpack组件之一,其原理和ObservableField是类似的,都是属于可观测的属性。
编辑ViewModelActivity:
class ViewModelActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val viewModel = ViewModelProviders.of(this).get(ProfileLiveDataViewModel::class.java) val binding: ActivityViewModelBinding = DataBindingUtil.setContentView(this, R.layout.activity_view_model) binding.viewmodel = viewModel binding.lifecycleOwner = this } }
此时,运行程序,显示如下:
加载全部内容