【Android 进阶】为什么你应该停止在 ViewModel `init` 中加载数据?

在 Android 应用开发中,"如何加载初始数据" 似乎是一个老生常谈的话题。然而,即使是经验丰富的开发者,常常也会在配置变更(Configuration Changes)单元测试(Unit Testing) 这两个极端之间左右为难。

你是否遇到过以下情况?

  1. 屏幕旋转后,数据又重新请求了一次,浪费了流量。
  2. 试图测试 ViewModel,但数据在实例化时就自动加载了,导致无法 Mock 初始状态。

本文将介绍一种结合了 Flow 操作符的"最佳实践",它能完美解决上述两个痛点,让你的代码既健壮又易于测试。


⛔ 常见的两种"不完美"写法

在介绍最佳方案之前,我们先快速回顾一下目前最主流的两种做法,以及它们的问题所在。

1. 这种写法:LaunchedEffect

在 Compose UI 中直接触发:

Kotlin 复制代码
@Composable
fun MyScreen(viewModel: MyViewModel) {
    LaunchedEffect(Unit) {
        viewModel.loadData()
    }
    // ...
}
  • 优点 :测试方便,因为你可以控制何时调用 loadData
  • 痛点配置变更噩梦 。当用户旋转屏幕时,Activity 重建,LaunchedEffect 重新执行,导致重复的网络请求。这不仅浪费资源,还可能导致 UI 闪烁。

2. 这种写法:ViewModel init

在 ViewModel 初始化时触发:

Kotlin 复制代码
class MyViewModel(private val repo: Repository) : ViewModel() {
    init {
        loadData()
    }
}
  • 优点 :解决了旋转问题。ViewModel 在配置变更期间是存活的,所以 init 不会重新运行。
  • 痛点测试地狱 。数据加载作为构造函数的副作用立即发生。你无法在 loadData 执行之前设置测试环境(比如 Mock 特定的流发射),这让编写精准的单元测试变得非常困难。

✅ 最佳实践:onStart + stateIn

我们需要的方案必须满足两点:

  1. 懒加载(Lazy) :只有 UI 开始监听时才加载(利于测试)。
  2. 缓存(Caching) :在屏幕旋转等短暂中断时,保持数据不重新加载。

我们可以利用 Kotlin Flow 的 onStart 操作符配合 stateInWhileSubscribed 策略来实现。

代码实现

看看这个优雅的实现方式:

Kotlin 复制代码
class MyViewModel(private val repository: MyRepository) : ViewModel() {
​
    // 1. 我们不再使用 init 块,也不使用 private MutableStateFlow
    
    val uiState: StateFlow<UiState> = repository.observeData()
        // 2. 关键点:使用 onStart 触发副作用(加载数据)
        .onStart {
            repository.refreshData() // 或者 emit(Loading)
        }
        // 3. 转换为 StateFlow
        .stateIn(
            scope = viewModelScope,
            // 4. 核心:设置 5 秒的超时缓存
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = UiState.Loading
        )
}

为什么这样做是完美的?

让我们深入分析这段代码背后的机制。

1. 解决测试难题 (onStart)

onStart 是一个中间操作符,它只有在 Flow 被收集(collected) 时才会执行。这意味着当你实例化 ViewModel 时,网络请求不会立即发生。

在单元测试中,你有充足的时间去 Mock 你的 Repository,然后再调用 viewModel.uiState.test { ... } 来触发加载。

2. 解决屏幕旋转问题 (WhileSubscribed(5000))

这是整个方案的灵魂所在。

  • 场景:用户旋转屏幕。
  • 发生的事情:旧的 Activity 销毁,停止收集 Flow;新的 Activity 创建,重新收集 Flow。
  • WhileSubscribed(5000) 的作用 :当订阅者数量从 1 变为 0 时,上游的数据流不会立即停止,而是会保持活跃状态 5 秒钟(stopTimeoutMillis)。
  • 结果 :因为屏幕旋转通常在几百毫秒内完成,新的订阅者会在 5 秒内出现。此时,Flow 认为"一直有订阅者",因此不会 重新触发 onStart,也就避免了重复请求。

3. 智能的后台行为

如果用户按 Home 键将应用退到后台,由于 WhileSubscribed 的超时机制:

  • 5秒内返回:数据无需重载,无缝衔接。
  • 超过5秒返回 :Flow 停止。当用户再次打开应用,onStart 重新执行。这其实是一个特性而非 Bug------如果用户离开很久,他们通常希望看到最新的数据,而不是陈旧的缓存。

总结

通过将数据加载逻辑从 init 块移动到 Flow 的 onStart 操作符,并结合 SharingStarted.WhileSubscribed(5000),我们达成了一个完美的平衡:

特性 LaunchedEffect ViewModel init Flow (onStart + stateIn)
抗屏幕旋转
易于单元测试
自动刷新过期数据
相关推荐
a3158238063 小时前
Android13隐藏某个App需要关注的源码文件
android·java·framework·launcher3·隐藏app
霸王大陆3 小时前
《零基础学 PHP:从入门到实战》模块十:从应用到精通——掌握PHP进阶技术与现代化开发实战-5
android·开发语言·php
AI视觉网奇3 小时前
android jni保存图片
android
私人珍藏库3 小时前
【安卓】Lightroom摄影师版PS滤镜免费
android·app·安卓·工具·软件
黑马源码库miui520865 小时前
JAVA成人用品商城系统源码微信小程序+h5+安卓+ios
android·java·微信小程序
h***34635 小时前
怎么下载安装yarn
android·前端·后端
一个平凡而乐于分享的小比特5 小时前
Linux、Debian、Yocto、Buildroot、Android系统详解
android·linux·操作系统·debian·yocto·buildroot
HackShendi6 小时前
Android全局监听音量按键事件
android
七夜zippoe6 小时前
基于MLC-LLM的轻量级大模型手机端部署实战
android·智能手机·架构·大模型·mlc-llm