Android 生命周期感知的数据加载设计
Android 生命周期感知的数据加载设计
移动端页面经常需要同时处理接口请求、配置变化、前后台切换和用户重复点击。如果这些逻辑全部堆在 Activity 或 Fragment 中,页面很容易出现重复请求、状态丢失、视图销毁后仍然回调等问题。更稳妥的做法是把“数据状态”和“界面生命周期”分开管理:ViewModel 负责业务状态,界面只在可见阶段收集并渲染。
一、先定义页面状态
页面不应依赖多个互不关联的布尔值判断当前阶段。例如,同时维护 isLoading、hasError 和 isEmpty,很可能出现加载中又显示错误页的矛盾状态。使用密封接口描述互斥状态更清晰:
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 保存和更新状态,用视图生命周期控制收集。这样既减少重复请求,也能降低内存泄漏和视图状态错乱的概率,为后续的缓存、分页和测试留下清晰扩展点。
- 点赞
- 收藏
- 关注作者
评论(0)