Android LiveData + MVVM 新手入门教程(基于 XML+Java)

liveData,是啥我都不知道 我以前只会一些 adpter vieHolder 这种方式开发,所以给我写一个关于 liveData ,mvvm 等相关的 ,详细的,适合新手学习的markdown 文档?

Android LiveData + MVVM 新手入门教程(基于XML+Java)

文档说明

  1. 适用人群 :仅掌握 Adapter/ViewHolder 传统安卓开发,不了解MVVM、LiveData、ViewModel的纯新手
  2. 技术栈Java + XML布局(贴合你的开发习惯,无Kotlin/Compose,零额外学习成本)
  3. 核心目标:理解MVVM的核心思想,掌握LiveData+ViewModel的基础使用,解决传统开发的痛点
  4. 学习节奏:从「传统开发痛点」入手 → 理解核心概念 → 分步实战 → 避坑指南,循序渐进无跳跃

一、先搞懂:为什么要学 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 层间交互规则(核心,必须遵守)

  1. View ←→ ViewModel :View可以调用ViewModel的方法(比如按钮点击触发ViewModel更新数据),ViewModel通过可观察数据(LiveData) 通知View更新UI,ViewModel绝对不能持有View的引用(比如Activity/Fragment对象);
  2. ViewModel ←→ Model:ViewModel可以调用Model的方法获取/存储数据(比如调用网络请求工具拿数据),Model通过回调把数据返回给ViewModel;
  3. 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 基础使用规则

  1. ViewModel是抽象类 ,使用时需自定义子类继承ViewModel
  2. 不能直接用new XXXViewModel()创建实例,必须通过ViewModelProvider获取(保证生命周期管理);
  3. ViewModel绝对不能持有任何UI层引用(比如Activity、Fragment、TextView、Context等),否则会导致内存泄漏;
  4. 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时,必须遵循**「私有可变,对外不可变」**的原则:

  1. 私有成员变量:用MutableLiveData(可变,可更新数据);
  2. 对外提供的方法/变量:用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分层思想。

需求说明

  1. 页面初始化时,TextView显示「初始数据:Hello MVVM」;
  2. 点击「同步更新按钮」,TextView显示「主线程更新:我是同步数据」;
  3. 点击「异步更新按钮」,模拟子线程网络请求,1秒后TextView显示「子线程更新:我是异步数据」;
  4. 屏幕旋转后,数据不丢失,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:运行测试(验证核心特性)

  1. 启动APP,TextView显示「初始数据:Hello MVVM」;
  2. 点击「同步更新按钮」,TextView立即更新为「主线程更新:我是同步数据」;
  3. 点击「异步更新按钮」,1秒后TextView更新为「子线程更新:我是异步数据」;
  4. 旋转屏幕,TextView依然显示最新的异步数据,无丢失;
  5. 退到后台再回到前台,数据正常,无崩溃。

本案例核心亮点

  1. Activity代码不到100行,仅做绑定,无任何业务逻辑,后续修改数据只需改ViewModel;
  2. 屏幕旋转数据不丢失,无需手动保存;
  3. 子线程更新数据无需Handler,postValue()自动切换主线程;
  4. Activity销毁后,LiveData自动取消订阅,无内存泄漏。

七、基础实战2:结合 RecyclerView(贴合实际开发)

你最熟悉Adapter/ViewHolder,本实战实现LiveData+ViewModel更新RecyclerView数据 ,是实际开发中最常用的场景,核心逻辑和基础实战一致,仅需在ViewModel中持有LiveData<List<实体类>>

需求说明

  1. 页面包含一个RecyclerView,展示水果列表;
  2. 点击「添加水果按钮」,向列表中添加一个水果,RecyclerView自动刷新;
  3. 点击「清空列表按钮」,清空列表,RecyclerView自动刷新;
  4. 屏幕旋转后,列表数据不丢失。

步骤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:运行测试

  1. 启动APP,RecyclerView显示默认的3种水果;
  2. 点击「添加水果」,列表自动添加「葡萄」,无需手动调用notifyDataSetChanged()
  3. 点击「清空列表」,列表自动清空;
  4. 旋转屏幕,列表数据保持最新,无丢失。

