mcpField
P
preserving-state-across-reloads

preserving-state-across-reloads

github.com/skydoves开发520126 次安装19 周前更新
代码仓库

关于 preserving-state-across-reloads

面向使用 Compose HotSwan 的 Android 开发者,说明热重载的三个层级如何影响 UI 状态,并指导如何让迭代尽量保留状态。

主要功能

  1. 01解释三种重载层级及状态保留情况
  2. 02根据状态丢失排查状态持有方式
  3. 03使用 rememberSaveable 保留状态
  4. 04将长期迭代状态提升到 ViewModel
  5. 05避免主题与 staticCompositionLocalOf 触发升级

适用场景

  1. 01热重载后滚动位置或对话框状态丢失
  2. 02判断某次代码修改会触发哪个重载层级
  3. 03调整状态持有方式以适应组合重置
  4. 04长时间调试界面时保留临时状态

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/skydoves/compose-performance-skills --skill preserving-state-across-reloads

安装后在 Claude Code 中输入/preserving-state-across-reloads确认已加载。

详细介绍

跨重新加载保留状态:让编辑保持在 HotSwan 第 1 级

Compose HotSwan 会通过逐步升级的三个级别应用每次热重载。第 1 级(定向重组)保留的内容最多:滚动位置、导航返回栈、remember 和 rememberSaveable 的值、ViewModel 状态、惰性布局项、对话框和底部工作表状态。第 2 级(组合重置)会释放并重新创建组合,丢弃每个可组合项的 remember。第 3 级(Activity.recreate())会重启 Activity,只有由 ViewModel 或 SavedInstanceState 持有的状态会保留。

每次重新加载都保持在第 1 级时,迭代循环最快,也最不容易出现意外。本技能介绍哪些编辑会触发哪个级别、各级别会保留哪些状态持有者,以及必须升级时如何提升临时 UI 状态。

何时使用本技能

  • 开发者报告“热重载后滚动位置跳到了顶部”“对话框状态丢失”“惰性列表重新获取了数据”或“标签页选择重置为 0”。
  • 开发者准备重构可组合项,并想了解更改会影响哪个重组作用域。
  • HotSwan 工具窗口状态显示第 2 级或第 3 级重新加载,开发者想了解升级的原因。
  • 开发者询问哪些状态持有者能在 Activity.recreate() 后保留,哪些能在组合重置后保留。
  • 用户提到“第 1 级”“第 2 级”“第 3 级”“组合重置”“HotSwan 状态保留”或“跨重新加载的 rememberSaveable”。

何时不使用本技能

  • 更改完全因不符合架构而被拒绝(添加了参数、更改了构造函数、添加了新的资源 ID),HotSwan 因此回退到完整增量构建。请参阅 ../understanding-hot-reload-limits/SKILL.md。
  • 未安装 HotSwan 或监视器未运行。请参阅 ../setting-up-compose-hotswan/SKILL.md。
  • 开发者希望由 AI Agent 自动驱动迭代循环。请参阅 ../iterating-with-ai-and-mcp/SKILL.md。
  • 即使不考虑热重载,重组范围本身也太大(父级每次状态更新都会使整个子树失效)。请先使用 ../../recomposition/debugging-recompositions/SKILL.md 进行诊断。

先决条件

  • 已安装 Compose HotSwan,并且正在对运行中的应用执行 WATCHING。设置说明位于 ../setting-up-compose-hotswan/SKILL.md。
  • 熟悉 Compose 重组作用域;底层机制请参阅 ../../recomposition/debugging-recompositions/SKILL.md。
  • 熟悉 rememberSaveable,以便理解必须跨进程死亡以及(在此上下文中)组合重置保留的状态。

三个级别

级别机制保留丢失触发条件
1. 定向重组只在原位置重组受影响的作用域导航返回栈、滚动位置、remember、rememberSaveable、ViewModel、惰性布局项、对话框和底部工作表状态无在单个可组合项作用域内进行简单的主体更改
2. 组合重置从头释放并重新创建所有组合Activity、ViewModel、导航(通过 NavController)、rememberSaveable(取决于保留方式)未由 Saveable 保留的每个可组合项的 remember 值、未由可保存状态持有者保存的滚动位置无法使用第 1 级时(主题更改、根作用域结构更改)
3. Activity.recreate()重新创建整个 ActivityViewModel、SavedInstanceState滚动位置、临时对话框状态、任何未保存的内容组合失败或检测到架构不匹配

每次重新加载后,HotSwan 工具窗口状态都会显示实际运行的级别。每次编辑后都查看该状态,确认迭代循环保持在预期级别。

工作流

1. 每次重新加载后,都要在工具窗口中查看层级

HotSwan 工具窗口会在每次重新加载时显示层级(1、2 或 3)。如果开发者预期是层级 1,而状态报告显示层级 2 或 3,说明编辑影响的范围超出了预期。先查看层级,再判断状态丢失是配置问题还是预期行为。

2. 如果在层级 2 丢失了状态,检查受影响的状态持有者

层级 2 会销毁每个可组合项中的 remember 块。检查丢失状态的可组合项,将其中开发者希望在可能升级层级的重新加载期间保留的值所用的本地 remember 改为 rememberSaveable。这是 HotSwan 驱动的迭代循环中效果最大的单项改动。

