Android Jetpack 页面架构实战:从 LiveData、ViewModel 到 DataBinding 的生命周期管理与数据绑定
目录
- [Android Jetpack 实战:用 LiveData、ViewModel 与 DataBinding 处理生命周期和页面绑定](#Android Jetpack 实战:用 LiveData、ViewModel 与 DataBinding 处理生命周期和页面绑定)
- [1. 前言](#1. 前言)
- [2. 前置页面:先用普通异步更新把问题暴露出来](#2. 前置页面:先用普通异步更新把问题暴露出来)
- [2.1 页面布局](#2.1 页面布局)
- [2.2 页面中的 Activity 实现](#2.2 页面中的 Activity 实现)
- [2.3 为什么这种写法会有生命周期风险](#2.3 为什么这种写法会有生命周期风险)
- [3. Jetpack LiveData:让数据具备生命周期感知能力](#3. Jetpack LiveData:让数据具备生命周期感知能力)
- [3.1 LiveData 的基本作用](#3.1 LiveData 的基本作用)
- [3.2 添加 LiveData 依赖](#3.2 添加 LiveData 依赖)
- [3.3 使用 MutableLiveData 包装数据](#3.3 使用 MutableLiveData 包装数据)
- [3.4 在页面中观察 LiveData 的变化](#3.4 在页面中观察 LiveData 的变化)
- [4. Jetpack LiveData 与 ViewModel 结合使用](#4. Jetpack LiveData 与 ViewModel 结合使用)
- [4.1 为什么继续引入 ViewModel](#4.1 为什么继续引入 ViewModel)
- [4.2 定义 MyViewModel](#4.2 定义 MyViewModel)
- [4.3 在 Activity 中获取 ViewModel 并监听数据](#4.3 在 Activity 中获取 ViewModel 并监听数据)
- [5. Jetpack DataBinding:把布局和数据源直接关联起来](#5. Jetpack DataBinding:把布局和数据源直接关联起来)
- [5.1 在 Gradle 中启用数据绑定](#5.1 在 Gradle 中启用数据绑定)
- [5.2 把布局改造成 DataBinding Layout](#5.2 把布局改造成 DataBinding Layout)
- [5.3 在 Activity 中创建 Binding 对象](#5.3 在 Activity 中创建 Binding 对象)
- [6. LiveData 与 DataBinding 结合使用](#6. LiveData 与 DataBinding 结合使用)
- [6.1 定义 DataBindingViewModel](#6.1 定义 DataBindingViewModel)
- [6.2 在 Activity 中同时创建 DataBinding 和 ViewModel](#6.2 在 Activity 中同时创建 DataBinding 和 ViewModel)
- [6.3 将布局文件和 ViewModel 关联](#6.3 将布局文件和 ViewModel 关联)
- [6.4 绑定 ViewModel 到布局](#6.4 绑定 ViewModel 到布局)
- [6.5 ViewModel 简单类型赋值](#6.5 ViewModel 简单类型赋值)
- [6.6 ViewModel drawable 类型赋值](#6.6 ViewModel drawable 类型赋值)
- [6.7 ViewModel 点击事件赋值](#6.7 ViewModel 点击事件赋值)
- [7. DataBinding 双向绑定](#7. DataBinding 双向绑定)
- [7.1 EditText 双向绑定](#7.1 EditText 双向绑定)
- [7.2 CheckBox 双向绑定](#7.2 CheckBox 双向绑定)
- [7.3 测试双向绑定结果](#7.3 测试双向绑定结果)
- [8. 小结](#8. 小结)
- [9. 相关代码附录](#9. 相关代码附录)
- [9.1 app/build.gradle](#9.1 app/build.gradle)
- [9.2 activity_live_data_main.xml](#9.2 activity_live_data_main.xml)
- [9.3 LiveDataMainActivity.java](#9.3 LiveDataMainActivity.java)
- [9.4 MyViewModel.java](#9.4 MyViewModel.java)
- [9.5 activity_data_binding.xml](#9.5 activity_data_binding.xml)
- [9.6 DataBindingActivity.java](#9.6 DataBindingActivity.java)
- [9.7 DataBindingViewModel.java](#9.7 DataBindingViewModel.java)
1. 前言
在 Android 页面开发里,真正麻烦的通常不是把一个字符串显示到 TextView 上,而是当数据来自异步任务、页面会旋转重建、界面控件需要和数据源双向同步时,如何让数据更新、页面生命周期和布局绑定保持稳定且清晰。
这一组示例正是沿着这条主线推进的。先用一个天气信息页面模拟异步返回结果,观察普通字段直接绑定页面时会出现的问题;接着用 LiveData 把数据包装成可感知生命周期的对象;再进一步结合 ViewModel,把数据更新逻辑从 Activity 中拆出去;最后接入 DataBinding,让布局直接读取 ViewModel 中的字段,并继续扩展到图片绑定、点击事件绑定以及 EditText、CheckBox 的双向绑定。
2. 前置页面:先用普通异步更新把问题暴露出来
在正式引入 LiveData 之前,先搭一个最简单的页面。这个页面只有一个文本和一个按钮,点击按钮后,模拟每隔两秒钟从服务器拿到一条天气信息,并直接更新到页面。
2.1 页面布局
先创建页面布局。这里虽然已经套上了 <layout> 根标签,但当前这一步先只把它当作普通页面使用,核心还是中间的 TextView 和下面的按钮。
代码路径:/LiveDataAndDataBindingByJavaProject/app/src/main/res/layout/activity_live_data_main.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<RelativeLayout
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".LiveDataMainActivity">
<TextView
android:id="@+id/tv_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="暂无信息"
android:textSize="23sp" />
<Button
android:id="@+id/btn_get_weather_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/tv_info"
android:layout_centerHorizontal="true"
android:text="获取天气信息" />
</RelativeLayout>
</layout>
页面效果如下:

2.2 页面中的 Activity 实现
接下来在页面中直接写逻辑。这里先定义一个普通的 String info 作为天气信息字段,点击按钮时调用 fetchWeatherData(),然后在延迟任务中反复更新天气文本。
代码路径:/LiveDataAndDataBindingByJavaProject/app/src/main/java/com/ls/livedataanddatabindingbyjavaproject/LiveDataMainActivity.java
java
public class LiveDataMainActivity extends AppCompatActivity {
private static final String TAG = "LiveDataMainActivity";
private TextView tvInfo;
private String info; //天气信息
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_live_data_main);
tvInfo = findViewById(R.id.tv_info);
findViewById(R.id.btn_get_weather_info).setOnClickListener(view -> {
fetchWeatherData();
});
}
/**
* 模拟从服务器获取到天气数据
*/
private void fetchWeatherData() {
// 2s后获取到数据
Handler handler = new Handler(Looper.getMainLooper());
handler.postDelayed(new Runnable() {
@Override
public void run() {
info = "Sunny,25℃";
Log.i(TAG, "run: 获取到天气信息: " + info);
tvInfo.setText(info);
handler.postDelayed(this, 2000);
}
}, 2000);
}
}
这段代码的执行过程非常直接:两秒后拿到 "Sunny,25℃",打印日志,并更新 tvInfo,然后继续每隔两秒重复一次。
2.3 为什么这种写法会有生命周期风险
问题恰恰出在这里。当前的 info 只是一个普通字符串,它本身完全不具备生命周期感知能力。天气信息的获取又是异步执行的,所以在等待返回结果的这段时间内,页面可能发生这些操作:
- 退出当前 Activity
- 旋转屏幕导致 Activity 重建
- 页面切到后台
一旦 Activity 已经销毁,而耗时任务还在继续执行,就会出现"页面没了,日志还在继续打"的情况。这不仅容易带来内存问题,也可能在更新页面控件时触发崩溃。
这正是下面引入 LiveData 的原因:

3. Jetpack LiveData:让数据具备生命周期感知能力
LiveData 的作用,不是简单地替代字符串或者对象本身,而是给数据增加一层生命周期感知能力。这样数据变化时,可以通知页面更新;而页面生命周期不活跃时,又不会继续往这个页面分发更新。
3.1 LiveData 的基本作用
如果这里只有一个 String info,那么它无法感知 Activity 的生命周期。为了让这个天气信息具备感知能力,需要对它做一层 LiveData 包装。
可以把这个过程理解成两部分:
- 把原来的
info包装成可观察的数据对象 - 告诉这个数据对象,当前是由哪一个 Activity 或组件来观察它
这样每当 info 发生变化时,LiveData 会负责把变化通知给页面;同时当页面生命周期不活跃时,LiveData 不会继续通知这个页面更新 UI。
对应关系如下图所示:

3.2 添加 LiveData 依赖
LiveData 是 Android Jetpack 的一部分,先要在 Gradle 里加入依赖。当前工程的配置如下:
代码路径:/LiveDataAndDataBindingByJavaProject/app/build.gradle
gradle
dependencies {
//如果是kotlin项目
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1" // ViewModel 依赖
//java项目
implementation "androidx.lifecycle:lifecycle-livedata:2.6.1" // Java 支持的 LiveData 版本
implementation "androidx.lifecycle:lifecycle-viewmodel:2.6.1" // Java 支持的 ViewModel 版本
}
工程里实际启用的是 Java 版本依赖:
gradle
//java项目
implementation "androidx.lifecycle:lifecycle-livedata:2.6.1" // Java 支持的 LiveData 版本
implementation "androidx.lifecycle:lifecycle-viewmodel:2.6.1" // Java 支持的 ViewModel 版本
3.3 使用 MutableLiveData 包装数据
接下来把原来的普通字符串改成 MutableLiveData<String>。这里的泛型表示需要包装的数据类型,也就是当前对 String 类型的天气信息进行 LiveData 包装。
java
public class LiveDataMainActivity extends AppCompatActivity {
private static final String TAG = "LiveDataMainActivity";
private TextView tvInfo;
private MyViewModel viewModel;
//天气信息
private MutableLiveData<String> info = new MutableLiveData<>();
一旦使用 MutableLiveData 包装之后,更新数据的方式也要一起改变。不能继续直接给 info 赋值,而是改成 setValue() 或 postValue()。
java
/**
* 模拟从服务器获取到天气数据
*/
public void fetchWeatherData() {
//2s后获取到数据
Handler handler = new Handler(Looper.getMainLooper());
handler.postDelayed(new Runnable() {
@Override
public void run() {
//在主线程更新LiveData的值
info.setValue("Sunny,25℃");
//在后台线程
// info.postValue("Sunny,25℃");
handler.postDelayed(this, 2000);
}
}, 2000);
}
这里两种更新方式的区别也要一起保留下来:
setValue()用在主线程更新数据postValue()用在子线程更新数据
3.4 在页面中观察 LiveData 的变化
包装好之后,还要让页面去观察这个 LiveData。观察入口是 observe(owner, observer)。
java
@Override
protected void onCreate(Bundle savedInstanceState) {
// 观察 liveData(info) 的变化
info.observe(this, new Observer<String>() {
@Override
public void onChanged(String s) {
//当info发生变化的时候,这里就会收到info的新的值
//可以Ui更新、或者是做一些和info变更有关的操作
Log.i(TAG, "run: 获取到天气信息:" + s);
tvInfo.setText(s);
}
});
这里的 this 指的就是生命周期所有者,也就是当前 Activity。如果 Activity 当前不活跃,就不会触发 onChanged() 回调;只有在合适的生命周期状态下,页面才会收到数据更新。
另外,使用 LiveData 包装同一个数据还有一个直接好处:不管这个数据来自用户输入、本地数据库还是服务器,都可以统一通过 setValue() / postValue() 更新,而页面只需要观察数据变化本身,不需要追踪每一种数据来源的细节。
4. Jetpack LiveData 与 ViewModel 结合使用
仅仅有 LiveData,还只是让数据变化和生命周期之间建立了连接。如果数据获取逻辑仍然写在 Activity 里,页面还是承担了太多工作。接下来继续引入 ViewModel,把和 UI 相关的数据、以及更新数据的逻辑,从 Activity 中拆出去。
4.1 为什么继续引入 ViewModel
页面销毁和重建时,如果完全靠 Activity 自己处理,通常要用下面这两个方法来保存和恢复数据:
java
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
}
@Override
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
}
这种处理方式比较繁琐,而且页面既要负责控件初始化、又要负责异步数据更新、还要负责保存恢复状态,职责很容易混杂在一起。
更自然的方式是,把更偏 UI 相关的数据交给 ViewModel 保存和管理。这样:
- 用户通过 UI 修改数据时,交给 ViewModel
- 网络请求返回数据时,交给 ViewModel
- 本地数据库更新数据时,交给 ViewModel
而 Activity 只做一件事:观察数据变化,并据此更新 UI。
4.2 定义 MyViewModel
先定义一个继承自 ViewModel 的类,把天气信息和获取天气数据的方法都移动进去。
代码路径:/LiveDataAndDataBindingByJavaProject/app/src/main/java/com/ls/livedataanddatabindingbyjavaproject/MyViewModel.java
java
public class MyViewModel extends ViewModel {
//天气信息
private MutableLiveData<String> info = new MutableLiveData<>();
public MutableLiveData<String> getInfo() {
return info;
}
public void setInfo(MutableLiveData<String> info) {
this.info = info;
}
/**
* 模拟从服务器获取到天气数据
*/
public void fetchWeatherData() {
//2s后获取到数据
Handler handler = new Handler(Looper.getMainLooper());
handler.postDelayed(new Runnable() {
@Override
public void run() {
//在主线程更新LiveData的值
info.setValue("Sunny,25℃");
//在后台线程
// info.postValue("Sunny,25℃");
handler.postDelayed(this, 2000);
}
}, 2000);
}
}
这里的结构很明确:
- 用
MutableLiveData<String>存天气信息 - 用
getInfo()暴露给外部观察 - 把"模拟从服务器获取数据"的逻辑也一起放到 ViewModel 中
4.3 在 Activity 中获取 ViewModel 并监听数据
接下来回到页面中,先获取 ViewModel,再通过 viewModel.getInfo().observe() 监听数据变化。此时页面点击按钮后,不再直接自己发起更新,而是调用 ViewModel 内部的方法。
代码路径:/LiveDataAndDataBindingByJavaProject/app/src/main/java/com/ls/livedataanddatabindingbyjavaproject/LiveDataMainActivity.java
java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_live_data_main);
tvInfo = findViewById(R.id.tv_info);
findViewById(R.id.btn_get_weather_info).setOnClickListener(view -> {
// 从服务端加载天气信息
viewModel.fetchWeatherData();
});
viewModel = new ViewModelProvider(this).get(MyViewModel.class);
// 观察LiveData(info)的变化
// 这里的this,指的就是生命周期所有者,如果不活跃就不会出发onChange
viewModel.getInfo().observe(this, new Observer<String>() {
@Override
public void onChanged(String s) {
// 当info发生变化的时候,这里就会收到info的新的值
// 可以UI更新,或者是做一些和info变更有关的操作
Log.i(TAG, "run: 获取到天气信息: " + s);
tvInfo.setText(s);
}
});
}
这样处理之后,Activity 的职责就明显收缩了:不再直接管理天气数据,也不再直接承载异步更新逻辑,而是专注于页面控件和观察回调。
5. Jetpack DataBinding:把布局和数据源直接关联起来
有了 LiveData 和 ViewModel 之后,页面和数据流已经比较清晰了,但 Activity 里仍然有不少样板代码,例如 findViewById()、手动设置点击事件、手动把字段同步到控件。接下来引入 DataBinding,继续把布局和数据源直接连起来。
5.1 在 Gradle 中启用数据绑定
第一步是在 Gradle 里开启数据绑定。
代码路径:/LiveDataAndDataBindingByJavaProject/app/build.gradle
gradle
android {
//启用数据绑定
dataBinding {
enabled = true
}
}
当前工程的配置如下:
gradle
plugins {
alias(libs.plugins.androidApplication)
}
android {
namespace 'com.ls.livedataanddatabindingbyjavaproject'
compileSdk 34
//启用数据绑定
dataBinding {
enabled = true
}
defaultConfig {
applicationId "com.ls.livedataanddatabindingbyjavaproject"
minSdk 26
targetSdk 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
...
}
这一步打开后,编译器会根据布局文件自动生成 Binding 类。

5.2 把布局改造成 DataBinding Layout
接下来把页面布局改造成支持数据绑定的结构。写法上要点有两个:
- 在原来的布局外再包一层
<layout> - 在
<data>标签中声明数据来源类型
代码路径:/LiveDataAndDataBindingByJavaProject/app/src/main/res/layout/activity_data_binding.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="data"
type="com.ls.livedataanddatabindingbyjavaproject.DataBindingViewModel" />
</data>
<LinearLayout
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
tools:context=".DataBindingActivity">
<TextView
android:id="@+id/tv_one"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{data.info}"
tools:text="你好你好" />
<TextView
android:id="@+id/tv_two"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{data.text}" />
<TextView
android:id="@+id/tv_three"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@={data.content}" />
<ImageView
android:id="@+id/iv_picture_new"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@{data.drawable}" />
<EditText
android:id="@+id/et_user_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入用户名"
android:text="@={data.userName}" />
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="@={data.check}"
android:text="是否记住密码" />
<Button
android:id="@+id/btn_hello"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{()->data.updateInfo()}"
android:text="我是一个按钮" />
<Button
android:id="@+id/btn_hello1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{()->data.queryDataUpdate()}"
android:text="我是一个按钮2" />
</LinearLayout>
</layout>
这里已经把后面需要逐项展开的几种绑定方式都放到布局里了:简单文本绑定、图片绑定、点击事件绑定、EditText 双向绑定、CheckBox 双向绑定。
页面效果如下:

另外,如果是从普通布局快速改造成 DataBinding Layout,也可以直接用 IDE 的快捷方式先套出 <layout> 结构:

5.3 在 Activity 中创建 Binding 对象
布局改造之后,页面就不能再用普通的 setContentView() 了,而是改用 DataBindingUtil.setContentView() 来绑定布局文件,并得到一个对应的 Binding 对象。
java
public class DataBindingActivity extends AppCompatActivity {
private ActivityDataBindingBinding mBinding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_data_binding);
//创建DataBinding
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_data_binding);
TextView tvOne = mBinding.tvOne;
}
}
这个 ActivityDataBindingBinding 的命名,就是根据当前布局和 Activity 名称生成的。拿到 mBinding 之后,就可以直接访问布局中定义了 id 的控件。
如果这里遇到 ActivityDataBindingBinding 报错,而导包又没有问题,通常不是真的写错了,而是布局刚改完后编译器还没来得及重新生成类,重新编译一次项目即可。

6. LiveData 与 DataBinding 结合使用
只有 DataBinding 的布局和 Binding 对象还不够,真正要让页面和数据联动起来,还需要把 ViewModel 中的 LiveData 字段和布局文件中的表达式连接起来。
6.1 定义 DataBindingViewModel
先定义一个专门给 DataBinding 页面使用的 ViewModel,把页面中每一个需要显示、输入、回写的字段都定义出来。
代码路径:/LiveDataAndDataBindingByJavaProject/app/src/main/java/com/ls/livedataanddatabindingbyjavaproject/DataBindingViewModel.java
java
public class DataBindingViewModel extends ViewModel {
private static final String TAG = "DataBindingViewModel";
private MutableLiveData<String> info = new MutableLiveData<>();
private MutableLiveData<String> text = new MutableLiveData<>();
private MutableLiveData<String> content = new MutableLiveData<>();
private MutableLiveData<Integer> resId = new MutableLiveData<>();
private MutableLiveData<String> userName = new MutableLiveData<>();
private MutableLiveData<Boolean> check = new MutableLiveData<>();
private MutableLiveData<Drawable> drawable = new MutableLiveData<>();
private Context mContext;
public void setContext(Context context) {
this.mContext = context;
}
public MutableLiveData<String> getInfo() {
return info;
}
public void setInfo(MutableLiveData<String> info) {
this.info = info;
}
public MutableLiveData<String> getText() {
return text;
}
public void setText(MutableLiveData<String> text) {
this.text = text;
}
public MutableLiveData<String> getContent() {
return content;
}
public void setContent(MutableLiveData<String> content) {
this.content = content;
}
public MutableLiveData<Integer> getResId() {
return resId;
}
public void setResId(MutableLiveData<Integer> resId) {
this.resId = resId;
}
public MutableLiveData<Drawable> getDrawable() {
return drawable;
}
public MutableLiveData<String> getUserName() {
return userName;
}
public MutableLiveData<Boolean> getCheck() {
return check;
}
/**
* 假装更新了服务器的信息
*/
public void updateInfo() {
this.info.setValue("你好啊");
this.text.setValue("我很好");
this.content.setValue("haha!!");
this.drawable.setValue(ContextCompat.getDrawable(mContext, R.mipmap.ic_launcher));
}
/**
* 查询双向绑定数据是否有影响
*/
public void queryDataUpdate() {
Log.i(TAG, "queryDataUpdate: userName = " + userName.getValue() + " check = " + check.getValue());
}
}
这一层定义得很细,因为后面布局里的每一个表达式都要直接对上这里的字段或方法。
6.2 在 Activity 中同时创建 DataBinding 和 ViewModel
接下来回到 Activity 中,先把 DataBinding 和 ViewModel 都创建出来。
代码路径:/LiveDataAndDataBindingByJavaProject/app/src/main/java/com/ls/livedataanddatabindingbyjavaproject/DataBindingActivity.java
java
public class DataBindingActivity extends AppCompatActivity {
private DataBindingViewModel mViewModel;
private ActivityDataBindingBinding mBinding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_data_binding);
//创建DataBinding
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_data_binding);
//创建viewModel
mViewModel = new ViewModelProvider(this).get(DataBindingViewModel.class);
mViewModel.setContext(this);
}
}
这里先通过 DataBindingUtil.setContentView() 创建 mBinding,再通过 new ViewModelProvider(this).get(DataBindingViewModel.class) 获取 mViewModel。由于后面需要在 ViewModel 中设置图片资源,所以还额外把 Context 传给了 ViewModel。
6.3 将布局文件和 ViewModel 关联
布局文件里虽然已经通过 <variable> 声明了 data 的类型,但此时还只是告诉布局"将来这里会接一个 DataBindingViewModel 类型的数据",还没有真的把当前 Activity 中的 mViewModel 绑定进去。
布局文件中的声明如下:
xml
<data>
<variable
name="data"
type="com.ls.livedataanddatabindingbyjavaproject.DataBindingViewModel" />
</data>
而控件真正读取数据时,用的是:
xml
<TextView
android:id="@+id/tv_one"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{data.info}"
tools:text="你好你好" />
这里 @{data.info} 表示从 data 这个 ViewModel 中读取 info 字段。由于布局预览阶段不一定能真正拿到运行时数据,所以额外配了 tools:text,这样在预览 UI 时也能直接看到文本效果。
6.4 绑定 ViewModel 到布局
前面 DataBinding 和 ViewModel 都定义好了,接下来还要做最后两步绑定:
- 把
mViewModel绑定到mBinding - 把
mBinding和当前 Activity 的生命周期绑定起来
java
//绑定viewModel到布局
mBinding.setData(mViewModel);
//为了和LiveData联动、考虑当前组件的生命周期状态
mBinding.setLifecycleOwner(this);
完整写法如下:
java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_data_binding);
//创建DataBinding
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_data_binding);
//创建viewModel
mViewModel = new ViewModelProvider(this).get(DataBindingViewModel.class);
mViewModel.setContext(this);
//绑定viewModel到布局
mBinding.setData(mViewModel);
//为了和LiveData联动、考虑当前组件的生命周期状态
mBinding.setLifecycleOwner(this);
}
只有这两步都完成之后,布局文件里的 LiveData 字段变化,才会真正和页面联动起来。
6.5 ViewModel 简单类型赋值
先看简单类型的联动更新。当前 Activity 里,如果主动调用 ViewModel 的 updateInfo(),那么布局中绑定到 info、text、content 的控件就会同步刷新。
java
//绑定viewModel到布局
mBinding.setData(mViewModel);
//为了和LiveData联动、考虑当前组件的生命周期状态
mBinding.setLifecycleOwner(this);
mBinding.btnHello.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//请求服务器
mViewModel.updateInfo();
}
});
对应的 ViewModel 方法如下:
java
/**
* 假装更新了服务器的信息
*/
public void updateInfo() {
this.info.setValue("你好啊");
this.text.setValue("我很好");
this.content.setValue("haha!!");
this.drawable.setValue(ContextCompat.getDrawable(mContext, R.mipmap.ic_launcher));
}
这一步的重点是,Activity 里不再手动 findViewById 再 setText(),而是直接更新 ViewModel,布局自己根据绑定关系刷新 UI。


6.6 ViewModel drawable 类型赋值
接下来单独把图片绑定这一项拆开说明。虽然它和上面的 updateInfo() 是同一个方法里完成的,但这里的讲解重点已经切换成"如何把 Drawable 类型数据绑定到布局中的 ImageView"。
布局中的写法如下:
xml
<ImageView
android:id="@+id/iv_picture_new"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@{data.drawable}" />
ViewModel 中对应的字段如下:
java
public class DataBindingViewModel extends ViewModel {
private MutableLiveData<Drawable> drawable = new MutableLiveData<>();
// drawable get、set
private Context mContext;
public void setContext(Context context) {
this.mContext = context;
}
/**
* 假装更新了服务器的信息
*/
public void updateInfo() {
this.drawable.setValue(ContextCompat.getDrawable(mContext, R.mipmap.ic_launcher));
}
而在 Activity 中,还要额外把上下文传入 ViewModel:
java
//创建viewModel
mViewModel = new ViewModelProvider(this).get(DataBindingViewModel.class);
mViewModel.setContext(this);
这样按钮触发 updateInfo() 之后,图片资源也会跟着一起更新。
6.7 ViewModel 点击事件赋值
再继续把点击事件这一项单独拆开。这里的重点不是"按钮点击后更新了什么",而是"点击事件本身也可以直接通过布局表达式写到 ViewModel 方法上"。
布局里的写法如下:
xml
<Button
android:id="@+id/btn_hello"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{()->data.updateInfo()}"
android:text="我是一个按钮" />
这样点击按钮之后,会直接调用 ViewModel 中的:
java
public void updateInfo() {
this.info.setValue("你好啊");
this.text.setValue("我很好");
this.content.setValue("haha!!");
this.drawable.setValue(ContextCompat.getDrawable(mContext, R.mipmap.ic_launcher));
}
这意味着按钮点击的处理逻辑不一定要再写一遍 setOnClickListener(),完全可以直接声明在布局里。
7. DataBinding 双向绑定
前面使用的 @{...} 都是单向绑定,也就是 ViewModel 把数据提供给布局。接下来继续扩展到双向绑定,让布局中的输入变化也能直接回写到 ViewModel。
7.1 EditText 双向绑定
EditText 的关键写法是 @={...},相比 @{...} 多了一个等号。这个等号表示:不仅 ViewModel 可以把值传给控件,控件的值变化也会同步回写到 ViewModel。
xml
<EditText
android:id="@+id/et_user_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入用户名"
android:text="@={data.userName}" />
ViewModel 中对应的字段如下:
java
private MutableLiveData<String> userName = new MutableLiveData<>();
// get
这样输入框里一旦输入内容,userName 就会同步发生变化。
7.2 CheckBox 双向绑定
CheckBox 的双向绑定同样使用 @={...},只不过对应的属性从 text 换成了 checked。
xml
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="@={data.check}"
android:text="是否记住密码" />
ViewModel 中对应的字段如下:
java
private MutableLiveData<Boolean> check = new MutableLiveData<>();
// get
现在 CheckBox 的勾选状态一发生变化,check 这个 LiveData 也会同步变化。
7.3 测试双向绑定结果
最后再保留一个单独的小节,用来验证双向绑定到底有没有生效。这里专门放了第二个按钮,点击后直接调用 ViewModel 中的日志方法,把当前的 userName 和 check 值打印出来。
布局中的按钮如下:
xml
<Button
android:id="@+id/btn_hello1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{()->data.queryDataUpdate()}"
android:text="我是一个按钮2" />
ViewModel 中对应的方法如下:
java
/**
* 查询双向绑定数据是否有影响
*/
public void queryDataUpdate() {
Log.i(TAG, "queryDataUpdate: userName = " + userName.getValue() + " check = " + check.getValue());
}
只要输入框里已经输入了用户名,CheckBox 的勾选状态也已经变化,那么点击这个按钮之后,日志里打印出来的值就会和页面上的当前状态保持一致。这样就能验证 EditText 和 CheckBox 的双向绑定链路已经真正打通。
8. 小结
这一组示例是按问题暴露、逐步引入解决方案的顺序展开的。
先通过一个最普通的天气信息页面,看到了异步数据直接写在 Activity 里时会遇到的生命周期问题;再用 LiveData 给数据增加生命周期感知能力;接着结合 ViewModel,把数据保存和更新逻辑从 Activity 中拆出去;最后再引入 DataBinding,让布局能够直接和 ViewModel 中的 LiveData 字段建立绑定,并继续扩展到图片、点击事件以及双向绑定。
从页面开发的角度看,LiveData 解决的是"数据变化怎么安全地通知页面",ViewModel 解决的是"数据和更新逻辑放在哪里",DataBinding 解决的是"布局如何直接声明自己依赖哪些数据"。这三者配合起来之后,页面的数据流、生命周期处理和控件绑定关系都会变得清晰很多。
9. 相关代码附录
9.1 app/build.gradle
代码路径:/LiveDataAndDataBindingByJavaProject/app/build.gradle
gradle
plugins {
alias(libs.plugins.androidApplication)
}
android {
namespace 'com.ls.livedataanddatabindingbyjavaproject'
compileSdk 34
//启用数据绑定
dataBinding {
enabled = true
}
defaultConfig {
applicationId "com.ls.livedataanddatabindingbyjavaproject"
minSdk 26
targetSdk 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation libs.appcompat
implementation libs.material
implementation libs.activity
implementation libs.constraintlayout
testImplementation libs.junit
androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core
//java项目
implementation "androidx.lifecycle:lifecycle-livedata:2.6.1" // Java 支持的 LiveData 版本
implementation "androidx.lifecycle:lifecycle-viewmodel:2.6.1" // Java 支持的 ViewModel 版本
}
9.2 activity_live_data_main.xml
代码路径:/LiveDataAndDataBindingByJavaProject/app/src/main/res/layout/activity_live_data_main.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<RelativeLayout
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".LiveDataMainActivity">
<TextView
android:id="@+id/tv_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="暂无信息"
android:textSize="23sp" />
<Button
android:id="@+id/btn_get_weather_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/tv_info"
android:layout_centerHorizontal="true"
android:text="获取天气信息" />
</RelativeLayout>
</layout>
9.3 LiveDataMainActivity.java
代码路径:/LiveDataAndDataBindingByJavaProject/app/src/main/java/com/ls/livedataanddatabindingbyjavaproject/LiveDataMainActivity.java
java
public class LiveDataMainActivity extends AppCompatActivity {
private static final String TAG = "LiveDataMainActivity";
private TextView tvInfo;
private MyViewModel viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_live_data_main);
tvInfo = findViewById(R.id.tv_info);
findViewById(R.id.btn_get_weather_info).setOnClickListener(view -> {
//从服务端加载天气信息
viewModel.fetchWeatherData();
});
viewModel = new ViewModelProvider(this).get(MyViewModel.class);
//观察liveData(info)的变化
//这里的this,指的就是生命周期所有者,如果不活跃就不会出发onChange
viewModel.getInfo().observe(this, new Observer<String>() {
@Override
public void onChanged(String s) {
//当info发生变化的时候,这里就会收到info的新的值
//可以Ui更新、或者是做一些和info变更有关的操作
Log.i(TAG, "run: 获取到天气信息:" + s);
tvInfo.setText(s);
}
});
}
}
9.4 MyViewModel.java
代码路径:/LiveDataAndDataBindingByJavaProject/app/src/main/java/com/ls/livedataanddatabindingbyjavaproject/MyViewModel.java
java
public class MyViewModel extends ViewModel {
//天气信息
private MutableLiveData<String> info = new MutableLiveData<>();
public MutableLiveData<String> getInfo() {
return info;
}
public void setInfo(MutableLiveData<String> info) {
this.info = info;
}
/**
* 模拟从服务器获取到天气数据
*/
public void fetchWeatherData() {
//2s后获取到数据
Handler handler = new Handler(Looper.getMainLooper());
handler.postDelayed(new Runnable() {
@Override
public void run() {
//在主线程更新LiveData的值
info.setValue("Sunny,25℃");
//在后台线程
// info.postValue("Sunny,25℃");
handler.postDelayed(this, 2000);
}
}, 2000);
}
}
9.5 activity_data_binding.xml
代码路径:/LiveDataAndDataBindingByJavaProject/app/src/main/res/layout/activity_data_binding.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="data"
type="com.ls.livedataanddatabindingbyjavaproject.DataBindingViewModel" />
</data>
<LinearLayout
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
tools:context=".DataBindingActivity">
<TextView
android:id="@+id/tv_one"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{data.info}"
tools:text="你好你好" />
<TextView
android:id="@+id/tv_two"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{data.text}" />
<TextView
android:id="@+id/tv_three"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@={data.content}" />
<ImageView
android:id="@+id/iv_picture_new"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@{data.drawable}" />
<EditText
android:id="@+id/et_user_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入用户名"
android:text="@={data.userName}" />
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="@={data.check}"
android:text="是否记住密码" />
<Button
android:id="@+id/btn_hello"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{()->data.updateInfo()}"
android:text="我是一个按钮" />
<Button
android:id="@+id/btn_hello1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{()->data.queryDataUpdate()}"
android:text="我是一个按钮2" />
</LinearLayout>
</layout>
9.6 DataBindingActivity.java
代码路径:/LiveDataAndDataBindingByJavaProject/app/src/main/java/com/ls/livedataanddatabindingbyjavaproject/DataBindingActivity.java
java
public class DataBindingActivity extends AppCompatActivity {
private DataBindingViewModel mViewModel;
private ActivityDataBindingBinding mBinding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_data_binding);
//创建DataBinding
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_data_binding);
//创建viewModel
mViewModel = new ViewModelProvider(this).get(DataBindingViewModel.class);
mViewModel.setContext(this);
//绑定viewModel到布局
mBinding.setData(mViewModel);
//为了和LiveData联动、考虑当前组件的生命周期状态
mBinding.setLifecycleOwner(this);
mBinding.btnHello.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//请求服务器
mViewModel.updateInfo();
}
});
}
}
9.7 DataBindingViewModel.java
代码路径:/LiveDataAndDataBindingByJavaProject/app/src/main/java/com/ls/livedataanddatabindingbyjavaproject/DataBindingViewModel.java
java
public class DataBindingViewModel extends ViewModel {
private static final String TAG = "DataBindingViewModel";
private MutableLiveData<String> info = new MutableLiveData<>();
private MutableLiveData<String> text = new MutableLiveData<>();
private MutableLiveData<String> content = new MutableLiveData<>();
private MutableLiveData<Integer> resId = new MutableLiveData<>();
private MutableLiveData<String> userName = new MutableLiveData<>();
private MutableLiveData<Boolean> check = new MutableLiveData<>();
private MutableLiveData<Drawable> drawable = new MutableLiveData<>();
private Context mContext;
public void setContext(Context context) {
this.mContext = context;
}
public MutableLiveData<String> getInfo() {
return info;
}
public void setInfo(MutableLiveData<String> info) {
this.info = info;
}
public MutableLiveData<String> getText() {
return text;
}
public void setText(MutableLiveData<String> text) {
this.text = text;
}
public MutableLiveData<String> getContent() {
return content;
}
public void setContent(MutableLiveData<String> content) {
this.content = content;
}
public MutableLiveData<Integer> getResId() {
return resId;
}
public void setResId(MutableLiveData<Integer> resId) {
this.resId = resId;
}
public MutableLiveData<Drawable> getDrawable() {
return drawable;
}
public MutableLiveData<String> getUserName() {
return userName;
}
public MutableLiveData<Boolean> getCheck() {
return check;
}
/**
* 假装更新了服务器的信息
*/
public void updateInfo() {
this.info.setValue("你好啊");
this.text.setValue("我很好");
this.content.setValue("haha!!");
this.drawable.setValue(ContextCompat.getDrawable(mContext, R.mipmap.ic_launcher));
}
/**
* 查询双向绑定数据是否有影响
*/
public void queryDataUpdate() {
Log.i(TAG, "queryDataUpdate: userName = " + userName.getValue() + " check = " + check.getValue());
}
}