【Android】ViewModel

一、为什么要引入ViewModel?

背景:Activity 存在配置变更销毁重建(屏幕旋转、系统字体缩放、语言切换),同时 Activity 生命周期受系统控制。

也就是普通的活动有以下局限:

  1. 屏幕旋转,Activity 销毁重建,内存中的临时数据全部丢失 比如聊天页输入的文字、列表加载好的数据,一转屏幕就没了。(原生方案:onSaveInstanceState保存数据,但是它只适合存少量简单数据,不适合大对象、列表数据。)
  2. Activity 职责过重 Activity 既要管 UI 绘制、点击事件,又要写业务逻辑、网络请求、数据处理。MVC 模式下 Activity 又当 View 又当 Controller,代码臃肿。
  3. 异步任务容易内存泄漏 在 Activity 里开子线程 / 网络请求,线程还没执行完,Activity 已经销毁。回调持有 Activity 引用 → 内存泄漏、空指针崩溃。 比如以前的 AsyncTask 就大量踩这个坑。

二、ViewModel 解决了什么

  1. 生命周期独立于 Activity,配置变更不会销毁 ViewModel 对象的生命周期:从 Activity 创建开始,直到 Activity 真正完全销毁(finish)才销毁 。 屏幕旋转重建 Activity,ViewModel不会重建,实例保留,内存里的数据完好保留。
  2. 分离业务逻辑和 UI,为 MVVM 铺路 把业务逻辑、数据处理、网络调用放到 ViewModel。 Activity/Fragment 只负责 UI:设置数据、处理点击,不写复杂业务。实现 UI 与业务解耦。
  3. 避免内存泄漏 ViewModel绝对不要持有 Activity/Fragment、View 的引用。 业务逻辑放在 ViewModel,异步回调结束直接操作内部 LiveData,不持有页面,页面销毁 ViewModel 跟着销毁,规避泄漏。
  4. Fragment 之间共享数据 同一个 Activity 下多个 Fragment,可以共用同一个 ViewModel 实例,实现 Fragment 之间通信,不需要接口回调、Bundle 传值。

三、ViewModel的生命周期

XML 复制代码
Activity创建 → ViewModel初始化
    ↓
屏幕旋转(Activity销毁重建)
    ↓
旧Activity销毁 → ViewModel被保留(不销毁)→ 新Activity获取同一个ViewModel实例
    ↓
Activity真正销毁(finish()或系统回收)→ ViewModel被清理(调用onCleared())

ViewModel的作用域绑定到ViewModelStoreOwner (如Activity/Fragment)的生命周期,它会一直保留在内存中,知道其ViewModelStoreOwner永久消失。

核心原理: Activity在配置变更(如旋转)被销毁时,会先调用retainNonConfigurationInstances() 获取旧Activity中的数据,其中包含ViewModel实例。这个数据会临时储存,等新的Activity创建时再传递过去,从而实现ViewModel的恢复。


四、项目理解:计数器界面

我们用最简单的计数器场景把前面的理论落地:页面有一个文本显示数字,一个按钮点击一次数字+1。我们先来看不用ViewModel的痛点,再看引入ViewModel后的解决效果。

1. 传统写法,不用ViewModel的问题

先写Activity代码,很简单:

java 复制代码
public class CounterActivity extends AppCompatActivity {
    // 计数器数据直接定义在Activity里
    private int count = 0;
    private TextView tvCount;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_counter);
        
        tvCount = findViewById(R.id.tv_count);
        Button btnAdd = findViewById(R.id.btn_add);
        
        btnAdd.setOnClickListener(v -> {
            count++;
            tvCount.setText(String.valueOf(count));
        });
    }
}

现在我们点击一次按钮,上面的数字就加一了,现在看似完成了任务,但此时我们旋转屏幕、切换系统语言或者调整字体大小,就会导致Activity销毁并重建,onCreate会重新执行,因此count就会被重置为0,数字归零。

这就是第一个痛点:配置变更导致内存临时数据全部丢失

原生的补救方案是用onSaveInstanceState

java 复制代码
public class MainActivity extends AppCompatActivity {

    private int count = 0;
    private TextView tvCount;

    @Override
    public void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt("count", count);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });

        tvCount = findViewById(R.id.tv_count);
        Button btnAdd = findViewById(R.id.btn_add);

        if(savedInstanceState != null) {
            count = savedInstanceState.getInt("count");
            tvCount.setText(String.valueOf(count));
        }

        btnAdd.setOnClickListener(v -> {
            count++;
            tvCount.setText(String.valueOf(count));
        });
    }
}

这次我们旋转屏幕后,计数就不会消失了。

这种方法的固有局限:

  • 只能存少量、可序列化的简单数据(int、String、轻量对象)
  • 无法保存大列表、Bitmap、正在执行的网络请求
  • 每个页面都要手写保存和恢复逻辑,繁琐且易出错

2. 引入ViewModel后的标准实现

2.1 编写ViewModel类

新建CounterViewModel并继承ViewModel,把数据和业务逻辑从Activity迁移到这里:

java 复制代码
public class CounterViewModel extends ViewModel {
    //计数器数据,存放在ViewModel中
    public int count = 0;

    //加法业务逻辑
    public void add() {
        count++;
    }

    //ViewModel销毁时回调,可用于释放资源
    @Override
    protected void onCleared() {
        super.onCleared();
    }
}

2.2 Activity中获取并使用ViewModel

注意:绝对不要直接new CounterViewModel() ,必须通过ViewModelProvider获取实例,这是保证实例复用的关键。

java 复制代码
public class MainActivity extends AppCompatActivity {

    private TextView tvCount;
    private CounterViewModel viewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });

        tvCount = findViewById(R.id.tv_count);
        Button btnAdd = findViewById(R.id.btn_add);

        //通过ViewModelProvider获取实例,绑定当前Activity
        viewModel = new ViewModelProvider(this).get(CounterViewModel.class);

        //初始化显示
        tvCount.setText(String.valueOf(viewModel.count));

        //点击事件只做UI触发,业务逻辑交给ViewModel
        btnAdd.setOnClickListener(v -> {
            viewModel.add();
            tvCount.setText(String.valueOf(viewModel.count));
        });
    }
}

这时候我们发现他也可以实现旋转屏幕数据不丢失的效果。

背后的逻辑:

  • 旋转时旧Activity执行销毁,但ViewModel实例不会被回收
  • 新Activity重建后,再次执行new ViewModelProvider...拿到的是同一个旧实例
  • 实例内部的count值完整保留,因此数据不会丢失
相关推荐
hunterandroid17 小时前
Android WebView JSBridge 治理实战:从线上白屏崩溃到协议化通信
android·前端
一个用户名i17 小时前
【Compose 系列】第 1 篇:认识 Compose,为什么要学它
android·android jetpack
极客猴子18 小时前
iPhone实时转写软件推荐:会议录音功能真实体验
android·人工智能·飞书
达令哥18 小时前
告别 ARouter!基于 Google 官方 Navigation 3 + KSP 打造 Compose 时代的双轨制路由框架
android·前端
YF021119 小时前
Android App启动与权限管控
android
嘟哩DuliDuli19 小时前
AI 短剧生成为什么要有角色库、场景库和镜头卡
android·人工智能·安全·ai·软件工程
NINO21 小时前
深度解析 Android ANR:从标准分析流程到 NativePollOnce 疑难破局
android
阿巴斯甜21 小时前
AppFunctions 完整介绍
android
蜡台21 小时前
Jetpack Compose 从入门到精通
android·前端·kotlin·compose·jepack