Android MVI架构中存在两类顺序成功结果时ViewState类应如何设计?
最优uiState设计方案
推荐采用「密封类分层定义状态+承载已加载数据」的方案,既保留MVI单一全量状态的特性,又避免非法状态组合和重复渲染问题。
方案核心逻辑
你之前的两种方案分别存在缺陷:
- 纯密封类拆分状态的方案无法同时承载主题、新闻两类数据,不符合两者需要同时展示的场景
- 平铺data class的方案存在状态不一致风险(比如error和success字段同时非空),且容易触发重复渲染
新方案将页面的状态流转用密封类明确划分,每个状态节点仅保留合法字段,同时允许上游加载完成的数据向下游状态传递:
sealed interface MainNewsState { // 初始闲置状态 object Idle : MainNewsState // 加载中状态,可携带已加载完成的主题数据 sealed class Loading(val loadedTopics: List<String>? = null) : MainNewsState { // 第一阶段:正在加载新闻主题 object LoadingTopic : Loading() // 第二阶段:主题加载完成,正在拉取对应新闻 data class LoadingNews(val topics: List<String>) : Loading(loadedTopics = topics) } // 错误状态,可携带已加载完成的主题数据(比如主题加载成功、新闻拉取失败的场景) data class Error( val errorMsg: String, val loadedTopics: List<String>? = null ) : MainNewsState // 全量加载完成状态,同时携带两类需要展示的数据 data class Success( val topics: List<String>, val newsList: List<ArticleDb> ) : MainNewsState }
UI层处理逻辑
方式1:按密封类分支处理(更推荐)
天然避免重复渲染,不需要额外加标记位:
viewModel.uiState.collect { state -> when(state) { is MainNewsState.Idle -> { /* 初始状态按需处理 */ } is MainNewsState.Loading.LoadingTopic -> showFullScreenLoading() is MainNewsState.Loading.LoadingNews -> { // 仅在进入该阶段时触发一次主题渲染 renderNewsTopics(state.topics) showNewsPartialLoading() } is MainNewsState.Error -> { // 有已加载的主题就优先渲染 state.loadedTopics?.let { renderNewsTopics(it) } showErrorToast(state.errorMsg) } is MainNewsState.Success -> { renderNewsTopics(state.topics) renderNewsList(state.newsList) } } }
如果你的renderNewsTopics方法本身没有做相同内容判重,可以配合Kotlin Flow的distinctUntilChanged运算符,仅在主题数据发生变化时才触发渲染:
viewModel.uiState .map { when(it) { is MainNewsState.Loading.LoadingNews -> it.topics is MainNewsState.Error -> it.loadedTopics is MainNewsState.Success -> it.topics else -> null } } .distinctUntilChanged() .collect { it?.let { renderNewsTopics(it) } }
方式2:兼容平铺data class偏好
如果你更习惯用data class的写法,不需要修改状态定义,只需要拆分监听维度即可解决重复渲染问题:
// 单独提取每个字段的变化流,仅在字段值变更时触发回调 val topicsFlow = viewModel.uiState .map { it.newsTopicsSuccess } .distinctUntilChanged() val newsFlow = viewModel.uiState .map { it.newsSuccess } .distinctUntilChanged() // 分别收集处理 lifecycleScope.launch { topicsFlow.collectLatest { it?.let { renderNewsTopics(it) } } } lifecycleScope.launch { newsFlow.collectLatest { it?.let { renderNewsList(it) } } }
内容的提问来源于stack exchange,提问作者Alexander
相关产品推荐
相关产品推荐

