Jetpack DataBinding

准备工作

在模块的 build.gradle 文件中添加 DataBinding 的支持

build.gradle
1
2
3
4
5
6
7
8
9
10
11
12
13
// Android Studio 3.6
android {
dataBinding{
enabled = true
}
}

// Android Studio 4.0
android {
buildFeatures {
dataBinding = true
}
}

如果提示:If you plan to use data binding in a Kotlin project, you should apply the kotlin-kapt plugin.

build.gradle
1
apply plugin: 'kotlin-kapt'

DataBinding 基本使用

DataBinding 组件通过使用声明式格式将数据源绑定到布局中。

DataBinding 在 Google 推荐的 MVVM 架构中发挥着重要的作用,MVVM 架构的本质是数据驱动页面,而目前 Android 系统提供给开发者的实现这一功能的最佳组合只有 DataBinding 和 Jetpack Compose。

基础布局绑定表达式

DataBinding 的布局文件必须使用 layout 根标记,并且通过 data 标签设置对应的数据实体类。

name 属性声明了在布局文件中可以使用的对象名称,type 是对应的实体类,然后就可以在 xml 中通过 @{} 表达式为文本组件赋值了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="user"
type="com.example.littlehelper.ui.test.User" />
</data>

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/et_user_name"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>

<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/et_user_id"
app:layout_constraintTop_toBottomOf="@id/et_user_name"
app:layout_constraintStart_toStartOf="parent"/>

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/tv_user_name"
app:layout_constraintTop_toBottomOf="@id/et_user_id"
app:layout_constraintStart_toStartOf="parent"
android:text="@{user.userName}"/>

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/tv_user_id"
app:layout_constraintTop_toBottomOf="@id/tv_user_name"
app:layout_constraintStart_toStartOf="parent"
android:text="@{user.userId}"/>

<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/btn_confirm"
android:text="确定"
app:layout_constraintTop_toBottomOf="@id/tv_user_id"
app:layout_constraintStart_toStartOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

修改完布局代码后,需要在 Activity 中进行数据绑定。同 ViewBinding 一样,启用 DataBinding 之后系统会为每个布局文件生成一个绑定类。默认情况下,他会转换为 Pascal 命名形式,并在末尾添加 Binding 后缀。比如 activity_main.xml 生成的对应类为 ActivityMainBinding。(Pascal 命名形式又称大驼峰命名形式,即每一个单词的首字母都转为大写字母,如 UserName、UserId 等)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class UserActivity : AppCompatActivity() {
lateinit var userBinding:ActivityUserBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 通过 layoutInflater 获取视图
userBinding = ActivityUserBinding.inflate(layoutInflater)
setContentView(userBinding.root)
// 点击按钮时将生成的对象绑定到生成的绑定类上
userBinding.btnConfirm.setOnClickListener{
val user = getUser()
// userBinding.tvUserName.text = user.userName
// userBinding.tvUserId.text = user.userId
// 使用 DataBinding 组件可以省去主动设置的过程
// 设置 userBinding.user 后,数据会自动填充到 xml 布局中
userBinding.user = user
}
}

/**
* 获取用户对象,模拟从网络获取
*/
private fun getUser(): User {
return User(getUserName(),getUserId())
}

/**
* 获取用户名
*/
private fun getUserName(): String? {
return userBinding.etUserName.text?.toString()
}

/**
* 获取用户 id
*/
private fun getUserId():String?{
return userBinding.etUserId.text?.toString()
}
}

视图中也可以引入表达式。比如添加表达式,如果 userId 是 “001” 则隐藏视图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
...
<import type="android.view.View" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

...

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/tv_user_id"
app:layout_constraintTop_toBottomOf="@id/tv_user_name"
app:layout_constraintStart_toStartOf="parent"
android:text="@{user.userId}"
android:visibility='@{user.userId.equals("001")?View.GONE : View.VISIBLE}'/>

...
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

DataBinding 不仅可以在 Activity 中使用,还可以在 Fragment、RecycleView 适配器中使用。在 RecycleView 适配器中使用数据绑定的代码如下:

1
val listItemBinding = DataBindingUtil.inflate(layoutInflater,R.layout.list_item,viewGroup,flase)

利用 DataBinding 绑定点击事件

一般点击事件是通过 setOnClickListener 方法,除此之外,还可以在 xml 中声明 onClick 属性,并在 Activity 中编写同名的方法即可:

1
2
3
4
5
6
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/btn_confirm"
android:text=""
android:onClick="confirm" />
1
2
3
fun confirm(){
...
}

DataBinding 的实现方式称为方法引用