3. 长时间迭代时,将面向用户的临时状态提升到 ViewModel

当开发者需要长时间迭代某个屏幕,并且单次编辑总会升级到层级 2 或层级 3 时,应将临时 UI 状态(选中的标签页、展开的项目、滚动位置、对话框是否打开)提升到 ViewModel 中。ViewModel 在所有三个层级中都会保留,因此迭代循环不会丢失工作台状态。

4. 快速迭代期间,避免修改主题和根 CompositionLocal

编辑 MaterialTheme 或任何 staticCompositionLocalOf 所使用的值,都会使相应 CompositionLocalProvider 的根内容 lambda 失效。HotSwan 无法将此限定在单个重组目标中,因此会升级到层级 2。将正在调整的颜色、尺寸或排版移到目标可组合项的本地覆盖中,等值最终确定后再移回主题。

5. 对开发者正在编辑的任何值,都避免使用 staticCompositionLocalOf

即使不涉及主题,通过 staticCompositionLocalOf 提供的任何值一旦更改,都会使提供者的全部内容失效。对于热重载会话期间可能发生变化的值,请使用 compositionLocalOf(它会跟踪读取情况)。

模式

模式:本地 remember 在层级 2 丢失状态
// WRONG (for tier 2 reloads)
@Composable
fun TabScreen() {
    var selected by remember { mutableIntStateOf(0) }
    Tabs(selected = selected, onSelected = { selected = it })
}
// WRONG because: a tier 2 composition reset disposes remember; the selected tab snaps back to 0 after a reload that escalates.
// RIGHT
@Composable
fun TabScreen() {
    var selected by rememberSaveable { mutableIntStateOf(0) }
    Tabs(selected = selected, onSelected = { selected = it })
}
模式:修改主题颜色导致升级到层级 2
// WRONG (forces tier 2)
val LightColors = lightColorScheme(primary = Color(0xFFEE0044))
// WRONG because: editing a top-level colour used by MaterialTheme invalidates the root CompositionLocal; the reload escalates to tier 2 and disposes per-composable remember.
// RIGHT (stays in tier 1)
@Composable
fun PrimaryButton(text: String) {
    Button(
        onClick = {},
        colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFEE0044)),
    ) { Text(text) }
}

视觉值最终确定后,将字面值移回主题。升级只会影响快速迭代循环期间。

模式:惰性列表的滚动位置在层级 2 和层级 3 中得以保留
// RIGHT
@Composable
fun Feed(items: List<Item>) {
    val state = rememberLazyListState()
    LazyColumn(state = state) {
        items(items, key = { it.id }) { Item(it) }
    }
}

rememberLazyListState 由 Saveable 支持,因此滚动位置在组合重置和 Activity 重建后仍会保留。配合稳定的 key,即使重新加载导致层级升级,可见项目也会继续显示。

模式:将迭代关键状态提升到 ViewModel
// RIGHT
class FeedViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(FeedState())
    val uiState: StateFlow<FeedState> = _uiState.asStateFlow()
}

@Composable
fun FeedScreen(viewModel: FeedViewModel) {
    val state by viewModel.uiState.collectAsStateWithLifecycle()
    Feed(state.items)
}

ViewModel 的生命周期长于 HotSwan 重新加载的所有层级(通常也长于配置更改),因此即使在层级 3,屏幕级工作台状态也不会受到影响。

模式:避免对迭代期间会更改的值使用 staticCompositionLocalOf
// WRONG (every edit forces tier 2)
val LocalAccent = staticCompositionLocalOf { Color.Red }
// WRONG because: changing the provided value invalidates the entire content of CompositionLocalProvider; HotSwan cannot scope that and escalates.
// RIGHT
val LocalAccent = compositionLocalOf { Color.Red }

compositionLocalOf 会跟踪读取情况,只使实际读取该值的位置失效;HotSwan 因此可以将重新加载限制在层级 1。

强制规则

  • 必须优先使用 rememberSaveable 而非 remember,用于开发者希望在可能升级层级的热重载期间保留的任何 UI 状态。
  • 必须避免在快速迭代期间修改主题和根 CompositionLocal;这些编辑会导致升级到层级 2。
  • 不得假设总是以层级 1 运行。每次重新加载后,都要查看 HotSwan 工具窗口报告的层级。
  • 不得对开发者在热重载会话期间正在主动编辑的值使用 staticCompositionLocalOf。
  • **推荐:**迭代循环较长时,将临时 UI 状态(选中的标签页、展开的项目、对话框是否打开、滚动位置)提升到 ViewModel 中。
  • **推荐:**使用 rememberLazyListState(以及对应的网格/分页状态持有者)管理滚动和可见项目状态,因为这些状态默认可保存。

验证

  • 编辑 composable 主体后,滚动位置和对话框状态保持不变,且工具窗口报告 tier 1
  • 编辑 MaterialTheme 颜色后,级别提升至 tier 2(状态会确认级别)
  • 由 rememberSaveable 保存的状态在 tier 2 组合重置后仍会保留
  • 由 ViewModel 保存的状态在 tier 3 Activity.recreate() 后仍会保留
  • rememberLazyListState 在 tier 2 重新加载后仍会保留滚动位置

参考资料

评论

支持 Markdown