You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.03 17:45:03