Android Jetpack 页面架构实战:从 LiveData、ViewModel 到 DataBinding 的生命周期管理与数据绑定

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(),那么布局中绑定到 infotextcontent 的控件就会同步刷新。

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 里不再手动 findViewByIdsetText(),而是直接更新 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 中的日志方法,把当前的 userNamecheck 值打印出来。

布局中的按钮如下:

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());
    }
}
相关推荐
⑩-2 小时前
为什么要用消息队列?使用场景?
java·rabbitmq
似水明俊德2 小时前
01-C#.Net-泛型-面试题
java·开发语言·面试·c#·.net
leonkay2 小时前
Golang语言闭包完全指南
开发语言·数据结构·后端·算法·架构·golang
Allnadyy2 小时前
【C++项目】从零实现高并发内存池(一):核心原理与设计思路
java·开发语言·jvm
浑水摸鱼仙君2 小时前
SpringSecurity和Flux同时使用报未认证问题
java·ai·flux·springsecurity·springai
一叶飘零_sweeeet2 小时前
Java 线程模型底层解密:从内核原理到生产级架构选型,全链路实战指南
java· java线程模型
am心3 小时前
企业开发项目流程记录
java
独自破碎E4 小时前
前后端分离+微服务架构下的用户认证
java·面试·架构
hssfscv4 小时前
力扣练习训练2(java)——二叉树的中序遍历、对称二叉树、二叉树的最大深度、买卖股票的最佳时机
java·数据结构·算法