Android 生命周期感知的数据加载设计

举报
yd_217846120 发表于 2026/09/07 08:50:01 2026/09/07
【摘要】 Android 生命周期感知的数据加载设计移动端页面经常需要同时处理接口请求、配置变化、前后台切换和用户重复点击。如果这些逻辑全部堆在 Activity 或 Fragment 中,页面很容易出现重复请求、状态丢失、视图销毁后仍然回调等问题。更稳妥的做法是把“数据状态”和“界面生命周期”分开管理:ViewModel 负责业务状态,界面只在可见阶段收集并渲染。 一、先定义页面状态页面不应依赖多...

Android 生命周期感知的数据加载设计

移动端页面经常需要同时处理接口请求、配置变化、前后台切换和用户重复点击。如果这些逻辑全部堆在 ActivityFragment 中,页面很容易出现重复请求、状态丢失、视图销毁后仍然回调等问题。更稳妥的做法是把“数据状态”和“界面生命周期”分开管理:ViewModel 负责业务状态,界面只在可见阶段收集并渲染。

一、先定义页面状态

页面不应依赖多个互不关联的布尔值判断当前阶段。例如,同时维护 isLoadinghasErrorisEmpty,很可能出现加载中又显示错误页的矛盾状态。使用密封接口描述互斥状态更清晰:

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

这样,每一时刻只会处于一种明确状态。新增业务状态时,编译器也可以帮助检查 when 分支是否完整。

二、让 ViewModel 持有唯一状态

ViewModel 不直接操作控件,只负责请求数据并更新状态。对外暴露只读流,可以避免界面层意外修改数据。

class ArticleViewModel(
    private val repository: ArticleRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow<ArticleUiState>(ArticleUiState.Loading)
    val uiState: StateFlow<ArticleUiState> = _uiState.asStateFlow()

    private var loaded = false

    fun loadIfNeeded() {
        if (loaded) return
        loaded = true
        load()
    }

    fun retry() {
        load()
    }

    private fun load() {
        viewModelScope.launch {
            _uiState.value = ArticleUiState.Loading
            runCatching { repository.queryArticles() }
                .onSuccess { articles ->
                    _uiState.value = if (articles.isEmpty()) {
                        ArticleUiState.Empty
                    } else {
                        ArticleUiState.Content(articles)
                    }
                }
                .onFailure { throwable ->
                    _uiState.value = ArticleUiState.Error(
                        throwable.message ?: "数据加载失败"
                    )
                }
        }
    }
}

loadIfNeeded 用于首次进入页面,避免视图重建时重复请求;retry 则保留明确的人工重试入口。是否允许覆盖请求、是否需要缓存,应由真实业务规则决定,不要在没有需求时叠加多层无意义兜底。

三、只在合适的生命周期收集

Fragment 的生命周期和它的视图生命周期并不完全相同。界面绑定应使用 viewLifecycleOwner,从而保证 onDestroyView 之后不再访问已销毁的控件。

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    viewLifecycleOwner.lifecycleScope.launch {
        viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
            viewModel.uiState.collect(::render)
        }
    }

    binding.retryButton.setOnClickListener {
        viewModel.retry()
    }

    viewModel.loadIfNeeded()
}

当页面进入 STARTED 状态时开始收集,离开后自动停止;再次返回时会收到 StateFlow 保存的最新状态。这比手动在多个生命周期回调中启动和取消任务更容易维护。

四、集中渲染界面

渲染函数应根据状态一次性设置所有相关视图,避免某个旧控件状态残留到下一次渲染。

private fun render(state: ArticleUiState) = with(binding) {
    progressBar.isVisible = state is ArticleUiState.Loading
    recyclerView.isVisible = state is ArticleUiState.Content
    emptyView.isVisible = state is ArticleUiState.Empty
    errorGroup.isVisible = state is ArticleUiState.Error

    when (state) {
        is ArticleUiState.Content -> articleAdapter.submitList(state.items)
        is ArticleUiState.Error -> errorText.text = state.message
        ArticleUiState.Loading,
        ArticleUiState.Empty -> Unit
    }
}

集中渲染还有一个好处:状态与视觉结果的对应关系非常直观,后续编写界面测试时也更容易覆盖所有分支。

五、一次性事件不要混进持久状态

列表内容、加载状态适合使用 StateFlow,因为新订阅者应该立即得到当前值。跳转、轻提示、弹窗等一次性行为则不应在旋转屏幕后再次执行,可以使用项目现有的事件机制或不带重放的 SharedFlow

private val _events = MutableSharedFlow<ArticleEvent>()
val events = _events.asSharedFlow()

sealed interface ArticleEvent {
    data class ShowMessage(val text: String) : ArticleEvent
    data class OpenDetail(val articleId: String) : ArticleEvent
}

关键不是机械地选择某个类,而是先区分两种语义:状态表示“现在是什么”,事件表示“刚刚发生了什么”。

六、常见问题

在 Fragment 中直接发起请求

配置变化会重建 Fragment,请求容易重复,临时状态也难以恢复。业务请求应尽量由 ViewModel 管理。

使用 Fragment 自身收集视图数据

Fragment 仍然存活时,它的视图可能已经销毁。涉及视图绑定时,应使用视图生命周期。

每次返回页面都刷新

自动刷新并非总是正确。资讯流可能需要刷新,静态配置可能只需首次加载。刷新策略应成为明确业务规则,而不是生命周期回调的副作用。

总结

生命周期感知的数据加载可以归纳为三点:用单一状态模型表达页面,用 ViewModel 保存和更新状态,用视图生命周期控制收集。这样既减少重复请求,也能降低内存泄漏和视图状态错乱的概率,为后续的缓存、分页和测试留下清晰扩展点。

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

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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