はじめに
前回の記事では、登録・編集画面の ViewModel 設計と状態管理について紹介しました。
今回は、本画面の設計において、特に意識した「状態をどこに持たせるか」と「画面の責務をどこまでにするか」の2点についてまとめます。
状態は一貫して ViewModel に集約する
画面・編集画面では、以下の保存対象となる状態をすべて ViewModel が保持しています。
- 選択されたカテゴリ
- サブカテゴリ
- アイコンや表示名
一方で、カテゴリー選択画面・サブカテゴリー選択画面は、
- 一覧を表示する
- ユーザー操作を受け取る
- 選択結果を呼び出し元に返す
という一時的なUIに徹し、選択結果を画面自身では保持しません。


これにより、以下のようなメリットがあります。
- 「今どこが状態を持っているのか」が常に明確
- 画面回転や再生成にも強い
- 登録・編集で設計を揃えやすい
画面遷移と状態更新を混ぜない
カテゴリ選択結果の受け渡しには、NavController の savedStateHandle を使用しています。
navController.previousBackStackEntry
?.savedStateHandle
?.set(key, selectedCategory)
navController.popBackStack()
この構成では、以下役割分担があります。
- Navigation は「画面を戻す」だけ
- 状態の反映は、戻り先の ViewModel が行う
画面遷移に状態を直接乗せないことで、
- パラメータの肥大化を防げる
- 編集途中の状態とズレにくい
- 後から画面構成を変更しやすい
といった利点がありました。
カテゴリ選択画面は「状態を持たないUI」と割り切る
今回のカテゴリ選択画面では、機能は実装していません。
- 選択中のハイライト表示
- 選択状態の保持
理由は、あくまでもカテゴリ選択前の一時的な選択となるため、状態を持たせるとViewModelが必要になることから役割を分けています。
また、カテゴリ選択後は「選択前の画面に戻る」という挙動に限定することで、Composableは表示と入力だけに集中できました。
編集機能は UI 状態として実装する
前提としてマスターデータに対しては編集、削除等は実施できないように以下で制御しています。
サブカテゴリー選択画面では、マスターデータを一部用意しつつ、ユーザー作成データとして以下の操作を可能にしています。
- サブカテゴリ名の追加
- 編集
- 削除




// ユーザー作成データであり、かつ編集モードの場合に削除、カテゴリ名の編集可能
val isUserEditable = isEditing && subCategorySource == "user"
// 編集モードだがユーザー作成ではない(マスターデータ)場合に無効化する
val isDisabled = isEditing && subCategorySource != "user"
このように分解することで、以下状態がコードから直感的に読み取れるようになりました。
- UIの見た目
- タップ時の挙動
- 操作可能・不可の制御
登録・編集で分けつつ、無理に抽象化しない
登録画面と編集画面はViewModelを分けていますが、カテゴリ選択画面(状態管理や画面遷移)のロジックは ViewModel、Composableで実装し、データ変換だけを共通化しています。
そのため、
- ViewModel 同士を直接依存させない
- Composable に Entity や DB モデルを持ち込まない
- 画面ごとの責務を肥大化させない
ことを目的に、変換専用の拡張関数を切り出しています。
おわりに
Compose では、UIの共通化、画面分割、状態管理をそれぞれ独立して考える必要がありますが、特に重要だと感じたのは 「その状態は最終的に保存されるのか?」 という視点でした。
この画面では特に状態管理で悩みましたが、次の基準を持って設計することで、画面数が増えても構成が破綻しにくくなったと感じています。
- 保存される状態 → ViewModel
- 一時的な選択や入力 → Composable

