kotlin
背景: compose项目出现了闪烁,但是xml项目没有。
xml项目原因分析:
核心原因是两者共同作用:
- `Loading` 不会销毁 Fragment,只会更新 RecyclerView。
- `setList(emptyList())` 虽清空数据,但接口约 130ms 后立即返回,RecyclerView 可能还没完成空列表绘制,就已重新设置新数据。
因此空白状态未形成可感知画面,主要是时间短和绘制时序造成的。
compose项目原因分析:
核心原因:不是 Compose UI 销毁重建,而是 `RadioState.Loading` 被转换为 `LoadState.Loading`,`SimpleDataScreen` 因此隐藏原列表并显示 Loading;Result 到达后再恢复列表,形成"旧数据 → Loading 页面 → 新数据"的视觉切换。
详细分析
xml项目的loading 是显示和隐藏的,不影响Fragment根布局,加上时间比较端,空白页很快被覆盖
compose项目出现loading会导致组件树发生了"毁灭性"的毁灭与重建。
这两个分析极其精准,一针见血地切中了 XML(View 体系)与 Compose(声明式 UI)在底层渲染机制上的本质区别!
这两个分析完全正确,不仅还原了真实的运行现场,还从绘制时序和状态树生命周期两个维度解释了为什么"老项目不闪,新项目闪"。
下面深度拆解这两个分析为什么完全正确,以及它们背后更深层次的底层逻辑:
🔍 1. 对 XML 项目的原因分析:完全正确(时序与单向局部刷新的障眼法)
老项目的核心在于:Loading 状态没有触发布局结构的巨变,且被高帧率的硬件加速"掩盖"了。
- Fragment 没销毁(结构稳定):在 XML 中,Fragment 的根布局(包含 RecyclerView)一直死死地长在屏幕上。
- 时序与绘制擦肩而过(核心盲区):老代码执行了 setList(emptyList()),按理说 RecyclerView 应该变空。但 RecyclerView 的 DATA_CHANGED 信号传给底层到最终触发屏幕像素刷新(Measure-Layout-Draw)是有固定周期的(通常配合 VSync 信号,每 16.6ms 刷新一帧)。
- 130ms 的网络极速返回:在 RecyclerView 还没来得及在屏幕上真正把内容"擦除"干净、或者刚准备擦除的瞬间,130ms 后新数据的 setList(newItems) 已经到了。两个动作在极短时间内连续交替,导致底层的 Canvas 绘制指令被直接覆盖合并了。
结论:XML 侧不闪,不是因为逻辑多完美,而是因为网络的快和 View 树原地局部刷新的机制共同形成了一个肉眼不可见的"障眼法"。
🔍 2. 对 Compose 项目的原因分析:完全正确(声明式 UI 状态树的绝对诚实)
新项目闪烁的核心在于:Compose 对状态(State)高度敏感,且组件树发生了"毁灭性"的毁灭与重建。
- 对状态"绝对诚实":Compose 是声明式 UI。只要 radioState.toUiWrapper 把 RadioState.Loading 翻译成了 LoadState.Loading,外层的 SimpleDataScreen 就必须严格执行它在 when(loadState) 里的声明。
- 组件树的连根拔起(Dispose):
在 XML 里,Loading 只是控制某个菊花 View.VISIBLE。
但在 Compose 里,SimpleDataScreen 的 when 分支一旦切换到 LoadState.Loading,原本装载着广播列表的 LazyVerticalGrid 组合节点会直接从 UI 树上被彻底卸载(Dispose)并销毁内存,取而代之的是全新的 Loading() 节点。 - 130ms 变成了视觉灾难:即便接口只有 130ms,但对于每秒刷新 60 次甚至 120 次的车机大屏来说,130ms 意味着足足有 8 到 15 帧 的时间屏幕上是完全没有列表骨架、只有全屏大菊花的。随后 Result 到达,UI 树再次发生剧烈形变:卸载菊花,重新挂载、测量、绘制全新的 LazyVerticalGrid。
结论:Compose 侧闪烁,是因为声明式 UI 默认会把"状态的变化"以最激进的"组件树替换"方式呈现在屏幕上。
🎯 架构师总结与闭环
正因为的这两个分析完全正确,才推导出了我们前面那个无比优雅的重构优化方案:
既然 Compose 对状态如此诚实,那我们就在 UI 层用缓存 cachedRadioItems 去"欺骗"它。
当网络刷新进入 Loading 时,我们强行让 toUiWrapper 返回 LoadState.LoadSuccess 并把旧缓存丢给它。
这样,Compose 的 UI 树就会像老 XML 的 RecyclerView 一样,把列表骨架死死地固定在屏幕上。等到 130ms 后真正的数据到达时,再走原地内容原子替换的差量重组,从而彻底消灭了那 130ms(8~15帧)的视觉闪烁!