八、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()不回调;
常见原因

  1. 忘记调用liveData.observe()方法观察数据;
  2. 观察时传的LifecycleOwner不是Activity/Fragment(比如传了ApplicationContext);
  3. 更新的是集合/对象的内部属性,而非集合/对象本身(比如list.add()后直接调用setValue(list),LiveData认为数据未变化,不回调);
    解决方案
  • 确保调用了observe(),且第一个参数是this(Activity/Fragment);
  • 更新集合时,创建新的集合对象(比如new ArrayList<>(oldList)),再调用setValue()

坑5:Activity重建后,LiveData重复观察

现象 :屏幕旋转后,数据更新时onChanged()回调多次;
原因 :在onCreate()中观察LiveData,Activity重建时会再次调用observe(),添加多个观察者;
解决方案onCreate()是观察LiveData的正确位置,LiveData会自动移除旧的观察者,若仍出现多次回调,检查是否手动添加了多个观察者。

十、总结 & 后续学习路线

核心知识点回顾

  1. MVVM:分层解耦的架构思想,分为View(Activity+XML)、ViewModel(核心桥梁)、Model(数据层),层间严格遵循交互规则;
  2. ViewModel:生命周期独立于Activity,是数据保险箱,负责业务逻辑和数据管理,不能持有UI引用;
  3. LiveData:带生命周期的可观察数据容器,是View和ViewModel的通信桥梁,核心特性是生命周期感知、自动取消订阅、数据驱动UI;
  4. 核心配合:ViewModel持有LiveData,View层观察LiveData,数据变化时自动更新UI,全程遵循「数据驱动UI」。

后续学习路线(循序渐进,基于本教程)

  1. ViewBinding:替代findViewById,简化控件初始化,避免空指针;
  2. Retrofit + ViewModel + LiveData:实现网络请求(Model层),结合ViewModel处理网络回调,LiveData通知UI更新;
  3. Room + ViewModel + LiveData:实现本地数据库,Room支持直接返回LiveData,数据变化时自动通知UI;
  4. ViewModelFactory:解决ViewModel中需要传入参数(比如Application Context)的问题;
  5. SingleLiveEvent:解决LiveData数据粘性的问题(部分场景不需要粘性数据);
  6. Kotlin + Flow/StateFlow:进阶的状态管理方案,贴合安卓官方最新推荐。

最终目标

掌握LiveData+ViewModel后,你会发现安卓开发变得简洁、安全、易维护,彻底告别传统开发的代码耦合、内存泄漏、数据丢失等痛点,为后续学习Compose、Jetpack其他组件打下坚实的基础。

记住:MVVM不是银弹,而是一种思想,核心是「解耦」,哪怕是简单页面,坚持分层开发,你的代码会越来越优雅。

相关推荐
今天多喝热水2 小时前
SpEL(Spring Expression Language) 表达式
java·后端·spring
wasp5202 小时前
Hudi 客户端实现分析
java·开发语言·人工智能·hudi
Hello.Reader2 小时前
Flink 2.0 从 flink-conf.yaml 到 config.yaml 的正确打开方式(含迁移与最佳实践)
java·前端·flink
李慕婉学姐2 小时前
【开题答辩过程】以《基于uni-app的手账记录小程序的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
java·小程序·uni-app
晚霞的不甘2 小时前
Flutter for OpenHarmony:注入灵魂:购物车的数据驱动与状态管理实战
android·前端·javascript·flutter·前端框架
福大大架构师每日一题2 小时前
milvus v2.6.9 发布:支持主键搜索、段重开机制、日志性能全面提升!
android·java·milvus
独自破碎E2 小时前
【滑动窗口】最长无重复子数组
java·开发语言
GIOTTO情2 小时前
Infoseek 媒介投放系统技术实现:基于与辉同行风波的风险防控架构设计
java·架构·媒体
木井巳2 小时前
【Java】数据类型及运算符重点总结
java·开发语言