ViewModel负责组装界面状态State。引发State变换的原因有很多,比如用户点击某个按钮,一次网络请求受到应答,一次本地数据库查询返回结果等等。因此ViewModel是根据各种事件生成State的对象,换句话说,是一个从多个事件流到状态的映射。
ViewModel: (InputFlow, NetworkFlow, DatabaseFlow, ...) -> State
用图来表示就是
┌─────────┐ 用户交互 ───▶ 事件流 ───┤ ││ViewModel├───▶ UI 状态 ───▶ 渲染UI 数据变更 ───▶ 数据流 ───┤ │└─────────┘
1. 常见事件流处理模式
我们考虑处理两个事件流的模式。多事件流的处理方法是类似的。
1.1. 模式1:取各自最新值
combine(flowA, flowB) { a, b ->// 当某个流更新时,获取两个流各自的最新值。Result(a, b) }
适用场景:表单联动验证(如邮箱+密码验证)、实时数据看板。
1.2. 模式2:合并流
merge(flowA, flowB) // 任意流更新时触发,取两个流中的最新值。
适用场景:同类型事件合并(如多个按钮点击事件)
1.3. 模式3:流水线
flowA.flatMapLatest { aValue -> flowB.map { bValue -> Processed(aValue, bValue) } }
适用场景:搜索建议(关键词变化触发新查询)、参数化数据加载
1.4. 模式4:序号匹配
flowA.zip(flowB) { a, b -> // 严格按序号匹配,双方各发1次才触发。Pair(a, b) }
适用场景:分页加载、操作-响应确认机制。
1.5. 模式5:优先流
combine(flowA, flowB) { a, b ->when {a.priority > b.priority -> Result(a)b.isCritical -> Result(b)else -> Result.merge(a, b)} }
特点:实现业务规则主导的数据优先级。 适用场景:多源数据冲突处理、关键操作优先。
1.6. 模式6:将异常转换为事件
val flowAState = flowA.map { Success(it) }.catch { emit(Error(it)) }.stateIn(viewModelScope, SharingStarted.Lazily, Loading)
特点:单个流的失败不影响整体功能。 适用场景:模块化数据展示、独立可失败操作。
2. 常见问题和方案
2.1. 问题1:输入流过多
输入流过多会导致代码可读性下降。封装中间事件和中间流可以解决这个问题。
// 创建中间组合流 val userPreferences = combine(themeFlow, fontSizeFlow, layoutFlow) { UserPrefs(it[0], it[1], it[2]) }val finalState = combine(userPrefs, contentFlow) { ... }
2.2. 问题2:状态频繁更新导致UI抖动
在流中增加防抖可以避免UI抖动。
searchQueryFlow.debounce(300) // 防抖.distinctUntilChanged().flatMapLatest { query -> repository.search(query).map { it.toState() } }
2.3. 问题3:订阅泄漏
使用stateIn和flatMapLatest可以避免订阅泄露。
代码1 错误例子:
init {viewModelScope.launch { // 直接启动协程dataFlow.collect { ... }} }
代码2 正确例子
val state = dataFlow.map { ... }.stateIn( // 使用stateIn扩展scope = viewModelScope,started = SharingStarted.Eagerly,initialValue = ...)
2.4. 问题4:错误恢复
使用retryWhen可以从错误中进行恢复。
flowA.retryWhen { cause, attempt -> if (attempt < 3) delay(200 * attempt) else throw cause}.catch { emit(fallbackData) }
2.5. 问题5:动态切换流
val activeFlow = MutableStateFlow<Flow<Data>>(flowA)val result = activeFlow.flatMapLatest { it }
支持在运行时切换数据源(如测试模式/生产模式切换)。
2.6. 问题6:缓存共享流
复用单数据源,减少重复订阅。
// 创建共享流 val sharedFlow = repository.dataSource.shareIn(viewModelScope, SharingStarted.WhileSubscribed())// 分流处理 val stateA = sharedFlow.map { extractA(it) } val stateB = sharedFlow.map { extractB(it) }
2.7. 问题7:将业务状态作为界面状态
不要直接使用业务状态作为界面状态(业务状态极为简单的除外)。
// 错误方式:混合业务状态和UI控制状态 data class BadState(val data: List<Item>,val loading: Boolean,val toastMessage: String?, // UI控制状态val dialogVisible: Boolean // UI控制状态 )// 正确方式:分层状态 // 业务状态 data class BusinessState(val data: List<Item>, val error: Throwable?)// UI状态(由业务状态转换) val uiState = businessState.map { when {it.data.isEmpty() -> EmptyStateit.error != null -> ErrorState(it.error)else -> SuccessState(it.data)} }