本节摘要:现代 Kotlin 安卓的 Activity 只该干三件事:绑定视图、收集状态、穷举渲染。本节给出这个三段式模板的完整实现与逐段拆解(每一段对应前几章的哪根支柱)、collectLatest 与 collect 的语义差、生命周期对收集的起停控制、以及视图销毁与配置变更下的状态恢复要点。
订单历史页,按现代写法完整给出(依赖第 1 章骨架的 viewBinding 与协程库):
class OrderHistoryActivity : AppCompatActivity() { private val viewModel: OrderHistoryVM by viewModels() private lateinit var binding: ActivityOrderHistoryBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityOrderHistoryBinding.inflate(layoutInflater) setContentView(binding.root) // 第一段 绑定 binding.swipeRefresh.setOnRefreshListener { // 交互转意图 viewModel.refresh() } lifecycleScope.launch { // 第二段 收集 repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.state.collect { render(it) } } } } private fun render(state: OrderHistoryVM.UiState) { // 第三段 渲染 binding.swipeRefresh.isRefreshing = state is OrderHistoryVM.UiState.Loading when (state) { OrderHistoryVM.UiState.Loading -> binding.progress.isVisible = true is OrderHistoryVM.UiState.Data -> { binding.progress.isVisible = false adapter.submitList(state.orders) binding.tvEmpty.isVisible = state.orders.isEmpty() } is OrderHistoryVM.UiState.Error -> { binding.progress.isVisible = false showRetryDialog(state.message) } } } }
三十八行,是一个生产级页面的合理骨架。逐段拆开看支柱的分布。绑定段:lateinit 管初始化时序(第 2 章)、绑定类属性非空直达(第 1 章)。收集段:lifecycleScope 提供树根(第 5 章)、repeatOnLifecycle 只在前台收集(省电)、collect 到的每个状态都是不可变快照(第 4 章)。渲染段:穷举 when 无 else(第 3 章)、智能转换直接取字段、新增状态编译期标红。交互段:点击与刷新不直接改 UI,只调用 ViewModel 的意图函数——UI 不持有业务状态,这是单向数据流的纪律起点。
by viewModels() 委托也值得停一拍:它把 ViewModel 的获取推迟到首次访问、自动绑定 ViewModelStore 的生命周期,比老的 ViewModelProvider 工厂链干净得多。这个"属性委托"语法是 Kotlin 委托机制在 Jetpack 里最常见的一张脸。
收集一个流,新值到来而上一轮渲染还没跑完(渲染超过发射间隔)时,两种收集语义分岔:
viewModel.state.collect { render(it) } // 排队 每个值都会被处理 viewModel.state.collectLatest { render(it) } // 取消旧的 只处理最新
collect 保守:每个状态都处理,代价是可能积压(渲染速度跟不上时,用户看到的是延迟的慢放)。collectLatest 激进:新状态到来立刻取消上一轮渲染 lambda(渲染 lambda 里若有挂起点,取消在挂起点生效——第 5.4 节的协作式取消),只保最新。UI 状态渲染默认 collect(StateFlow 已收敛最新值,积压本就有限),昂贵的派生计算(如按查询重算列表)用 collectLatest。选错的表现很典型:collectLatest 用在"必须逐条处理"的日志流上会丢条目,collect 用在高频重算上会越拖越慢。
repeatOnLifecycle(STARTED) 的语义值得展开:它在每次进入 STARTED 时新起一个协程执行块内的收集,退出 STARTED(onStop)时取消该协程,再进入再重启。效果是后台零收集——省电、避免后台无意义渲染,且与 StateFlow 的当前值语义配合得天衣无缝:回到前台重新收集,立刻拿到最新状态,无缝续播。漏掉 repeatOnLifecycle 直接在 lifecycleScope 里 collect 的写法,是电量榜单常客(第 5 章末尾提过的反面模式)。
收集之外,UI 层还有两件生命周期的事要立住。其一是配置变更:旋转屏幕时 Activity 销毁重建,lifecycleScope 整树取消重长(第 5 章的树图),而 ViewModel 与它的 viewModelScope 存活——数据不重拉,新 Activity 重新收集即可拿到现值。这就是"界面短命数据长命"的分工,写 UI 代码时永远不要往 Activity 字段里塞业务状态,它的家在 ViewModel。其二是进程死亡恢复:系统杀进程后 Activity 与 ViewModel 全灭,回来靠 SavedStateHandle 恢复关键最小状态(8.2 节展开),UI 层的配合点是在 onSaveInstanceState 里不做多余的事——交给 ViewModel。

同样的模板搬到 Fragment,有两处必须修正。视图绑定随视图生命周期走:onDestroyView 里必须置空绑定引用,否则实例存活而视图重建后,旧绑定指向已废弃视图(第 2 章 lateinit 雷区):
class OrderHistoryFragment : Fragment() { private var _binding: ActivityOrderHistoryBinding? = null // 可空 视图可能不在 private val binding get() = _binding!! // 逻辑上只在视图存续期访问 override fun onCreateView(...): View { _binding = ActivityOrderHistoryBinding.inflate(layoutInflater) return binding.root } override fun onDestroyView() { super.onDestroyView() _binding = null // 视图走了 绑定跟着走 } }
这里的 !! 是社区惯例的例外场景:访问点被生命周期方法包围,"视图存续期"的约定由 Fragment 结构保证。也可以用属性委托库(如 Google 示例里的 FragmentViewBindingDelegate)把这个约定机器化。收集挂 viewLifecycleOwner:Fragment 有实例与视图两条生命周期,repeatOnLifecycle 应挂在 viewLifecycleOwner 的 lifecycleScope 上,否则视图销毁后收集还挂在实例上,向已废弃视图渲染。
UI 消费端就位。下一节进入生产端:ViewModel 如何组织状态与事件,以及进程被杀后靠什么恢复。