Android开发MVVM中DataBinding的使用

开启DataBinding

在 RecyclerView 中 , 如果要使用DataBinding架构组件进行数据绑定 , 首先要 启用 DataBinding , 并 导入 RecyclerView 依赖 ,

在 Module 模块下的 build.gradle.kts 构建脚本 中 , 配置如下内容 :

build.gradle.kts

1
2
3
4
5
6
7
8
9
10
android {
enable = true
}

dependencies {
// 导入 RecyclerView 依赖
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.10.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
}

修改主题

新建项目都是Jetpack Compose的主题了,修改为AppCompat的主题

1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Xhcontrolbrowser" parent="Theme.AppCompat.Light.NoActionBar" />
</resources>

布局

res下新建文件夹layout

添加activity_main.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
<?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>
</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

</layout>

创建的XML可以将光标放置在第一个字符位置 , 按下 Alt + 回车 , 弹出如下下拉菜单 ,转换为DataBinding的XML。

image-20240410111930289

添加实体类

不自动更新

1
class Student(var name: String, var age: Int) {}

单向绑定

方式1

如果想单向刷新

类继承BaseObservable,在需要更新字段的set方法中添加notifyChange();即可

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
import androidx.databinding.BaseObservable;
import androidx.databinding.Bindable;

public class Student extends BaseObservable {
private String name;
private int age;

public Student() {}

public Student(String name, int age) {
this.name = name;
this.age = age;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
notifyChange();
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
notifyChange();
}
}

方式2

该类的属性用ObservableField封装同样的也可以实现单向绑定

1
2
3
4
public class Person  {
public ObservableField<Integer> age = new ObservableField<>();
public ObservableField<String> name = new ObservableField<>();
}

也可以用一下类型

1
2
3
4
5
6
7
8
9
10
11
BaseObservable,
ObservableBoolean,
ObservableByte,
ObservableChar,
ObservableDouble,
ObservableField,
ObservableFloat,
ObservableInt,
ObservableLong,
ObservableParcelable,
ObservableShort

ObservableCollection
dataBinding 也提供了包装类用于替代原生的 List 和 Map,分别是 ObservableList 和 ObservableMap

该对象的属性会自带set和get方法,调用set方法即可实现页面控件绑定的数据自动刷新

1
2
3
4
5
6
7
8
public  class Presenter{
public void onClick(Person person){
person.name.set( "new test");
person.age.set( 30);
Log.i("Presenter","onClick" + person.name);
content.set("new content");
}
}

双向绑定

对于输入控件,使用@={}表达式即可实现页面和绑定的值双向自动刷新

1
2
3
4
5
6
7
8
9
10
11
<EditText
android:id="@+id/editTextTextPersonName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="85dp"
android:layout_marginBottom="7dp"
android:ems="10"
android:inputType="textPersonName"
android:text="@={content}"
app:layout_constraintBottom_toBottomOf="@+id/textView3"
app:layout_constraintEnd_toEndOf="parent" />

代码中

1
2
3
4
5
6
7
8
9
10
11
12
13

public class MainActivity extends AppCompatActivity {
ActivityMainBinding activityMainBinding;
ObservableField<String> content;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activityMainBinding = DataBindingUtil.setContentView(this,R.layout.activity_main);
content = new ObservableField<>("我的内容");
activityMainBinding.setContent(content);
}

}

列表项的XML

list_item_user.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
<?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="student"
type="com.xhkjedu.xh_control_browser.model.Student" />
</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="50dip">

<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{student.name}"
android:textSize="24sp"
tools:text="Tom"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf(student.age)}"
android:textSize="24sp"
tools:text="18"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.7"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

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

创建的list_item_user.xml会自动生成类ListItemUserBinding

RecyclerView.Adapter

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
55
56
57
58
59
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.RecyclerView
import com.xhkjedu.xh_control_browser.R
import com.xhkjedu.xh_control_browser.databinding.ListItemUserBinding
import com.xhkjedu.xh_control_browser.model.Student

class StudentAdapter : RecyclerView.Adapter<StudentAdapter.MyViewHolder>() {
val stuList = mutableListOf<Student>()

fun addData(list: List<Student>) {
stuList.addAll(list)
notifyDataSetChanged()
}

fun updateDataByIndex(index: Int) {
notifyItemChanged(index)
}

fun getData(): List<Student> {
return stuList
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
// 获取 DataBinding 布局
val itemBinding: ListItemUserBinding = DataBindingUtil.inflate<ListItemUserBinding>(
LayoutInflater.from(parent.context),
R.layout.list_item_user,
parent,
false
)
// 将 DataBinding 布局设置给 ViewHolder
return MyViewHolder(itemBinding)
}

override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.itemBinding.student = stuList[position]
}

override fun getItemCount(): Int {
return stuList.size
}

