android中ViewModel 和 onSaveInstanceState 的最佳使用方法

核心区别:设计目的与生命周期

特性 ViewModel onSaveInstanceState
设计目的 持有和管理UI相关的数据 保存和恢复短暂的UI状态
数据范围 "大"数据:复杂对象、列表、网络请求结果、数据库查询结果等。 "小"数据:简单的、可序列化/可打包的数据(Primitive类型、String、Parcelable等)。
生命周期 存活于配置更改 (如屏幕旋转)。不存活于进程终止(如用户离开应用后系统为回收内存而杀死应用)。 存活于配置更改和进程终止。数据被写入磁盘(Bundle),即使应用被杀死也能恢复。
存储位置 内存(RAM)中。访问速度极快。 序列化后写入磁盘(Bundle)。读写有开销。
适用场景 保持屏幕旋转时的数据;在Fragment间共享数据;作为数据层的"前台缓存"。 保存滚动位置、文本框中的临时输入、选中的ID等,确保即使在应用被杀死后也能完美还原用户体验。

一个绝佳的类比:

想象在电脑上写文档。

  • ViewModel 就像 RAM。正在编辑的整个文档都在里面,操作非常快。但如果突然断电(进程被杀死),所有没保存的东西就丢了。
  • onSaveInstanceState 就像 按 Ctrl+S 快速保存。不会保存整个文档,而是会保存一些关键信息(比如光标位置、最近编辑的段落)。即使断电,重启后也能迅速恢复到刚才的状态。

最佳实践与使用方法

核心思想:二者不是替代关系,而是互补关系。 应该结合使用它们来打造最佳用户体验。

  1. 使用 ViewModel 作为数据的"单一信源"

    • 所有核心数据(从Repository、UseCase获取的数据)都应存放在 ViewModel 中。
    • UI(Activity/Fragment)观察 ViewModel 暴露的 LiveData/StateFlow 来更新界面。
  2. 使用 onSaveInstanceState 作为UI状态的"备份"

    • 仅用它来保存那些重建UI所必需的最小化状态信息,而不是完整的数据对象。
    • 例如,保存一个项目的ID,而不是整个项目对象。在重建时,用这个ID从 ViewModel 中重新获取完整数据。

代码详解与示例

假设有一个 "用户详情"页面(UserDetailActivity) ,从网络加载用户数据,并有一个可滚动的TextView

1. ViewModel 代码 (UserDetailViewModel.java)
java 复制代码
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;

public class UserDetailViewModel extends ViewModel {
    // 使用 LiveData 或 StateFlow 持有数据(核心数据)
    private MutableLiveData<User> _user = new MutableLiveData<>();
    public LiveData<User> user = _user;

    private MutableLiveData<Boolean> _isLoading = new MutableLiveData<>();
    public LiveData<Boolean> isLoading = _isLoading;

    private String userId; // 要加载的用户ID

    // 初始化加载数据
    public void init(String userId) {
        if (this.userId != null) {
            // ViewModel 已经初始化过,配置旋转时直接使用现有数据
            return;
        }
        this.userId = userId;
        loadUser(userId);
    }

    private void loadUser(String userId) {
        _isLoading.setValue(true);
        // 模拟网络请求
        UserRepository.getUser(userId, new Callback<User>() {
            @Override
            public void onSuccess(User user) {
                _isLoading.setValue(false);
                _user.setValue(user); // 数据加载成功,更新LiveData
            }

            @Override
            public void onError(Exception e) {
                _isLoading.setValue(false);
                // 处理错误
            }
        });
    }

    // 清空资源(可选)
    @Override
    protected void onCleared() {
        super.onCleared();
        // 取消正在进行的网络请求等
    }
}
2. Activity 代码 (UserDetailActivity.java)
java 复制代码
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;

import android.os.Bundle;
import android.widget.TextView;

public class UserDetailActivity extends AppCompatActivity {
    private static final String SAVE_STATE_USER_ID = "SAVE_STATE_USER_ID";
    private static final String SAVE_STATE_SCROLL_POSITION = "SAVE_STATE_SCROLL_POSITION";

