mcpField
P
preserving-state-across-reloads

preserving-state-across-reloads

github.com/skydoves開発520126 回インストール19 週間前に更新
リポジトリ

preserving-state-across-reloads について

Compose HotSwanを使うAndroid開発者向けのスキルです。3段階のリロードがUI状態に与える影響を説明し、反復作業中に状態を保つ方法を示します。

主な機能

  1. 013段階のリロードと状態保持を解説
  2. 02状態ホルダーから状態消失を診断
  3. 03rememberSaveableによるUI状態の保持
  4. 04長期作業の状態をViewModelへ移動
  5. 05リロード段階が上がる編集を回避

活用シーン

  1. 01リロード後にスクロールやダイアログ状態が失われた場合
  2. 02編集によって起きるリロード段階の確認
  3. 03コンポジションリセットに適した状態管理の選択
  4. 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 の再読み込み後もスクロール位置を維持する

参考資料

コメント