class MyViewHolder : RecyclerView.ViewHolder {
/**
* RecyclerView 列表项布局文件是 item.xml
* 生成的对应的 DataBinding 类是 ItemBinding 类
* ItemBinding 类等同于布局文件
*/
lateinit var itemBinding: ListItemUserBinding

constructor(itemView: View) : super(itemView)
constructor(itemBinding: ListItemUserBinding) : super(itemBinding.root) {
this.itemBinding = itemBinding
}
}
}

Activity

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
55
56
57
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.LinearLayoutManager
import com.xhkjedu.xh_control_browser.adapter.StudentAdapter
import com.xhkjedu.xh_control_browser.databinding.ActivityMainBinding
import com.xhkjedu.xh_control_browser.model.Student
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class MainActivity : AppCompatActivity() {
// 在页面中定义协程作用域
private val coroutineScope = CoroutineScope(Dispatchers.IO)
private val adapter = StudentAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// 设置布局文件
// 布局文件是 activity_main.xml
// 该类名称生成规则是 布局文件名称 + Binding
val activityMainBinding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)

// 设置 RecyclerView 的 布局管理器 / 数据适配器
activityMainBinding.recyclerView.layoutManager = LinearLayoutManager(this)
activityMainBinding.recyclerView.adapter = adapter
loadData()
}

fun loadData() {
coroutineScope.launch {
var i = 0
while (i < 5) {
withContext(Dispatchers.Main) {
if (i == 0) {
adapter.addData(
listOf(
Student("Jerry", 12),
Student("Mickey", 16),
Student("Donald", 14)
)
)
} else {
adapter.getData()[0].age += 1
adapter.updateDataByIndex(0)
}
}

delay(3000)
i++
}

}
}
}

这里更改属性的时候UI并没有刷新

要想自动刷新

可以把上面的实体使用ObservableField包一下

1
class Student(var name: ObservableField(String), var age: ObservableField(Int){}

这样赋值的时候也要调用对应的方法才行,比较麻烦。

建议在页面上再使用ObservableField包裹。

SwipeRefreshLayout结合

添加依赖

1
2
3
dependencies {
implementation(libs.androidx.swiperefreshlayout)
}

libs.versions.toml

1
2
3
4
5
[versions]
swiperefreshlayout = "1.1.0"

[libraries]
androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swiperefreshlayout" }

刷新状态绑定类

RefreshUtils.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import android.graphics.Color;

import androidx.databinding.BindingAdapter;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;

public class RefreshUtils {
@BindingAdapter("refreshing")
public static void setRefreshing(SwipeRefreshLayout view, boolean refreshing) {
//设置加载动画背景颜色
view.setProgressBackgroundColorSchemeColor(Color.parseColor("#f3f3f3"));
//设置进度动画的颜色
view.setColorSchemeResources(android.R.color.holo_blue_bright, android.R.color.holo_green_light, android.R.color.holo_orange_light, android.R.color.holo_red_light);
if(view.isRefreshing() != refreshing){
view.setRefreshing(refreshing);
}
}
}

创建ViewModel

BaseViewModel.kt

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
import androidx.databinding.ObservableBoolean
import androidx.databinding.ObservableField
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

open class BaseViewModel : ViewModel() {
@JvmField
var zIsShowMsg = ObservableBoolean(false)
@JvmField
var zShowMsgContent = ObservableField("")

@JvmField
val isLoading = ObservableBoolean(false)

fun mLaunch(block: suspend () -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
try {
block()
} catch (e: Exception) {
withContext(Dispatchers.Main){
zIsShowMsg.set(true)
zShowMsgContent.set("接口请求失败")
}
}
}
}

fun loadDataWithLoading(block: suspend () -> Unit){
viewModelScope.launch(Dispatchers.IO) {
try {
isLoading.set(true)
block()
} catch (e: Exception) {
withContext(Dispatchers.Main){
zIsShowMsg.set(true)
zShowMsgContent.set("接口请求失败")
}
}
isLoading.set(false)
}
}
}

MainViewModel.kt

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
import android.util.Log
import android.view.View
import androidx.databinding.ObservableArrayList
import androidx.databinding.ObservableBoolean
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import com.google.gson.Gson
import com.xhkjedu.xh_control_browser.api.ApiManager
import com.xhkjedu.xh_control_browser.model.VisitModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class MainViewModel : BaseViewModel() {
private val TAG = "MainViewModel"

val visitList: ObservableArrayList<VisitModel> = ObservableArrayList()

fun loadData() {
loadDataWithLoading {
val result = ApiManager.appService.browserListPad().body()
withContext(Dispatchers.Main) {
result?.let {
if (result.code == 0) {
visitList.clear()
visitList.addAll(result.obj)

Log.i(TAG, "loadData: " + Gson().toJson(visitList))
zIsShowMsg.set(false)
} else {
zIsShowMsg.set(true)
zShowMsgContent.set(result.msg)
}
}
}
}
}

fun onRefreshListener(): OnRefreshListener {
return OnRefreshListener {
loadData()
}
}

fun onClick(view: View?) {
Log.i(TAG, "onClick")
isLoading.set(false)
}
}