首先新建一个 ClickHandlers 类,添加 confirm 方法:

1
2
3
4
5
6
7
class ClickHandlers {
val TAG = "TAG_ClickHandlers"

fun confirm(view:View){
Log.d(TAG,"触发点击事件了")
}
}

然后,在视图中引入 ClickHandlers 类,并为 Button 添加 onClick 属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
...
<variable
name="clickHandlers"
type="com.example.littlehelper.ui.test.ClickHandlers" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
...
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/btn_confirm"
android:text="确定"
android:onClick="@{clickHandlers::confirm}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

最后,在 Activity 中绑定监听器类。

1
userBinding.clickHandlers = ClickHandlers()

方法引用的表达式是在编译时处理的,如果 ClickHandlers 中没有对应的方法,则会在编译阶段报错。在 gradle 2.0 及更高版本中提供了监听器绑定的方法,与方法引用不同的是,监听器绑定要在事件发生时才执行表达式。在 ClickHandlers 添加方法:

1
2
3
4
5
6
7
class ClickHandlers {
val TAG = "TAG_ClickHandlers"

fun confirm(view:View,user:User){
Log.d(TAG,"触发点击事件了")
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="user"
type="com.example.littlehelper.ui.test.User" />
<variable
name="clickHandlers"
type="com.example.littlehelper.ui.test.ClickHandlers" />
<import type="android.view.View" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
...
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/btn_confirm"
android:text="确定"
android:onClick="@{(view)->clickHandlers.confirm(view,user)}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

合理地使用监听器表达式可以将部分代码从 Activity 中抽取出来,便于阅读和维护。但实际开发中不建议使用复杂的监听器,容易导致代码非常臃肿,建议根据实际情况取舍。

标签布局使用 DataBinding

实际开发中,为了优化布局经常会使用 include、merge 标签将部分布局抽取出来,这种布局称为标签布局。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="user"
type="com.example.littlehelper.ui.test.User" />
<import type="android.view.View" />
</data>

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_user_id" >

<TextView
android:id="@+id/tv_user_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{user.userName}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/tv_user_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{user.userId}"
android:visibility='@{user.userId.equals("001")?View.GONE : View.VISIBLE}'
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_user_name" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

通过 include 标签引入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:bind="http://schemas.android.com/tools">
<data>
<variable
name="user"
type="com.example.littlehelper.ui.test.User" />
<import type="android.view.View" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/et_user_name"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>

<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/et_user_id"
app:layout_constraintTop_toBottomOf="@id/et_user_name"
app:layout_constraintStart_toStartOf="parent"/>

<include
android:id="@+id/include_user_data"
layout="@layout/user_data"
bind:user="@{user}" />

<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/btn_confirm"
android:text="确定"
app:layout_constraintTop_toBottomOf="@id/include_user_data"
app:layout_constraintStart_toStartOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

自定义 BindingAdapter

当启用 DataBinding 后,系统会自动生成 UI 组件对应的 BindingAdapter 类,如在 TextViewBindingAdapter 中生成了 setText 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
final CharSequence oldText = view.getText();
if (text == oldText || (text == null && oldText.length() == 0)) {
return;
}
if (text instanceof Spanned) {
if (text.equals(oldText)) {
return;
}
} else if (!haveContentsChanged(text, oldText)) {
return;
}
view.setText(text);
}

可以看到通过 BindingAdapter 注解生成 android:text 方法实际就是调用了 setText 的方法,所以可以将结果直接绑定到 xml 中。

但对于加载网络图片以及包含特殊业务逻辑的数据则需要自定义 BindingAdapter 了。以加载用户网络头像为例:

效果图:

添加依赖:

1
implementation("io.coil-kt:coil:1.1.1")

创建 User 类,包含用户姓名、用户 Id、用户头像和用户性别:

1
2
3
4
5
6
data class User(
var userName:String?,
var userId:String?,
var userPhoto:String?,
var userGender:Int
)

接下来,新建 ItemBind 类,可以有如下三种写法:

1
2
3
4
5
6
7
8
9
10
11
12
object ItemBind {
/**
* 添加 BindingAdapter 注解
* value 对应的是在 xml 中可以引用的属性值,然后在 xml 布局中就可以使用 android:imgUrl 属性为 ImageView 控件指定网络图片资源了。
*/
@JvmStatic
@BindingAdapter(value = ["android:imgUrl"])
fun setUserPhoto(iView: ImageView, imageUrl: String?) {
Log.d("TAG", "imageUrl:$imageUrl")
iView.load(imageUrl)
}
}
1
2
3
4
5
6
7
8
9
10
class ItemBind {
companion object{
@JvmStatic
@BindingAdapter(value = ["android:imgUrl"])
fun setUserPhoto(iView: ImageView, imageUrl: String?) {
Log.d("TAG", "imageUrl:$imageUrl")
iView.load(imageUrl)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.littlehelper.ui.test

import android.util.Log
import android.widget.ImageView
import androidx.databinding.BindingAdapter
import coil.load

// 使用顶层函数
@BindingAdapter(value = ["android:imgUrl"])
fun setUserPhoto(iView: ImageView, imageUrl: String?) {
Log.d("TAG", "imageUrl:$imageUrl")
iView.load(imageUrl)
}

注意上面的写法,否则会报如下的错误:

java.lang.IllegalStateException: Required DataBindingComponent is null in class ActivityUserBindingImpl. A BindingAdapter in com.example.littlehelper.ui.test.ItemBind is not static and requires an object to use, retrieved from the DataBindingComponent. If you don’t use an inflation method taking a DataBindingComponent, use DataBindingUtil.setDefaultComponent or make all BindingAdapter methods static.

第一种和第二种方式:增加@JvmStatic注解,这样kotlin在编译为java代码的时候,可以为其生成对应的静态方法。否则只是在使用上和静态方法差不多,但在运行时它们任然是真正对象的成员实例,其实内部还是非静态的方法调用,只是通过静态字段的初始化,保证了其单例的特性。

第三种方式:在kotlin里面顶级函数,就对应java的静态函数。

接下来修改布局代码,将头像地址绑定到 ImageView 控件上:

1
2
3
4
<ImageView
android:layout_width="200dp"
android:layout_height="200dp"
android:imgUrl="@{user.userPhoto}" />

最后在 Activity 中为 User 对象设置一个头像地址:

1
2
3
// 这里使用了访问网络图片的功能,所以还需要配置网络权限。
// 如果访问的是 http 明文网络,在 Android 9.0 中限制了明文网络的访问,还需配置明文访问权限。
return User(getUserName(),getUserId(), "https://tvax2.sinaimg.cn/large/d030806aly1h4i509i1s5j21cs2301kx.jpg",1)

BindingAdapter 还可以为注解指定多个属性值

在实际业务开发中,为用户设置头像时,在网络图片加载出来之前一般需要设置占位符,实现占位符图片与用户性别相关功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.example.littlehelper.ui.test

import android.util.Log
import android.widget.ImageView
import androidx.databinding.BindingAdapter
import coil.load
import com.example.littlehelper.R

/**
* 添加 BindingAdapter 注解
*
* value 对应的是在 xml 中可以引用的属性值,然后在 xml 布局中就可以使用 android:imgUrl 属性为 ImageView 控件指定网络图片资源了。
*
* requireAll 为 false 时,在 xml 中设置任何一个属性时都会调用此方法,
* 如果为 true,则只有在 xml 设置所有属性时才会调用 setUserPhoto 方法。
*/
@BindingAdapter(
value = ["android:imgUrl","android:gender"],
requireAll = false
)
fun setUserPhoto(iView: ImageView, imageUrl: String?,gender:Int) {
if (gender == 1){
iView.load(imageUrl){
placeholder(R.drawable.ic_launcher_background)
crossfade(true) // 创建视觉效果,使图像细节看起来像淡入淡出,类似于渐进式 JPEG
}
}else{
iView.load(imageUrl){
placeholder(R.drawable.ic_launcher_foreground)
}
}
}
1
2
3
4
5
<ImageView
android:layout_width="200dp"
android:layout_height="200dp"
android:imgUrl="@{user.userPhoto}"
android:gender="@{user.userGender}" />
1
return User(getUserName(),getUserId(), "https://tvax2.sinaimg.cn/large/d030806aly1h4i509i1s5j21cs2301kx.jpg",1)

双向数据绑定

在前面示例中,用户在输入框中输入信息,点击确定按钮后,会将用户输入的信息赋值给 User 对象,通过绑定对象实现在 TextView 中显示用户输入的信息,这种绑定称为单向绑定。在原始方法中可通过给 EditText 设置 addTextChangedListener 方法监听输入框内容变化,从而实现在用户输入信息时不需要通过点击事件将输入的信息显示在 TextView 上。

而双向绑定为开发者提供了更简便的方式:

效果图示例:

代码示例:

BaseObservable 是一种可观察的数据,继承自 BaseObservable 的数据类负责在属性更改时发出通知。
具体操作过程是向 getter 分配 Bindable 注释,然后在 setter 中调用 notifyPropertyChanged 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class User(var userPhoto:String,var userGender:Int) : BaseObservable(){

@get:Bindable
var userName:String = ""
set(value) {
field = value
// BR 是自动生成的,其中包含用于数据绑定的资源的 ID。
// import com.example.littlehelper.BR
// userName 可能会报错找不到,可以尝试 Build-》Clean Project、Rebuild Project
notifyPropertyChanged(BR.userName) // APT技术:字节码插桩。APT又是注解处理器技术
}

@get:Bindable
var userId:String = ""
set(value) {
field = value
// APT技术:字节码插桩。APT又是注解处理器技术
notifyPropertyChanged(BR.userId)
}
}

@={} 表达式可在接收属性数据更改的同时监听用户更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:bind="http://schemas.android.com/tools">
<data>
<variable
name="user"
type="com.example.littlehelper.ui.test.User" />
<import type="android.view.View" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<!-- @={} 表达式可在接收属性数据更改的同时监听用户更新 -->
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/et_user_name"
android:text="@={user.userName}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>

<!-- @={} 表达式可在接收属性数据更改的同时监听用户更新 -->
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/et_user_id"
android:text="@={user.userId}"
app:layout_constraintTop_toBottomOf="@id/et_user_name"
app:layout_constraintStart_toStartOf="parent"/>

<include
android:id="@+id/include_user_data"
layout="@layout/user_data"
bind:user="@{user}" />

<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/btn_confirm"
android:text="确定"
app:layout_constraintTop_toBottomOf="@id/include_user_data"
app:layout_constraintStart_toStartOf="parent"/>

<ImageView
android:layout_width="200dp"
android:layout_height="200dp"
android:id="@+id/iv"
android:imgUrl="@{user.userPhoto}"
android:gender="@{user.userGender}"
app:layout_constraintTop_toBottomOf="@id/btn_confirm"
app:layout_constraintStart_toStartOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

最后在 Activity 中绑定 User 对象

1
2
3
4
5
6
7
8
9
10
11
12
class UserActivity : AppCompatActivity() {
lateinit var userBinding:ActivityUserBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
userBinding = ActivityUserBinding.inflate(layoutInflater)
setContentView(userBinding.root)

val user = User("https://tvax2.sinaimg.cn/large/d030806aly1h4i509i1s5j21cs2301kx.jpg",0)
userBinding.user = user
}
}

在实际开发中,可能还会使用自定义特性的双向数据绑定。

DataBinding 与 ViewBinding 的区别

两者都可以生成可直接引用视图的绑定类,从这一点来说,两者都可以代替 findViewById。但 ViewBinding 仅有引用视图的功能,因此和 DataBinding 相比,ViewBinding 有以下优势:

  • 编译速度快:ViewBinding 不需要处理 DataBinding 的注解,编译时间短,编译速度更快。
  • 使用简洁:ViewBinding 对布局元素没有限制,不需要以 layout 开头,启动视图绑定就可以在项目中使用。

DataBinding 更像是 ViewBinding 的扩展版本,它提供了更多常用的功能,所以 ViewBinding 不具备布局表达式、双向数据绑定等功能。

DataBinding 虽然是 MVVM 模式的核心实现方式,但其设置数据的相关逻辑都写在了 xml 中,这会导致调试困难。

源码解析

以 UserActivity 为例,当启用 DataBinding 时,系统会自动生成 ActivityUserBinding 和 ActivityUserBindingImpl 类。调用绑定 User 对象的方式时,则会进入 ActivityUserBindingImpl 的 setUser 方法:

1
2
3
4
5
6
7
8
9
public void setUser(@Nullable com.example.littlehelper.ui.test.User User) {
updateRegistration(0, User);
this.mUser = User;
synchronized(this) {
mDirtyFlags |= 0x1L;
}
notifyPropertyChanged(BR.user);
super.requestRebind();
}

BR 类是 DataBinding 在模块中生成的一个类,BR 类包含用于数据绑定的资源和 id:

1
2
3
4
5
6
7
8
9
public class BR {
public static final int _all = 0;

public static final int user = 1;

public static final int userId = 2;

public static final int userName = 3;
}

setUser 方法最终会进入 executeBindings 方法执行绑定:

1
2
3
4
5
6
7
if ((dirtyFlags & 0x19L) != 0) {
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.etUserId, userUserId);
}
...
if ((dirtyFlags & 0x15L) != 0) {
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.etUserName, userUserName);
}

executeBindings 方法最终将调用 TextViewBindingAdapter 的 setText方法进行赋值,进而将绑定的数据结果显示在页面上。


备注

参考资料

《Android Jetpack开发 原理解析与应用实战》

欢迎关注微信公众号:非也缘也