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搭配使用),它简化了保存状态的过程。
相关推荐
毕设源码-钟学长2 小时前
【开题答辩全过程】以 Android的传统中医诊断管理系统为例,包含答辩的问题和答案
android
脚踏实地,坚持不懈!2 小时前
Android,Jetpack Compose,坦克大战游戏案例Demo
android·游戏
yzpyzp3 小时前
kotlin的函数前面增加suspend关键字的作用
android·开发语言·kotlin
jiet_h3 小时前
Android Kotlin ObjectAnimator 和 ValueAnimator 全面解析
android·开发语言·kotlin
Android技术之家3 小时前
Kotlin与Compose:Android开发的现代化变革
android·java·开发语言·kotlin
诸神黄昏EX4 小时前
Android SystemServer 系列专题【篇五:UserController用户状态控制】
android
2501_916007474 小时前
Transporter App 使用全流程详解:iOS 应用 ipa 上传工具、 uni-app 应用发布指南
android·ios·小程序·https·uni-app·iphone·webview
叽哥5 小时前
Kotlin学习第 5 课:Kotlin 面向对象编程:类、对象与继承
android·java·kotlin
叽哥5 小时前
Kotlin学习第 6 课:Kotlin 集合框架:操作数据的核心工具
android·java·kotlin