【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)
抗屏幕旋转
易于单元测试
自动刷新过期数据
相关推荐
andr_gale25 分钟前
04_rc文件语法规则
android·framework·aosp
祖国的好青年1 小时前
VS Code 搭建 React Native 开发环境(Windows 实战指南)
android·windows·react native·react.js
黄林晴2 小时前
警惕!AGP 9.2 别只改版本号,R8 规则与构建链路全线收紧
android·gradle
小米渣的逆袭2 小时前
Android ADB 完全使用指南
android·adb
儿歌八万首2 小时前
Jetpack Compose Canvas 进阶:结合 animateFloatAsState 让自定义图形动起来
android·动画·compose
zhangphil3 小时前
Android Page 3 Flow读sql数据库媒体文件,Kotlin
android·kotlin
神探小白牙3 小时前
echarts,3d堆叠图
android·3d·echarts
李白的天不白4 小时前
如何项目发布到github上
android·vue.js
summerkissyou19874 小时前
Android-RTC、NTP 和 System Time(系统时间)
android