    private UserDetailViewModel viewModel;
    private TextView userBioTextView;
    private String userId;
    private int scrollPosition = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_user_detail);

        userBioTextView = findViewById(R.id.tv_user_bio);

        // 1. 从 Intent 或 SavedState 中获取 userId
        if (savedInstanceState == null) {
            // 正常启动:从Intent获取
            userId = getIntent().getStringExtra("USER_ID");
        } else {
            // 重建恢复:从Bundle获取(可能是进程被杀死后)
            userId = savedInstanceState.getString(SAVE_STATE_USER_ID);
            scrollPosition = savedInstanceState.getInt(SAVE_STATE_SCROLL_POSITION, 0);
        }

        // 2. 初始化 ViewModel
        viewModel = new ViewModelProvider(this).get(UserDetailViewModel.class);
        viewModel.init(userId); // 传递ID,ViewModel内部会判断是否已加载

        // 3. 观察 ViewModel 中的数据
        viewModel.user.observe(this, user -> {
            if (user != null) {
                // 更新UI with user data
                userBioTextView.setText(user.getBio());
                // 恢复滚动位置
                userBioTextView.scrollTo(0, scrollPosition);
            }
        });

        viewModel.isLoading.observe(this, isLoading -> {
            // 显示或隐藏加载进度条
        });
    }

    // 4. 保存UI状态(用于进程终止)
    @Override
    protected void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);
        // 只保存最小必要信息:用户ID和滚动位置
        outState.putString(SAVE_STATE_USER_ID, userId);
        outState.putInt(SAVE_STATE_SCROLL_POSITION, userBioTextView.getScrollY()); // 获取当前滚动位置
    }
}

两种场景下的工作流程

场景一:屏幕旋转(配置更改)
  1. 旋转前 :Activity被销毁,onSaveInstanceState被调用,userIdscrollPosition被保存到Bundle中。
  2. 旋转后 :新Activity创建,onCreate(Bundle savedInstanceState)被调用,savedInstanceState不为null
  3. 恢复数据 :从Bundle中取出userIdscrollPosition
  4. 获取核心数据ViewModel没有被销毁 ,它仍然持有完整的User对象。新Activity获取到同一个ViewModel实例,立即观察到User数据并更新UI,然后应用滚动位置。
    • 结果 :用户体验无缝衔接,没有重复的网络请求
场景二:应用被系统杀死后恢复
  1. 被杀前onSaveInstanceState被调用,userIdscrollPosition被写入磁盘。
  2. 被杀后 :用户重新打开应用,系统重新创建Activity,并将保存的Bundle传入onCreate
  3. 恢复数据 :从Bundle中取出userIdscrollPosition
  4. 获取核心数据ViewModel是全新的、空的。viewModel.init(userId)被调用,根据保存的userId发起新的网络请求来获取完整的用户数据。
  5. 数据加载成功后,更新UI并滚动到之前的位置。
    • 结果:用户看到了和离开时几乎一样的界面,但需要等待数据重新加载。

总结与最佳方法

  1. 永远使用 ViewModel

    • 用来持有所有非UI状态的核心数据
    • 防止因配置更改导致的不必要数据重载(如网络请求、数据库查询)。
  2. 谨慎使用 onSaveInstanceState

    • 只用来保存恢复UI所必需的、轻量的、可序列化的状态(ID、位置、临时文本)。
    • 为应对进程死亡的场景提供保障。
  3. 分工合作

    • ViewModel 保证配置更改时体验流畅
    • onSaveInstanceState + ViewModel 共同保证进程死亡后体验连贯
  4. 不要滥用

    • 切勿将大型对象(如Bitmap)或复杂结构放入Bundle,这会导致TransactionTooLargeException
    • 如果UI状态非常复杂,考虑使用SavedStateHandle(与ViewModel搭配使用),它简化了保存状态的过程。
相关推荐
独自破碎E15 小时前
【BISHI9】田忌赛马
android·java·开发语言
代码s贝多芬的音符17 小时前
android 两个人脸对比 mlkit
android
darkb1rd19 小时前
五、PHP类型转换与类型安全
android·安全·php
gjxDaniel19 小时前
Kotlin编程语言入门与常见问题
android·开发语言·kotlin
csj5019 小时前
安卓基础之《(22)—高级控件(4)碎片Fragment》
android
峥嵘life20 小时前
Android16 【CTS】CtsMediaCodecTestCases等一些列Media测试存在Failed项
android·linux·学习
stevenzqzq20 小时前
Compose 中的状态可变性体系
android·compose
似霰21 小时前
Linux timerfd 的基本使用
android·linux·c++
darling3311 天前
mysql 自动备份以及远程传输脚本,异地备份
android·数据库·mysql·adb
你刷碗1 天前
基于S32K144 CESc生成随机数
android·java·数据库