liveData,是啥我都不知道 我以前只会一些 adpter vieHolder 这种方式开发,所以给我写一个关于 liveData ,mvvm 等相关的 ,详细的,适合新手学习的markdown 文档?
Android LiveData + MVVM 新手入门教程(基于XML+Java)
文档说明
- 适用人群 :仅掌握
Adapter/ViewHolder传统安卓开发,不了解MVVM、LiveData、ViewModel的纯新手 - 技术栈 :Java + XML布局(贴合你的开发习惯,无Kotlin/Compose,零额外学习成本)
- 核心目标:理解MVVM的核心思想,掌握LiveData+ViewModel的基础使用,解决传统开发的痛点
- 学习节奏:从「传统开发痛点」入手 → 理解核心概念 → 分步实战 → 避坑指南,循序渐进无跳跃
一、先搞懂:为什么要学 LiveData + MVVM?(传统开发的痛点)
你之前用「Activity/Fragment + Adapter/ViewHolder」的开发方式,核心是把所有逻辑写在Activity/Fragment中,比如:
- 初始化XML控件、设置Adapter
- 发起网络请求/读取本地数据
- 数据解析后更新UI、刷新RecyclerView
- 处理按钮点击、生命周期回调
这种方式在简单页面没问题,但页面复杂后会出现4个致命痛点,也是LiveData+MVVM要解决的核心问题:
痛点1:代码耦合严重,维护困难
Activity既管UI展示,又管业务逻辑,还管数据请求,几千行代码堆在一起,改一个按钮逻辑可能影响网络请求,排查bug要翻遍整个类。
痛点2:屏幕旋转/系统回收,数据丢失
比如Activity中请求网络拿到数据后,屏幕旋转 (Activity会销毁重建),之前的请求数据、列表数据会全部丢失,需要手动用Bundle保存,代码繁琐且容易漏。
痛点3:容易内存泄漏,导致APP崩溃
比如子线程发起网络请求,请求还没结束,Activity已经被销毁,但若子线程持有Activity的引用,会导致Activity无法被GC回收,引发内存泄漏,最终OOM崩溃。
痛点4:手动刷新UI,代码繁琐
数据更新后(比如网络请求返回、按钮点击),需要手动调用textView.setText()、adapter.notifyDataSetChanged()刷新UI,代码分散,容易遗漏导致UI和数据不一致。
而 LiveData + MVVM 就是安卓官方推出的「解耦+生命周期安全」的解决方案,从根源上解决这些问题,且学习成本极低,完全基于你已掌握的知识延伸。
二、核心概念:MVVM 架构(通俗理解,无抽象术语)
MVVM是一种软件架构设计思想,核心是「分层解耦」,把原本写在Activity中的代码拆分成3个独立的层,每层只做自己的事,层与层之间通过固定方式交互。
MVVM 三层核心分工(通俗比喻)
把开发一个页面比作「做一道菜」:
- View(视图层) :对应「盘子+菜的摆盘」→ 安卓中就是 Activity/Fragment + XML布局 ,只负责展示UI、接收用户点击,不做任何业务逻辑和数据处理;
- ViewModel(视图模型层) :对应「厨师」→ 安卓中就是自定义的
XXXViewModel类,负责处理业务逻辑、管理数据、协调数据来源(网络/数据库/本地),是View和Model的中间桥梁; - Model(模型层) :对应「食材+调料」→ 安卓中就是实体类(Bean)+ 数据来源 (比如网络请求工具、数据库、本地SP),只负责数据的存储和获取,不关心数据怎么展示、怎么处理。
MVVM 层间交互规则(核心,必须遵守)
- View ←→ ViewModel :View可以调用ViewModel的方法(比如按钮点击触发ViewModel更新数据),ViewModel通过可观察数据(LiveData) 通知View更新UI,ViewModel绝对不能持有View的引用(比如Activity/Fragment对象);
- ViewModel ←→ Model:ViewModel可以调用Model的方法获取/存储数据(比如调用网络请求工具拿数据),Model通过回调把数据返回给ViewModel;
- View × Model :View和Model永远不直接交互,所有数据流转必须经过ViewModel。
传统开发 vs MVVM 开发(直观对比)
| 开发方式 | 代码存放位置 | 屏幕旋转数据是否丢失 | 内存泄漏风险 | UI刷新方式 |
|---|---|---|---|---|
| 传统开发 | 所有代码堆在Activity/Fragment | 是,需手动保存 | 高,易持有引用 | 手动调用setText/notifyDataSetChanged |
| MVVM开发 | 分层存放(View/ViewModel/Model) | 否,ViewModel自动保存 | 低,生命周期独立 | 数据驱动UI,自动刷新 |
三、ViewModel 详解:MVVM的「核心桥梁」
1. 什么是ViewModel?
ViewModel是Jetpack架构组件的核心类,专门负责存放和处理与UI相关的数据和业务逻辑 ,其核心特性是:ViewModel的生命周期独立于Activity/Fragment。
2. ViewModel 核心优势(解决传统开发痛点2)
用一张图直观理解ViewModel和Activity的生命周期关系:
- Activity创建 → ViewModel创建;
- Activity屏幕旋转/重建 → ViewModel不会销毁,数据完全保留;
- Activity真正销毁(比如按返回键退出)→ ViewModel才会销毁。
简单说:ViewModel是「数据保险箱」,只要页面没真正退出,数据就不会丢,无需手动用Bundle保存,彻底解决屏幕旋转数据丢失的问题。
3. ViewModel 基础使用规则
- ViewModel是抽象类 ,使用时需自定义子类继承
ViewModel; - 不能直接用
new XXXViewModel()创建实例,必须通过ViewModelProvider获取(保证生命周期管理); - ViewModel绝对不能持有任何UI层引用(比如Activity、Fragment、TextView、Context等),否则会导致内存泄漏;
- ViewModel中可通过
viewModelScope处理协程(Java中可用子线程),无需手动管理线程生命周期。
四、LiveData 详解:「带生命周期的可观察数据容器」
1. 什么是LiveData?
LiveData是Jetpack架构组件的类,本质是一个「持有数据的容器」,但它有两个核心特性,让它成为View和ViewModel之间的「数据通信桥梁」:
- 可观察:数据发生变化时,会自动通知所有「观察它的对象」(比如Activity);
- 生命周期感知 :能自动感知View层(Activity/Fragment)的生命周期,只向活跃状态的View发送数据更新。
2. LiveData 通俗比喻
把LiveData比作**「带智能提醒的快递柜」**:
- 快递柜里的「快递」就是LiveData持有的数据;
- ViewModel是「快递员」,负责往快递柜里放快递(更新数据);
- Activity/Fragment是「收件人」,负责「观察」快递柜(订阅LiveData);
- 只有收件人处于「在家(活跃状态:Started/Resumed)」,快递柜才会发「取件提醒」(通知数据更新);
- 收件人「出门(非活跃状态:Paused)」,快递柜不发提醒;
- 收件人「搬家(销毁:Destroyed)」,快递柜自动取消收件人的订阅,不再发任何提醒。
3. LiveData 核心特性(解决传统开发痛点3/4)
特性1:生命周期感知,自动取消订阅(无内存泄漏)
Activity观察LiveData时,会把自己的生命周期传给LiveData,当Activity销毁时,LiveData会自动移除观察者,不会再发送数据更新,也不会持有Activity的引用,彻底解决内存泄漏问题。
特性2:数据驱动UI,自动刷新(无需手动调用)
当ViewModel通过LiveData更新数据后,LiveData会自动通知活跃的Activity,Activity只需在「数据变化的回调」中更新UI即可,无需手动调用setText()、notifyDataSetChanged(),代码更简洁。
特性3:数据粘性,自动恢复最新数据
当Activity因屏幕旋转重建后,重新观察LiveData时,LiveData会自动把最新的一次数据发送给Activity,Activity无需重新请求数据,直接刷新UI即可。
特性4:主线程安全,无需手动切换线程
LiveData的setValue()(主线程更新)和postValue()(子线程更新)会自动保证数据更新的回调在主线程执行,Activity在回调中可直接更新UI,无需手动用Handler切换线程。
4. LiveData 关键子类和方法
(1)核心子类:MutableLiveData
LiveData是抽象类 ,无法直接使用,实际开发中用它的子类MutableLiveData,提供了数据更新的方法,二者的关系:
LiveData:不可变,只有观察方法,没有更新数据的方法;MutableLiveData:可变 ,继承LiveData,增加了setValue()和postValue()方法,用于更新数据。
(2)两个核心更新方法:setValue() vs postValue()
必须牢记,新手最易踩坑 ,二者的区别仅在于调用线程不同:
| 方法 | 调用线程 | 作用 | 适用场景 |
|---|---|---|---|
setValue(T value) |
只能在主线程调用 | 直接更新LiveData的数据,立即通知观察者 | 按钮点击、UI线程的逻辑处理等主线程场景 |
postValue(T value) |
可在子线程调用 | 内部通过Handler切换到主线程,再调用setValue() |
网络请求、子线程数据解析、数据库查询等子线程场景 |
核心原则 :子线程更新数据,一律用postValue();主线程更新数据,用setValue()。
(3)观察方法:observe(LifecycleOwner, Observer)
Activity/Fragment通过observe()方法观察LiveData,参数说明:
- 第一个参数:
LifecycleOwner→ 生命周期持有者,Activity/Fragment天生实现了该接口,直接传this即可; - 第二个参数:
Observer→ 观察者,数据变化时会回调onChanged()方法,在该方法中更新UI。
5. LiveData 封装原则(数据安全,必须遵守)
为了保证数据不被View层随意修改,ViewModel中定义LiveData时,必须遵循**「私有可变,对外不可变」**的原则:
- 私有成员变量:用
MutableLiveData(可变,可更新数据); - 对外提供的方法/变量:用
LiveData(不可变,仅能观察,不能更新数据)。
这样可以保证所有数据更新都必须通过ViewModel的业务方法,避免View层直接修改数据,导致数据变化不可追溯。
五、环境准备:添加核心依赖
LiveData+ViewModel是Jetpack组件,需在模块级build.gradle (通常是app/build.gradle)中添加依赖,无需引入任何第三方库,添加后同步即可。
完整依赖代码(直接复制)
gradle
dependencies {
// 基础依赖(你原本就有的,保留)
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'com.google.android.material:material:1.11.0'
// 核心:LiveData + ViewModel 依赖(必须添加,最新稳定版)
implementation "androidx.lifecycle:lifecycle-viewmodel:2.7.0"
implementation "androidx.lifecycle:lifecycle-livedata:2.7.0"
}
android {
compileSdk 34 // 建议≥31,最低可到21,不影响使用
defaultConfig {
applicationId "com.xxx.mvvm_demo" // 你的包名
minSdk 21 // 最低兼容安卓5.0,几乎覆盖所有设备
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
}
}
六、基础实战1:简单页面(TextView+按钮)
实现一个最简单的页面:XML包含「TextView展示数据」+「两个按钮(同步/异步更新数据)」,通过LiveData+ViewModel实现数据更新,全程遵循MVVM分层思想。
需求说明
- 页面初始化时,TextView显示「初始数据:Hello MVVM」;
- 点击「同步更新按钮」,TextView显示「主线程更新:我是同步数据」;
- 点击「异步更新按钮」,模拟子线程网络请求,1秒后TextView显示「子线程更新:我是异步数据」;
- 屏幕旋转后,数据不丢失,TextView保持最新内容。
步骤1:创建Model层(数据实体/数据来源)
本案例数据简单,无需网络/数据库,直接用字符串数据,Model层仅创建一个常量类(模拟数据仓库),后续可替换为网络请求工具。
java
// 包名:com.xxx.mvvm_demo.model
public class DataModel {
// 模拟本地/网络数据
public static final String SYNC_DATA = "主线程更新:我是同步数据";
public static final String ASYNC_DATA = "子线程更新:我是异步数据";
}
步骤2:创建ViewModel层(核心,处理逻辑+持有LiveData)
自定义MainViewModel继承ViewModel,遵循LiveData封装原则,实现数据更新的业务方法。
java
// 包名:com.xxx.mvvm_demo.viewmodel
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.xxx.mvvm_demo.model.DataModel;
// 自定义ViewModel,继承官方ViewModel
public class MainViewModel extends ViewModel {
// 1. 私有可变LiveData:仅ViewModel内部可修改(封装原则)
private MutableLiveData<String> _contentData = new MutableLiveData<>();
// 2. 对外暴露不可变LiveData:View层仅能观察,不能修改(封装原则)
public LiveData<String> getContentData() {
return _contentData;
}
// 构造方法:初始化数据
public MainViewModel() {
// 主线程初始化,用setValue()
_contentData.setValue("初始数据:Hello MVVM");
}
// 业务方法1:同步更新数据(主线程调用)
public void updateSyncData() {
_contentData.setValue(DataModel.SYNC_DATA);
}
// 业务方法2:异步更新数据(模拟网络请求,子线程)
public void updateAsyncData() {
new Thread(new Runnable() {
@Override
public void run() {
// 模拟网络请求延迟1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 子线程更新数据,必须用postValue()
_contentData.postValue(DataModel.ASYNC_DATA);
}
}).start();
}
}
步骤3:编写View层(XML布局)
在res/layout/下创建activity_main.xml,包含TextView和两个Button,传统XML写法,无任何特殊配置。
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dp">
<!-- 展示LiveData中的数据 -->
<TextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:textColor="#333333"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<!-- 同步更新按钮 -->
<Button
android:id="@+id/btn_sync"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="同步更新数据"
app:layout_constraintTop_toBottomOf="@id/tv_content"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="30dp"/>
<!-- 异步更新按钮 -->
<Button
android:id="@+id/btn_async"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="异步更新数据"
app:layout_constraintTop_toBottomOf="@id/btn_sync"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="15dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
步骤4:编写View层(Activity,仅做绑定+观察)
Activity作为View层,只做3件事 :初始化控件、获取ViewModel、观察LiveData+绑定点击事件,无任何业务逻辑,代码极简。
java
// 包名:com.xxx.mvvm_demo
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import com.xxx.mvvm_demo.viewmodel.MainViewModel;
public class MainActivity extends AppCompatActivity {
// 1. 声明控件和ViewModel
private TextView tvContent;
private Button btnSync, btnAsync;
private MainViewModel mMainViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 2. 初始化XML控件(传统findViewById,你熟悉的方式)
initView();
// 3. 获取ViewModel实例(必须用ViewModelProvider,不能new)
mMainViewModel = new ViewModelProvider(this).get(MainViewModel.class);
// 4. 核心:观察LiveData,数据变化时自动更新UI
observeLiveData();
// 5. 绑定按钮点击事件,调用ViewModel的业务方法
bindClickEvent();
}
/**
* 初始化控件(仅做控件查找,无其他逻辑)
*/
private void initView() {
tvContent = findViewById(R.id.tv_content);
btnSync = findViewById(R.id.btn_sync);
btnAsync = findViewById(R.id.btn_async);
}
/**
* 观察LiveData,数据变化时更新UI
*/
private void observeLiveData() {
// 调用LiveData的observe方法,传this(LifecycleOwner)和Observer
mMainViewModel.getContentData().observe(this, new Observer<String>() {
@Override
public void onChanged(String newData) {
// 数据变化的回调,仅做一件事:更新UI
tvContent.setText(newData);
}
});
}
/**
* 绑定点击事件,转发给ViewModel处理(Activity不处理业务逻辑)
*/
private void bindClickEvent() {
// 同步更新按钮
btnSync.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mMainViewModel.updateSyncData();
}
});
// 异步更新按钮
btnAsync.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mMainViewModel.updateAsyncData();
}
});
}
}
步骤5:运行测试(验证核心特性)
- 启动APP,TextView显示「初始数据:Hello MVVM」;
- 点击「同步更新按钮」,TextView立即更新为「主线程更新:我是同步数据」;
- 点击「异步更新按钮」,1秒后TextView更新为「子线程更新:我是异步数据」;
- 旋转屏幕,TextView依然显示最新的异步数据,无丢失;
- 退到后台再回到前台,数据正常,无崩溃。
本案例核心亮点
- Activity代码不到100行,仅做绑定,无任何业务逻辑,后续修改数据只需改ViewModel;
- 屏幕旋转数据不丢失,无需手动保存;
- 子线程更新数据无需Handler,
postValue()自动切换主线程; - Activity销毁后,LiveData自动取消订阅,无内存泄漏。
七、基础实战2:结合 RecyclerView(贴合实际开发)
你最熟悉Adapter/ViewHolder,本实战实现LiveData+ViewModel更新RecyclerView数据 ,是实际开发中最常用的场景,核心逻辑和基础实战一致,仅需在ViewModel中持有LiveData<List<实体类>>。
需求说明
- 页面包含一个RecyclerView,展示水果列表;
- 点击「添加水果按钮」,向列表中添加一个水果,RecyclerView自动刷新;
- 点击「清空列表按钮」,清空列表,RecyclerView自动刷新;
- 屏幕旋转后,列表数据不丢失。
步骤1:创建Model层(水果实体类)
java
// 包名:com.xxx.mvvm_demo.model
public class Fruit {
private String name; // 水果名称
private int iconRes; // 水果图标资源ID
// 构造方法
public Fruit(String name, int iconRes) {
this.name = name;
this.iconRes = iconRes;
}
// Getter方法(View层需要获取数据,无Setter,保证数据不可变)
public String getName() {
return name;
}
public int getIconRes() {
return iconRes;
}
}
步骤2:创建View层(RecyclerView的Adapter/ViewHolder)
传统写法,和你之前的开发方式完全一致,无任何LiveData/ViewModel相关代码,适配性100%。
java
// 包名:com.xxx.mvvm_demo.adapter
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.xxx.mvvm_demo.R;
import com.xxx.mvvm_demo.model.Fruit;
import java.util.List;
// 传统Adapter,和你之前写的完全一致
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.FruitViewHolder> {
private Context mContext;
private List<Fruit> mFruitList;
// 构造方法:传入上下文和数据列表
public FruitAdapter(Context context, List<Fruit> fruitList) {
this.mContext = context;
this.mFruitList = fruitList;
}
// 更新数据的方法(供Activity调用)
public void setFruitList(List<Fruit> fruitList) {
this.mFruitList = fruitList;
notifyDataSetChanged(); // 刷新列表
}
@NonNull
@Override
public FruitViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(mContext).inflate(R.layout.item_fruit, parent, false);
return new FruitViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull FruitViewHolder holder, int position) {
Fruit fruit = mFruitList.get(position);
holder.ivIcon.setImageResource(fruit.getIconRes());
holder.tvName.setText(fruit.getName());
}
@Override
public int getItemCount() {
return mFruitList == null ? 0 : mFruitList.size();
}
// 传统ViewHolder
static class FruitViewHolder extends RecyclerView.ViewHolder {
ImageView ivIcon;
TextView tvName;
public FruitViewHolder(@NonNull View itemView) {
super(itemView);
ivIcon = itemView.findViewById(R.id.iv_fruit_icon);
tvName = itemView.findViewById(R.id.tv_fruit_name);
}
}
}
步骤3:编写RecyclerView的item布局(item_fruit.xml)
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="60dp"
android:gravity="center_vertical"
android:paddingHorizontal="15dp"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_fruit_icon"
android:layout_width="40dp"
android:layout_height="40dp"
android:scaleType="centerCrop"/>
<TextView
android:id="@+id/tv_fruit_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:textSize="18sp"/>
</LinearLayout>
步骤4:创建ViewModel层(持有LiveData<List>)
遵循封装原则,实现添加/清空水果的业务方法,数据更新后通过LiveData通知View层。
java
// 包名:com.xxx.mvvm_demo.viewmodel
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.xxx.mvvm_demo.R;
import com.xxx.mvvm_demo.model.Fruit;
import java.util.ArrayList;
import java.util.List;
public class FruitViewModel extends ViewModel {
// 私有可变LiveData:持有水果列表
private MutableLiveData<List<Fruit>> _fruitList = new MutableLiveData<>();
// 对外暴露不可变LiveData
public LiveData<List<Fruit>> getFruitList() {
return _fruitList;
}
// 构造方法:初始化默认数据
public FruitViewModel() {
List<Fruit> defaultList = new ArrayList<>();
defaultList.add(new Fruit("苹果", R.mipmap.ic_apple));
defaultList.add(new Fruit("香蕉", R.mipmap.ic_banana));
defaultList.add(new Fruit("橙子", R.mipmap.ic_orange));
// 初始化数据,主线程用setValue()
_fruitList.setValue(defaultList);
}
// 业务方法1:添加水果
public void addFruit() {
// 先获取当前列表(LiveData的getValue()获取最新数据)
List<Fruit> currentList = _fruitList.getValue();
if (currentList == null) {
currentList = new ArrayList<>();
}
// 添加新水果
currentList.add(new Fruit("葡萄", R.mipmap.ic_grape));
// 更新数据
_fruitList.setValue(currentList);
}
// 业务方法2:清空列表
public void clearFruitList() {
_fruitList.setValue(new ArrayList<>());
}
}
步骤5:编写View层(XML布局+Activity)
(1)XML布局(activity_fruit.xml)
包含RecyclerView和两个按钮。
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_fruit"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/ll_button"/>
<LinearLayout
android:id="@+id/ll_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:padding="10dp"
app:layout_constraintBottom_toBottomOf="parent">
<Button
android:id="@+id/btn_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="添加水果"
android:layout_marginEnd="20dp"/>
<Button
android:id="@+id/btn_clear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="清空列表"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
(2)Activity代码(仅做绑定+观察)
java
// 包名:com.xxx.mvvm_demo
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import com.xxx.mvvm_demo.adapter.FruitAdapter;
import com.xxx.mvvm_demo.model.Fruit;
import com.xxx.mvvm_demo.viewmodel.FruitViewModel;
import java.util.List;
public class FruitActivity extends AppCompatActivity {
private RecyclerView rvFruit;
private Button btnAdd, btnClear;
private FruitAdapter mFruitAdapter;
private FruitViewModel mFruitViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fruit);
initView();
initRecyclerView();
getViewModel();
observeLiveData();
bindClickEvent();
}
// 初始化控件
private void initView() {
rvFruit = findViewById(R.id.rv_fruit);
btnAdd = findViewById(R.id.btn_add);
btnClear = findViewById(R.id.btn_clear);
}
// 初始化RecyclerView(传统写法)
private void initRecyclerView() {
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
rvFruit.setLayoutManager(layoutManager);
mFruitAdapter = new FruitAdapter(this, null);
rvFruit.setAdapter(mFruitAdapter);
}
// 获取ViewModel
private void getViewModel() {
mFruitViewModel = new ViewModelProvider(this).get(FruitViewModel.class);
}
// 观察LiveData,数据变化时更新Adapter
private void observeLiveData() {
mFruitViewModel.getFruitList().observe(this, new Observer<List<Fruit>>() {
@Override
public void onChanged(List<Fruit> fruitList) {
// 数据变化,调用Adapter的set方法更新列表
mFruitAdapter.setFruitList(fruitList);
}
});
}
// 绑定点击事件
private void bindClickEvent() {
btnAdd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mFruitViewModel.addFruit();
}
});
btnClear.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mFruitViewModel.clearFruitList();
}
});
}
}
步骤6:运行测试
- 启动APP,RecyclerView显示默认的3种水果;
- 点击「添加水果」,列表自动添加「葡萄」,无需手动调用
notifyDataSetChanged(); - 点击「清空列表」,列表自动清空;
- 旋转屏幕,列表数据保持最新,无丢失。
八、LiveData + MVVM 最佳实践(新手必须遵守)
1. 分层严格解耦,各司其职
- View层(Activity/Fragment):只做UI相关操作(初始化控件、观察LiveData、绑定点击事件),不写任何业务逻辑、不直接操作数据、不持有Model层引用;
- ViewModel层:只做业务逻辑和数据管理,不持有任何UI层引用、不直接更新UI、不处理生命周期;
- Model层:只做数据的获取和存储,不关心数据怎么展示、怎么处理。
2. LiveData 封装原则:私有可变,对外不可变
java
// 正确写法
private MutableLiveData<String> _data = new MutableLiveData<>();
public LiveData<String> getData() {
return _data;
}
// 错误写法(View层可直接修改数据)
public MutableLiveData<String> data = new MutableLiveData<>();
3. 正确获取 ViewModel 实例
必须用ViewModelProvider获取,不能直接new:
java
// 正确
MainViewModel viewModel = new ViewModelProvider(this).get(MainViewModel.class);
// 错误(无法管理生命周期,屏幕旋转会重新创建,数据丢失)
MainViewModel viewModel = new MainViewModel();
4. 严格区分 setValue() 和 postValue()
- 主线程(按钮点击、UI逻辑):用
setValue(); - 子线程(网络请求、子线程解析):用
postValue(); - 禁止子线程调用
setValue(),会直接崩溃。
5. ViewModel 中不持有 UI 层引用
ViewModel中绝对不能持有 Activity、Fragment、Context、TextView、RecyclerView等UI层对象,若需上下文,可使用Application Context(通过ViewModel的构造方法传入,需自定义ViewModelFactory),新手阶段尽量避免在ViewModel中使用上下文。
6. 单一职责:一个ViewModel对应一个页面/功能
不要把所有页面的逻辑都写在一个ViewModel中,比如「首页ViewModel」「我的页面ViewModel」「水果列表ViewModel」,各自独立,便于维护。
7. 可选优化:用ViewBinding替代findViewById
传统findViewById容易出现空指针,安卓官方推荐ViewBinding ,自动生成控件绑定代码,简化开发,开启方式:
在模块级build.gradle中添加:
gradle
android {
buildFeatures {
viewBinding true
}
}
使用方式(以MainActivity为例):
java
// 自动生成的绑定类,布局名转驼峰+Binding
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 绑定布局
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// 直接通过binding获取控件,无需findViewById
binding.tvContent.setText("Hello ViewBinding");
binding.btnSync.setOnClickListener(v -> { /* 点击逻辑 */ });
}
九、新手常见坑 & 解决方案
坑1:子线程调用setValue()导致崩溃
现象 :子线程中调用liveData.setValue(),APP崩溃,报错CalledFromWrongThreadException;
原因 :setValue()只能在主线程调用;
解决方案 :子线程一律用postValue()。
坑2:直接new ViewModel导致屏幕旋转数据丢失
现象 :旋转屏幕后,数据恢复为初始值;
原因 :直接new的ViewModel无法被系统管理,屏幕旋转时会重新创建;
解决方案 :用ViewModelProvider获取ViewModel实例。
坑3:ViewModel持有Activity引用导致内存泄漏
现象 :APP长时间运行后崩溃,内存泄漏检测工具提示Activity泄漏;
原因 :ViewModel生命周期比Activity长,持有Activity引用会导致Activity无法被GC回收;
解决方案:ViewModel中绝对不持有Activity、Fragment、Context等UI层引用。
坑4:LiveData数据更新后,UI不刷新
现象 :调用了setValue()/postValue(),但Activity的onChanged()不回调;
常见原因:
- 忘记调用
liveData.observe()方法观察数据; - 观察时传的LifecycleOwner不是Activity/Fragment(比如传了ApplicationContext);
- 更新的是集合/对象的内部属性,而非集合/对象本身(比如
list.add()后直接调用setValue(list),LiveData认为数据未变化,不回调);
解决方案:
- 确保调用了
observe(),且第一个参数是this(Activity/Fragment); - 更新集合时,创建新的集合对象(比如
new ArrayList<>(oldList)),再调用setValue()。
坑5:Activity重建后,LiveData重复观察
现象 :屏幕旋转后,数据更新时onChanged()回调多次;
原因 :在onCreate()中观察LiveData,Activity重建时会再次调用observe(),添加多个观察者;
解决方案 :onCreate()是观察LiveData的正确位置,LiveData会自动移除旧的观察者,若仍出现多次回调,检查是否手动添加了多个观察者。
十、总结 & 后续学习路线
核心知识点回顾
- MVVM:分层解耦的架构思想,分为View(Activity+XML)、ViewModel(核心桥梁)、Model(数据层),层间严格遵循交互规则;
- ViewModel:生命周期独立于Activity,是数据保险箱,负责业务逻辑和数据管理,不能持有UI引用;
- LiveData:带生命周期的可观察数据容器,是View和ViewModel的通信桥梁,核心特性是生命周期感知、自动取消订阅、数据驱动UI;
- 核心配合:ViewModel持有LiveData,View层观察LiveData,数据变化时自动更新UI,全程遵循「数据驱动UI」。
后续学习路线(循序渐进,基于本教程)
- ViewBinding:替代findViewById,简化控件初始化,避免空指针;
- Retrofit + ViewModel + LiveData:实现网络请求(Model层),结合ViewModel处理网络回调,LiveData通知UI更新;
- Room + ViewModel + LiveData:实现本地数据库,Room支持直接返回LiveData,数据变化时自动通知UI;
- ViewModelFactory:解决ViewModel中需要传入参数(比如Application Context)的问题;
- SingleLiveEvent:解决LiveData数据粘性的问题(部分场景不需要粘性数据);
- Kotlin + Flow/StateFlow:进阶的状态管理方案,贴合安卓官方最新推荐。
最终目标
掌握LiveData+ViewModel后,你会发现安卓开发变得简洁、安全、易维护,彻底告别传统开发的代码耦合、内存泄漏、数据丢失等痛点,为后续学习Compose、Jetpack其他组件打下坚实的基础。
记住:MVVM不是银弹,而是一种思想,核心是「解耦」,哪怕是简单页面,坚持分层开发,你的代码会越来越优雅。