XML

activity_main.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
<?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.xhkjedu.xh_control_browser.vm.MainViewModel" />
<variable
name="viewModel"
type="MainViewModel" />
</data>

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh_view"
onRefreshListener="@{viewModel.onRefreshListener()}"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:refreshing="@{viewModel.isLoading}">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</RelativeLayout>
</layout>

Activity

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
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.xhkjedu.xh_control_browser.adapter.VisitAdapter
import com.xhkjedu.xh_control_browser.databinding.ActivityMainBinding
import com.xhkjedu.xh_control_browser.vm.MainViewModel

class MainActivity : AppCompatActivity() {
private var TAG = "MainActivity"
private val adapter = VisitAdapter()
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
// 设置布局文件
// 布局文件是 activity_main.xml
// 该类名称生成规则是 布局文件名称 + Binding
val activityMainBinding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
activityMainBinding.viewModel = viewModel
// 设置 RecyclerView 的 布局管理器 / 数据适配器
activityMainBinding.recyclerView.layoutManager = LinearLayoutManager(this)
activityMainBinding.recyclerView.adapter = adapter
activityMainBinding.lifecycleOwner = this
adapter.setData(viewModel.visitList)
viewModel.loadData()
}
}

XML上的值绑定

数据绑定

字符串

1
@{student.name}

数字转字符串

1
@{String.valueOf(student.age)}

布尔转字符串

1
@{enabled ? "真" :"假"}

字符串拼接

1
@{"年龄:"+person.age}

双向绑定

1
@={content}

显示

1
2
3
4
5
6
7
8
9
10
11
<data>
<import type="android.view.View" />
<import type="com.xhkjedu.xh_control_browser.vm.MainViewModel" />
<variable name="viewModel" type="MainViewModel" />
</data>

<androidx.cardview.widget.CardView
android:layout_width="400dp"
android:layout_height="400dp"
android:visibility="@{viewModel.zIsShowMsg ? View.VISIBLE : View.GONE}">
</androidx.cardview.widget.CardView>

方法绑定

不带参数

Activity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MainActivity extends AppCompatActivity {
ActivityMainBinding activityMainBinding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activityMainBinding = DataBindingUtil.setContentView(this,R.layout.activity_main);
activityMainBinding.setPresenter(new Presenter());
}

public class Presenter{
public void onClick(View view){
Log.i("Presenter","onClick");
}
}
}

布局中

在布局文件中,data节点设置该点击事件对象,然后在控件的android:onClick="@{presenter.onClick}"属性中设置绑定即可。

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
<?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="presenter"
type="com.example.databinding.MainActivity.Presenter" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="147dp"
android:layout_marginBottom="75dp"
android:text="Button"
android:onClick="@{presenter.onClick}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

带参数

1
2
3
4
5
public  class Presenter{
public void onClick(Person person){
Log.i("Presenter","onClick" + person.name);
}
}

XML中

1
2
3
4
5
6
7
8
9
10
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="147dp"
android:layout_marginBottom="75dp"
android:text="Button"
android:onClick="@{()->presenter.onClick(person)}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />

高级用法

DataBinding支持在普通方法上添加@注解来添加自定义控件属性,该方法需满足以下条件:

修饰方法, 要求方法必须public static
方法参数第一个要求必须是View
方法名不作要求

示例:

1
2
3
4
5
6
@BindingAdapter("imageurl")
public static void bindImageUrl(ImageView view,String url){
Glide.with(view)
.load(url)
.into(view);
}

使用方法如下:

1
2
3
4
5
6
<ImageView
android:id="@+id/imageView"
android:layout_width="150dp"
android:layout_height="150dp"
app:imageurl="@{item.head}"
/>

ViewBinding与DataBinding区别

1)ViewBinding

ViewBinding会根据xml布局文件自动生成对应的XXXBinding类,然后通过XXXBinding.inflate(layoutInflater)生成一个对应的binding对象, 这个binding对象包含了这个xml布局文件中具有 ID 的所有视图对象,可以直接引用,省去了findViewById的操作。 binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root)

2)DataBinding

DataBinding是一个数据绑定库,它将xml布局中的界面组件绑定到代码中的数据对象, 可以通过对实体字段添@Bindable注解结合notifyPropertyChanged()实现双向绑定,也可以通过对自定义view添加带@BindingAdapter注解的方法来实现自定义属性。 将xml改成databinding 布局后,这样就可以直接绑定并注入xml了: binding = DataBindingUtil.setContentView(this, R.layout.activity_xxx)

通过导包了解,ViewBinding自动生成的XXXBinding也属于DataBinding,也就是DataBinding包含了ViewBinding。