【習慣トラッカー開発記 #7】習慣登録画面と編集画面の共通化について

はじめに

前回の記事では、Room × Flow × ViewModel を使ったデータ構造とDB設計について整理しました。

今回はその続きとして、習慣の登録画面と編集画面のUI設計について紹介します。

一見似ている2つの画面ですが、

  • 完全に分けて実装するのか
  • どこまで共通化するのか
  • 差分はどこで吸収するのか

など実装しながら修正し完成しました。

今回の前提構成

  • Jetpack Compose
  • ViewModelで状態管理
  • 登録画面・編集画面は別Composable
  • 共通UIは HabitForm に集約

構成イメージは以下の通りです。

HabitRegisterScreen
        │
        └── HabitForm(共通UI)
        │
HabitEditScreen
        └── HabitForm(共通UI + 差分UI)

登録画面

編集画面

なぜ「登録」と「編集」を分けたのか

登録画面と編集画面はUIが似ていますが、責務は異なります。

  • 登録画面
    • 新規習慣の入力(追加専用のシンプルな画面)
  • 編集画面
    • 既存データの更新
    • 習慣自自体の一時停止・削除や編集

そのため、画面自体は分けつつ、入力フォームだけを共通化する方針を取りました。

共通UIとしての HabitForm

HabitForm は「入力UIを表示すること」だけに責務を絞っています。

例えば、以下登録画面の例になります。

  • 習慣名
  • カテゴリアイコン
  • 開始・終了時間
  • 曜日選択
  • メモ入力
  • 保存ボタン

そして、状態の更新や保存処理は、すべて コールバック経由で外に委譲しています。

@Composable
fun HabitForm(
    habit: HabitUiModel,
    saveButtonText: String,
    onSave: (HabitUiModel) -> Unit,
    bottomContent: (@Composable () -> Unit)? = null
)

この設計により、HabitForm 自体は「どの画面で使われているか」を意識しません。

これにより、UIは「状態を受け取って表示するだけ」の存在になります。

画面差分は bottomContent で定義する

編集画面では、以下 登録画面には存在しないUI が必要になります。

  • 一時停止期間の設定
  • 削除ボタン

条件分岐でUIを切り替えるのではなく、「Composableをそのまま差し込む」形にしたのがポイントです。

HabitForm(
    habit = habit,
    saveButtonText = "保存する",
    onSave = onSave,
    bottomContent = {
        // 編集画面専用UI
        SuspendSetting()
        DeleteButton()
    }
)

この形にすることで以下メリットがあります。

  • HabitForm が肥大化しない
  • 画面ごとの差分が明確
  • 将来の拡張もしやすい

UI状態とViewModel状態の切り分け

曜日選択や時間ピッカーなど、一時的なUI操作の状態は HabitForm 側で保持しています。

一方で、最終的に保存される値はすべて ViewModel コールバックで通知します。

onDayOfWeekChange(selectedDays)
onStartTimeChange(updatedStartTime)

これらをすることでComposeとViewModelで役割が明確になります。

  • ViewModelはビジネスロジックに集中
  • ComposableはUI表現に集中

登録画面での利用例

HabitRegisterScreen {
    HabitForm(
        habit = habit,
        saveButtonText = "登録する",
        onSave = onSave
    )
}

編集画面での利用例

HabitEditScreen {
    HabitForm(
        habit = habit,
        saveButtonText = "保存する",
        onSave = onSave,
        bottomContent = {
		      // 編集画面で使用するフォーム度を配置
        }
    )
}

bottomContentを使用することで同じフォームを使いながら、画面の責務は明確に分離できています。

まとめ

今回のポイントは以下です。

観点方針
画面構成登録・編集は分離
共通UIHabitFormに集約
差分UI(編集画面特有の機能)bottomContentで注入
状態管理UI状態とViewModel状態を分離

Composeでは「どこまで共通化するか」を意識して組み立てました。

また、Composableの責務を小さく保つことで、より役割を分担できたかと思います。

次回は、登録・編集画面それぞれのViewModel 設計と状態管理についてまとめます。

UIの共通化までは比較的スムーズに進みましたが、「状態をどこで持つか」でかなり悩みました。

次回はそのあたりの試行錯誤や、最終的にどう整理したのかを、

実装ベースで深掘りしていく予定です。

タイトルとURLをコピーしました