preserving-state-across-reloads について
Compose HotSwanを使うAndroid開発者向けのスキルです。3段階のリロードがUI状態に与える影響を説明し、反復作業中に状態を保つ方法を示します。
主な機能
- 013段階のリロードと状態保持を解説
- 02状態ホルダーから状態消失を診断
- 03rememberSaveableによるUI状態の保持
- 04長期作業の状態をViewModelへ移動
- 05リロード段階が上がる編集を回避
活用シーン
- 01リロード後にスクロールやダイアログ状態が失われた場合
- 02編集によって起きるリロード段階の確認
- 03コンポジションリセットに適した状態管理の選択
- 04長時間の反復作業で一時的なUI状態を保持
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ skydoves/ compose-performance-skills --skill preserving-state-across-reloadsインストール後、Claude Code で/preserving-state-across-reloadsを入力して読み込まれたか確認します。
詳細
リロード後も状態を保持する:編集を HotSwan の Tier 1 内にとどめる
Compose HotSwan は、ホットリロードを段階的に強化される 3 つの Tier を通じて適用します。Tier 1(対象を絞った再コンポジション)では、最も多くの状態が保持されます。スクロール位置、ナビゲーションのバックスタック、remember と rememberSaveable の値、ViewModel の状態、遅延レイアウトのアイテム、ダイアログとボトムシートの状態が保持されます。Tier 2(コンポジションのリセット)では、コンポジションが破棄されて再作成され、コンポーザブルごとの remember が失われます。Tier 3(Activity.recreate())では、Activity が再起動され、ViewModel または SavedInstanceState が保持する状態のみが残ります。
すべてのリロードが Tier 1 内にとどまると、反復サイクルが最も速く、予想外の動作も少なくなります。このスキルでは、どの編集がどの Tier を引き起こすか、各 Tier でどの状態保持手段が状態を保持するか、編集を上位 Tier に進める必要がある場合に、一時的な UI 状態をどのようにホイストするかを説明します。
このスキルを使用する場面
- 開発者が「ホットリロード後にスクロール位置が先頭に戻った」「ダイアログの状態が失われた」「遅延カラムが再取得された」「タブの選択がゼロにリセットされた」と報告した場合。
- 開発者がコンポーザブルをリファクタリングしようとしており、その変更がどの再コンポジションスコープに影響するか知りたい場合。
- HotSwan ツールウィンドウのステータスに Tier 2 または Tier 3 のリロードが表示され、その理由を知りたい場合。
- 開発者が
Activity.recreate()とコンポジションのリセットで、どの状態保持手段が状態を保持するか尋ねた場合。 - ユーザーが「Tier 1」「Tier 2」「Tier 3」「コンポジションのリセット」「HotSwan の状態保持」「リロード後の rememberSaveable」に言及した場合。
このスキルを使用しない場面
- 変更がスキーマ違反として全面的に拒否され(パラメーターの追加、コンストラクターの変更、新しいリソース ID の追加など)、HotSwan が完全なインクリメンタルビルドに切り替えた場合。
../understanding-hot-reload-limits/SKILL.mdを参照してください。 - HotSwan がインストールされていないか、ウォッチャーが実行されていない場合。
../setting-up-compose-hotswan/SKILL.mdを参照してください。 - 開発者が AI エージェントに反復サイクルを自律的に進めさせたい場合。
../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を理解していること。
3 つの Tier
| Tier | 仕組み | 保持されるもの | 失われるもの | 発生する条件 |
|---|---|---|---|---|
| 1. 対象を絞った再コンポジション | 影響を受けるスコープのみをその場で再コンポジションする | ナビゲーションのバックスタック、スクロール位置、remember、rememberSaveable、ViewModel、遅延レイアウトのアイテム、ダイアログとボトムシートの状態 | なし | 1 つのコンポーザブルスコープ内の単純な本文変更 |
| 2. コンポジションのリセット | すべてのコンポジションを破棄し、最初から再作成する | Activity、ViewModel、ナビゲーション(NavController 経由)、rememberSaveable(保持の仕組みに依存) | Saveable によって保持されないコンポーザブルごとの remember の値、セーブ可能な状態保持手段で保持されていないスクロール位置 | Tier 1 が利用できない場合(テーマの変更、ルートスコープの構造的な変更) |
3. Activity.recreate() | Activity 全体を再作成する | ViewModel、SavedInstanceState | スクロール位置、一時的なダイアログの状態、保存されていないすべてのもの | コンポジションに失敗した場合、またはスキーマの不一致が検出された場合 |
実行された Tier は、各リロード後に HotSwan ツールウィンドウのステータスに表示されます。編集のたびに確認し、反復サイクルが想定した Tier にとどまっているか確かめてください。
ワークフロー
1. リロードのたびにツールウィンドウでティアを確認する
HotSwan のツールウィンドウには、リロードごとにティア(1、2、または3)が表示されます。開発者がティア1を想定していたのに、ステータスにティア2または3と表示された場合、意図した範囲より広い部分に編集が影響しています。失われた状態が設定の問題なのか、想定どおりの動作なのかを判断する前に、ティアを確認してください。
2. ティア2で状態が失われた場合は、影響を受けた状態の保持先を確認する
ティア2では、コンポーザブルごとの remember ブロックが破棄されます。状態が失われたコンポーザブルを調べ、開発者がティアが上がる可能性のあるリロード後も保持したい値について、ローカルの remember を rememberSaveable に変更してください。これは、HotSwan を使った反復作業のループで最も効果の高い変更です。
3. 長時間の反復作業では、ユーザーに表示される一時的な状態を ViewModel に引き上げる
開発者が画面の調整を長時間続け、個々の編集が何度もティア2またはティア3に上がる場合は、一時的な UI 状態(選択中のタブ、展開中の項目、スクロール位置、ダイアログの表示状態)を ViewModel に引き上げてください。ViewModel は3つすべてのティアで維持されるため、反復作業のループ中に作業画面の状態が失われることはありません。
4. 短時間で反復する作業中は、テーマとルートの CompositionLocal の変更を避ける
MaterialTheme またはいずれかの staticCompositionLocalOf で使用されている値を編集すると、対応する CompositionLocalProvider のルートコンテンツラムダが無効になります。HotSwan はこれを単一の再コンポーズ対象に限定できないため、ティア2に引き上げます。調整中の色、寸法、タイポグラフィは、対象のコンポーザブルにローカルなオーバーライドとして移し、値が確定してからテーマに戻してください。
5. 開発者が編集中の値には staticCompositionLocalOf を使わない
テーマ以外であっても、staticCompositionLocalOf を通じて提供される値は、変更時にプロバイダーのコンテンツ全体を無効にします。ホットリロードのセッション中に変更される可能性がある値には、読み取りを追跡する compositionLocalOf を使ってください。
パターン
パターン:ティア2でローカルの remember が失われる
// 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にとどめることができます。
必須ルール
- 必須: ティアが上がる可能性のあるホットリロード後も開発者が維持したい UI 状態には、
rememberよりrememberSaveableを優先してください。 - 必須: 短時間で反復する作業のセッション中は、テーマとルートの
CompositionLocalの編集を避けてください。これらはティア2への引き上げを引き起こします。 - 必須ではありません: 常にティア1で実行されると想定しないでください。リロードのたびに、HotSwan のツールウィンドウに表示されるティアを確認してください。
- 禁止: ホットリロードのセッション中に開発者が編集中の値に
staticCompositionLocalOfを使わないでください。 - 推奨: 反復作業のループが長い場合は、一時的な UI 状態(選択中のタブ、展開中の項目、ダイアログの表示状態、スクロール位置)を
ViewModelに引き上げてください。 - 推奨: スクロール位置と表示項目の状態には、デフォルトで保存可能な
rememberLazyListState(および対応するグリッド/ページャーの状態保持機構)を利用してください。
検証
- コンポーザブルの本体を編集してもスクロール位置とダイアログの状態が維持され、ツールウィンドウにティア 1 と表示される
-
MaterialThemeの色を編集するとティア 2 に引き上げられる(ステータスでティアを確認できる) -
rememberSaveableによって保持される状態は、ティア 2 のコンポジションリセット後も維持される -
ViewModelによって保持される状態は、ティア 3 のActivity.recreate()後も維持される -
rememberLazyListStateはティア 2 の再読み込み後もスクロール位置を維持する
参考資料
- HotSwan の状態保持ドキュメント:https://github.com/skydoves/compose-hotswan-web (
/docs/state-preservation内) - HotSwan JetBrains プラグインの掲載ページ:https://plugins.jetbrains.com/plugin/30551-compose-hotswan/
- Android Developers、「
rememberSaveableを使った UI 状態の保存」:https://developer.android.com/develop/ui/compose/state-saving - Android Developers、「
ViewModelの概要」:https://developer.android.com/topic/libraries/architecture/viewmodel - Android Developers、「
CompositionLocal」:https://developer.android.com/develop/ui/compose/compositionlocal