Android ViewModel 与 StateFlow 界面状态

举报
shenlan9755 发表于 2026/09/29 08:48:25 2026/09/29
【摘要】 Android ViewModel 与 StateFlow 界面状态摘要:界面状态分散在 Activity、Composable 和网络回调中时,容易出现旋转丢状态、重复请求和页面数据不一致。本文以 ViewModel、StateFlow 和单向数据流为主线,搭建一个可测试、能感知生命周期的 Android 页面状态模型。 一、把界面看作状态的呈现一个列表页面通常有加载中、加载成功、空结果...

Android ViewModel 与 StateFlow 界面状态

摘要:界面状态分散在 Activity、Composable 和网络回调中时,容易出现旋转丢状态、重复请求和页面数据不一致。本文以 ViewModel、StateFlow 和单向数据流为主线,搭建一个可测试、能感知生命周期的 Android 页面状态模型。

一、把界面看作状态的呈现

一个列表页面通常有加载中、加载成功、空结果和失败等状态。若分别用多个布尔值表示,可能出现 isLoading 与 hasError 同时为真的组合。用一个状态类型表达页面当前所处阶段,可以避免无效组合:

data class Article(
    val id: Long,
    val title: String
)

sealed interface ArticleUiState {
    data object Loading : ArticleUiState
    data class Content(val articles: List<Article>) : ArticleUiState
    data object Empty : ArticleUiState
    data class Error(val message: String) : ArticleUiState
}

页面只需根据状态绘制内容,不必自行推断数据是否完整。

二、分离数据来源与界面逻辑

Repository 负责协调数据来源,ViewModel 负责把数据变化转换为界面状态。ViewModel 不应持有 Activity、View 或 Composable 的引用:

interface ArticleRepository {
    suspend fun loadArticles(): List<Article>
}

class ArticleViewModel(
    private val repository: ArticleRepository
) : ViewModel() {
    private val _uiState = MutableStateFlow<ArticleUiState>(ArticleUiState.Loading)
    val uiState: StateFlow<ArticleUiState> = _uiState.asStateFlow()

    init {
        refresh()
    }

    fun refresh() {
        viewModelScope.launch {
            _uiState.value = ArticleUiState.Loading
            _uiState.value = try {
                val items = repository.loadArticles()
                if (items.isEmpty()) ArticleUiState.Empty
                else ArticleUiState.Content(items)
            } catch (cancelled: CancellationException) {
                throw cancelled
            } catch (error: Exception) {
                ArticleUiState.Error(error.message ?: "加载失败")
            }
        }
    }
}

取消异常要继续向上传播,否则协程取消可能被误显示成普通错误。viewModelScope 会随 ViewModel 清理而取消任务。

三、让 Compose 按生命周期收集状态

使用生命周期感知的收集函数,可避免页面处于后台时仍持续收集界面数据:

@Composable
fun ArticleRoute(viewModel: ArticleViewModel) {
    val state by viewModel.uiState.collectAsStateWithLifecycle()
    ArticleScreen(state = state, onRefresh = viewModel::refresh)
}

@Composable
fun ArticleScreen(
    state: ArticleUiState,
    onRefresh: () -> Unit
) {
    when (state) {
        ArticleUiState.Loading -> CircularProgressIndicator()
        ArticleUiState.Empty -> Text("还没有内容")
        is ArticleUiState.Error -> ErrorPanel(state.message, onRefresh)
        is ArticleUiState.Content -> ArticleList(state.articles)
    }
}

ArticleScreen 只接受输入并绘制界面,便于预览和单元测试。状态收集与 ViewModel 的连接放在 Route 层,职责更清楚。

四、区分持久状态与短暂事件

页面标题、已加载列表和筛选条件适合作为可重放的状态。一次性提示或跳转属于事件;如果把它们塞进普通 StateFlow,页面重建后可能再次消费旧事件。可以让界面动作携带回调,由界面处理完后再通知 ViewModel 更新状态;需要队列语义时,再使用明确规定缓冲和重放策略的事件流。

事件设计至少要回答:新订阅者是否收到旧事件?暂时没有订阅者时事件是否保留?同一事件可否消费多次?把这些语义写清楚,比只选一个流类型更重要。

五、处理筛选和请求竞争

用户快速切换筛选条件时,前一个请求可能晚于后一个返回。对由输入驱动的查询,可将条件建模为 Flow 并使用 flatMapLatest 取消过期任务:

private val query = MutableStateFlow("")

val results: StateFlow<SearchUiState> = query
    .debounce(300)
    .map(String::trim)
    .distinctUntilChanged()
    .flatMapLatest { value ->
        flow {
            emit(SearchUiState.Loading)
            emit(SearchUiState.Content(repository.search(value)))
        }.catch { error ->
            emit(SearchUiState.Error(error.message ?: "搜索失败"))
        }
    }
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), SearchUiState.Loading)

实际项目中仍需把取消异常按协程规则处理,并在 Repository 中定义网络失败与空结果的语义。

六、测试状态转换

为 ViewModel 注入 Repository 接口,可用假的实现控制返回值和异常。测试重点是状态顺序:初始加载、数据到达后的内容状态,以及失败后是否能再次刷新。协程测试应使用可控调度器,避免依赖真实等待时间。

七、常见问题

  1. 不要在 Composable 每次重组时直接发起网络请求;请求应由 ViewModel 的明确动作驱动。
  2. 不要把可推导的数据重复存进多个 StateFlow,避免同步规则变复杂。
  3. StateFlow 总会有当前值;状态初值要表达真实的页面阶段。
  4. 不要吞掉 CancellationException,它是结构化并发的一部分。
  5. 列表项使用稳定 ID,避免数据变化后界面错配。

八、总结

ViewModel 与 StateFlow 可以把页面变成清晰的状态映射:Repository 提供数据,ViewModel 管理异步状态,Compose 负责展示并上报用户动作。明确状态模型、生命周期收集规则和事件语义后,页面重建、刷新和测试都会更可控。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。