Claude Code を使った Flutter UI イテレーション:ユーザーストーリーをウィジェットツリー、状態、ナビゲーションに変換しつつ、変更をモジュール化してレビューしやすく保つ実践的ループ。

text\nlib/\n features/\n orders/\n screens/\n widgets/\n state/\n routes.dart\n\n\nウィジェットは小さく合成的に保ってください。ウィジェットが明確な入力と出力を持つと、状態に触れずにレイアウトを変えられ、UI を書き換えずに状態を変更できます。プレーンな値とコールバックを受け取るウィジェットを優先し、グローバル状態を避けてください。\n\nレビュー可能なループの例:\n\n- そのスライスについて 3~6 行の UI スペックを書く(何が現れるか、タップが何をするか、読み込み/エラーの見た目)。\n- 必要最小限のファイルのみを生成または編集する(多くの場合は 1 画面と 1〜2 ウィジェット)。\n- 画面を実行して、1 回のクリーンアップパス(名前付け、間隔、未使用 prop の削除)を行う。\n- スライスに合ったメッセージでコミットする。\n\n厳格なルールを設けてください:すべての変更は元に戻しやすく、分離しやすいこと。作業中に横道のリファクタを避けてください。関連しない問題に気づいたら、書き留めて別のコミットで修正します。\n\nツールがスナップショットとロールバックをサポートするなら、各スライスをスナップショットポイントとして使ってください。Koder.ai のようなプラットフォームはスナップショットとロールバックを含み、思い切った UI 変更を試す際に安全性を高めます。\n\nさらに、初期の反復を穏やかに保つ習慣:共有コンポーネントを編集するより新しいウィジェットを追加することを優先してください。共有コンポーネントは小さな変更が大きな差分に変わる箇所です。\n\n## ステップバイステップ:ユーザーストーリーからウィジェットツリーを生成する\n\n速い UI 作業が安全であるのは、考えることとタイプすることを分けるときです。まずコード生成の前に明確なウィジェットツリープランを作ってください。\n\n1) ウィジェットツリーのアウトラインだけを依頼します。ウィジェット名、階層、各部分が何を表示するかが欲しいだけです。まだコードは書かないでください。ここで欠けている状態や空の画面、変なレイアウト選択を安価に見つけられます。\n\n2) コンポーネントの責任分解を依頼します。各ウィジェットは集中させます:あるウィジェットはヘッダー、別はリスト、別は空/エラー UI を担当する、といった具合です。後で状態が必要ならその旨をメモしておきますが、今は実装しません。\n\n3) 画面のスキャフォールドとステートレスウィジェットを生成します。まずはプレースホルダコンテンツと明確な TODO を含む単一の画面ファイルから始めます。入力は明示的に(コンストラクタパラメータ)しておき、後で実際の状態を差し替えられるようにします。\n\n4) スタイリングとレイアウトの詳細は別パスにします:間隔、タイポグラフィ、テーマ、レスポンシブ動作。スタイリングは別差分にしてレビューを単純に保ちます。\n\n### 有効なプロンプトパターン\n\nアシスタントが実装不能な UI を想像しないように、制約を前提に置いてください:\n\n- 対象デバイス(電話のみ、タブレットも、向き)\n- デザイン制約(Material 3、既存テーマ色、間隔ルール)\n- ナビゲーション期待(戻る挙動、deep link があれば)\n- 受け入れ基準(何が表示されタップ可能であるべきか)\n- 既存コードの境界(どのファイル/ウィジェットを残すか、命名規則)\n\n具体例:ユーザーストーリーが "As a user, I can review my saved items and remove one." の場合、アプリバー、アイテム行のあるリスト、空状態を含むウィジェットツリーを依頼します。次に SavedItemsScreen、SavedItemTile、EmptySavedItems のような分解を要求します。まずステートレスなスキャフォールドとダミーデータで始め、最後にスタイリング(区切り線、パディング、明確な削除ボタン)を別パスで追加します。\n\n## UI コードを膨らませずに状態処理を追加する\n\nイテレーションが崩れるのは、ウィジェットごとに決定ロジックが入り始めたときです。ウィジェットツリーはダムに保ち、状態を読み取って描画するだけにします。\n\nまず状態を平易な言葉で命名してください。ほとんどの機能は単に "loading" と "done" 以上が必要です:\n\n- Loading(初回読み込みやリフレッシュ)\n- Empty(まだデータがない)\n- Error(リクエスト失敗、権限拒否)\n- Success(データ準備完了)\n- Partial input(フォームが開始されているが有効でない状態)\n\n次に状態を変えるイベントを列挙します:タップ、フォーム送信、プルトゥリフレッシュ、戻る、リトライ、ユーザーがフィールドを編集した、など。事前にこれをやっておくと後の推測が減ります。\n\n### ウィジェットから状態を分離する\n\n機能ごとに 1 つの状態アプローチを選んで貫いてください。目標は "ベストなパターン" ではなく、差分が一貫することです。\n\n小さな画面では、ChangeNotifier や ValueNotifier のようなシンプルなコントローラで十分なことが多いです。ロジックは 1 箇所にまとめます:\n\n- 入力:UI からのイベント(submit、refresh、edit)\n- 出力:UI が描画する単一の状態オブジェクト\n- 副作用:API 呼び出しやナビゲーション要求\n\nコードを書く前に、状態遷移を平易な英語(あるいはチームが使う言語)で書いてください。ログイン画面の例:\n\n"ユーザーが Sign in をタップしたら:Loading にする。メールが無効なら Partial input に留めてインラインメッセージを表示。パスワードが間違っていれば Error をセットしてリトライを有効化。成功したら Success にして Home に遷移。"\n\nそれに合致する最小限の Dart コードを生成してください。レビューは差分とルールを比較するだけで済みます。\n\n### 無効な入力に対するテスト可能なルールを追加する\n\nバリデーションを明示化します。無効な入力時にどうするか決めてください:\n\n- 送信をブロックするのか、送信を許可してエラーを表示するのか?\n- どのフィールドがどのタイミングでエラーを表示するか?\n- 戻ると途中入力を破棄するか保持するか?\n\nこれらを文章化しておけば、UI はクリーンに保てて状態コードも小さくできます。\n\n## 実際のユーザー挙動に合うナビゲーションを設計する\n\n良いナビゲーションは小さな地図として始めます。各ユーザーストーリーについて、次の 4 つの瞬間を書き出してください:ユーザーがどこから入ってくるか、次に最もありそうなステップ、どうやってキャンセルするか、"戻る" が何を意味するか(前の画面か安全なホームか)。\n\n### まずルートマップを作り、次に画面間で何を渡すかをロックする\n\nシンプルなルートマップは、後で手戻りを生む質問に答えます:\n\n- Entry:どの画面が最初に開き、どこから(タブ、通知、deep link)来るか\n- Next:主要な前進パス\n- Cancel:中断したときにどこに着地するか\n- Back:戻ることが許されているか、何を保持するか\n- Fallback:必要なデータが足りないときにどこに飛ばすか\n\n次に画面間で渡すパラメータを定義します。明示的にしてください:IDs(productId、orderId)、フィルタ(期間、ステータス)、ドラフトデータ(部分入力のフォーム)。これを省くと、コンテキストを見つけるためにグローバルシングルトンに詰め込むか、画面を再構築する羽目になります。\n\n### Deep link と結果を返すパターンに備える\n\nDeep link は当日に出荷しなくても重要です。ユーザーがフローの途中に着地したときにどうするか決めておく:欠損データを読み込めるか、安全な入り口にリダイレクトするか。\n\nまた、どの画面が結果を返すかを決めてください。例:"住所選択"画面は addressId を返し、チェックアウト画面はフルリフレッシュせずに更新する。返す形は小さく型付きにしておくと差分が読みやすくなります。\n\n実装前にエッジケースを列挙してください:未保存の変更(確認ダイアログを表示)、認証が必要(ログイン後に再開)、データが欠けている/削除されている(エラー表示と明確な脱出経路)。\n\n## UI 変更をレビューしやすくモジュール化する\n\n速く反復するときの本当のリスクは "UI が間違っている" ことではなく "レビューできない UI" です。チームメイトが何が変わったのか、なぜ変わったのか、何が安定しているのかがわからなければ、次の反復はどんどん遅くなります。\n\n役立つルール:まずインターフェース(API)を固定し、それから内部を動かす。公開ウィジェットの props(入力)、小さな UI モデル、ルート引数を安定化させてください。これらが名前付きで型付きなら、ウィジェットツリーを大きく変えても他の部分を壊しません。\n\n### 差分を小さく保つための小さな安定した縫い目を優先する\n\nコード生成前に差分フレンドリーな計画を出すように依頼してください。どのファイルが変わり、どれを untouched にするかを示す計画が欲しいです。これでレビューが集中し、意図せぬリファクタによる振る舞い変更を防げます。\n\n差分を小さく保つパターン:\n\n- 公開ウィジェットは薄く保つ:必要なデータとコールバックだけを受け取る。\n- ビジネスルールは早めにウィジェットの外に出す:コントローラ/ビューモデルに移す。\n- UI が頻繁に変わらなくなったら、明確で型付きの API を持つ再利用可能ウィジェットに抽出する。\n- ルート引数は明示的に(多くの optional フィールドより単一の引数オブジェクト)。\n- PR の説明に短い変更ログを入れる:何を変えたか、なぜ、何をテストするか。\n\n### レビュアーが好む具体例\n\nユーザーストーリーが "チェックアウトから配送先住所を編集できる" の場合、まずルート引数を固定します:CheckoutArgs(cartId, shippingAddressId) は安定させます。次に画面内部を反復します。レイアウトが落ち着いたら AddressForm、AddressSummary、SaveBar に分割します。\n\n状態処理が変わっても(たとえばバリデーションがウィジェットから CheckoutController に移るなど)、レビューは読みやすいままです:UI ファイルは主に描画に関する変更だけで、コントローラにロジックの差分がまとまって見えます。\n\n## AI アシスタントと反復する際の一般的な失敗と罠\n\n反復を遅くする最速の方法は、アシスタントに「すべてを一度に」変更させることです。1 つのコミットがレイアウト、状態、ナビゲーションを同時に変えると、レビュアーは何が原因で壊れたのかわからず、ロールバックが面倒になります。\n\n安全な習慣は、1 反復=1 意図です:ウィジェットツリーを整え、その後で状態を配線し、最後にナビゲーションを接続します。\n\n### 混沌を生む間違い\n\n生成されたコードが画面ごとに新しいパターンを発明するのを許すのは問題です。1 ページは Provider を使い、次は setState、3 番目はカスタムコントローラを持つようになると、アプリはすぐに一貫性を失います。小さなパターンセットを選び、それを守ってください。\n\n別の問題は build() 内に非同期処理を置くことです。クイックデモでは動いても、リビルド時に複数回呼ばれ、チラつきや追跡困難なバグを招きます。呼び出しは initState()、ビューモデル、あるいは専用コントローラに移し、build() は描画に集中させてください。\n\n命名は静かな罠です。コンパイルは通るが Widget1、data2、temp のような名前だと将来のリファクタが苦労します。明確な名前はアシスタントが次の変更をより良く生成するのにも役立ちます。\n\n回避ガードレール:\n\n- 1 回の反復で変更するのは「レイアウト」「状態」「ナビゲーション」のうち 1 つだけ\n- 機能内で同じ状態パターンを使う\n- build() 内にネットワークや DB 呼び出しを置かない\n- プレースホルダの名前は機能を増やす前に変更する\n- ネストが増える代わりにウィジェットを抽出する\n\n### ネスト地獄の罠\n\n視覚的なバグ修正で Container、Padding、Align、SizedBox をどんどん重ねると、ツリーは読みにくくなります。ボタンの位置がおかしいときは、ラッパーを増やすのではなく、まずラッパーを減らす、単一の親レイアウトを使う、あるいは制約を持つ小さなウィジェットに抽出することを試してください。\n\n例:読み込み時に合計金額がジャンプするチェックアウト画面。アシスタントは価格行を "安定化" するためにさらに多くのウィジェットでラップするかもしれません。よりクリーンな修正は、行構造を変えずに単純な読み込みプレースホルダでスペースを確保することです。\n\n## 次のコミット前のクイックチェックリスト\n\nコミット前に 2 分のパスでユーザー価値と意図しないリグレッションから保護してください。目的は完全性ではなく、この反復がレビューしやすく、テストしやすく、元に戻しやすいことを保証することです。\n\n### コミット準備チェックリスト\n\nユーザーストーリーを一度読み、実行中のアプリ(あるいは簡単なウィジェットテスト)に対して次を検証します:\n\n- ウィジェットツリーがストーリーと一致するか: 受け入れ基準の主要要素が存在し、可視であるか。テキスト、ボタン、空白が意図的に見えるか。\n- すべての状態に到達できるか: Loading、Error、Empty がコード上のスケッチで終わらず、各状態をトリガーできて見栄えが受け入れられるか。\n- ナビゲーションと戻る挙動は妥当か: 戻るが期待する画面に戻り、ダイアログが閉じ、deep link(使う場合)は妥当な場所に着地するか。\n- 差分は小さく所有されているか: 変更が責務の明確な少数ファイルに限定されているか。ドライブバイリファクタはないか。\n- ロールバックがきれいか: これを revert しても他の画面がビルド・実行できるか。一時フラグやプレースホルダ資産を残していないか。\n\n簡単な現実チェック:新しい Order details 画面を追加したなら、(1) 一覧から開ける、(2) ローディングスピナーが見える、(3) エラーをシミュレートできる、(4) 空の注文を見ることができる、(5) 戻るで一覧に戻れる——という最低限の動作ができるべきです。\n\nワークフローがスナップショットやロールバックをサポートするなら、大きなレイアウト変更前にスナップショットを取り、メインブランチを危険にさらさないでください。Koder.ai のようなプラットフォームはこれをサポートします。\n\n## 現実的な例:ユーザーストーリーから 3 回の反復で画面まで\n\nユーザーストーリー:"As a shopper, I can browse items, open a details page, save an item to favorites, and later view my favorites."。言葉から画面までを 3 つの小さなレビュー可能なステップで進めます。\n\n反復 1: ブラウズリスト画面のみに集中します。実データに結びつかないレンダリングができる程度のウィジェットツリーを作る:Scaffold、AppBar、プレースホルダ行の ListView、読み込みと空状態の明確な UI。状態はシンプルに:loading(CircularProgressIndicator)、empty(短いメッセージと場合によっては Try again ボタン)、ready(リストを表示)。\n\n反復 2: 詳細画面とナビゲーションを追加します。onTap がルートを push し、小さなパラメータオブジェクト(例:item id、title)を渡すことを明示します。詳細ページはまず読み取り専用で、タイトル、説明プレースホルダ、Favorite アクションボタンを持ちます。ポイントはストーリーに合わせること:list -> details -> back、余計なフローはなし。\n\n反復 3: お気に入りの状態更新と UI フィードバックを導入します。お気に入りの単一の真実(メモリ内でも可)を追加し、両画面に配線します。タップでアイコンを即時更新し小さな確認(SnackBar など)を表示。次に同じ状態を読む Favorites 画面を追加し、空状態を処理します。\n\nレビュー可能な差分の例:\n\n- browse_list_screen.dart:ウィジェットツリー+loading/empty/ready UI\n- item_details_screen.dart:UI レイアウトとナビゲーションパラメータ受け取り\n- favorites_store.dart:最小限の状態ホルダと更新メソッド\n- app_routes.dart:ルートと型付きナビゲーションヘルパー\n- favorites_screen.dart:状態を読み空/リスト UI を表示\n\nもし 1 つのファイルが "すべてが起こる場所" になったら、次に進む前に分割してください。小さく名前のわかるファイルは次の反復を速く安全にします。\n\n## 次のステップ:このループを機能全体で繰り返せるようにする\n\nワークフローが "集中しているときだけ機能する" のでは困ります。画面を切り替えたり、別のメンバーが触ったら壊れます。ループを習慣にするには書き留め、変更サイズにガードレールを設けてください。\n\n### 再利用できるプロンプトテンプレートを作る\n\nチームで同じテンプレートを使えば、すべての反復が同じ入力から始まり同じ種類の出力を生みます。短く具体的に:\n\n- ユーザーストーリー+受け入れ基準(Done の定義)\n- UI 制約(デザインシステム、間隔、再利用すべきコンポーネント)\n- 状態ルール(状態はどこに置くか、ローカルか共有か)\n- ナビゲーションルール(ルート、deep link、戻る挙動)\n- 出力ルール(触るファイル、更新すべきテスト、差分で説明すること)\n\nこれによりアシスタントが途中で新しいパターンを思いつく確率が下がります。\n\n### 差分の "小ささ" を定義して予測可能にする\n\nレビューで守りやすい小ささの定義を決めてください。例:各反復で変更ファイル数を制限し、UI リファクタと振る舞い変更を分ける。\n\nシンプルなルール例:\n\n- 反復ごとに変更は 3〜5 ファイル以内\n- 1 反復につき 1 つの新しいウィジェットか 1 つのナビゲーションステップ\n- 反復途中で新しい状態管理手法を導入しない\n- 次の反復に行く前に変更はコンパイルして動作確認をする\n\n悪いステップをすばやく取り消せるようチェックポイントを設けてください。最低でもコミットやローカルスナップショットを使い、大きなリファクタ前に保存します。スナップショット/ロールバックをサポートするなら積極的に使ってください。\n\nエンドに:チャットベースで Flutter アプリを生成・修正できるワークフローを探しているなら、Koder.ai のようなツールは計画モードで生成前にプランと想定ファイル変更をレビューできる機能を提供します。小さくてテスト可能な UI スペック を最初に作成してください。3~6 行で次をカバーします:\n\n- 表示されるもの(主要ウィジェット/コンポーネント)\n- タップの挙動(主要なインタラクション)\n- 読み込み/エラー/空状態の見た目\n- 30秒で検証する方法\n\nそのスライス(たいていは 1 画面 + 1〜2 ウィジェット)のみを実装します。
ユーザーストーリーを次の 4 つに分けて書き換えます:\n\n- 画面(Screens):何が変わるか、何がそのままか\n- コンポーネント(Components):新しいウィジェットと配置場所\n- 状態(States):loading、empty、error、success(それぞれ何を表示するか)\n- イベント(Events):タップ、戻る、リトライ、リフレッシュ、フォーム編集\n\n受け入れ条件をすぐに説明できないなら、そのストーリーはまだ曖昧で、クリーンな差分には向きません。
まず ウィジェットツリーのアウトラインのみを生成するように頼みます(名前+階層+各部分が何を表示するか)。コードはまだ求めません。\n\n次に コンポーネントの責務分解(どのウィジェットが何を担当するか)を要求します。\n\nその後に、明示的な入力(値+コールバック)を持つ ステートレスなスキャフォールド を生成し、スタイリングは別パスにします。
反復ごとに 1 つの意図だけ を扱うというルールを適用してください。\n\n- 反復 A:ウィジェットツリー/レイアウト\n- 反復 B:状態の接続\n- 反復 C:ナビゲーションの接続\n\n一つのコミットでレイアウト、状態、ルートすべてを変えると、レビュアーは何が原因でバグが出たかわかりづらくなり、ロールバックが大変になります。
ウィジェットを“ダム(無垢)”に保ち、状態をレンダリングするだけにします。実用的なデフォルト:\n\n- 1 つのコントローラ/ビューモデルがイベントと非同期処理を担当\n- 単一の状態オブジェクト(loading/empty/error/success)を UI に公開\n- UI は状態を読み、コールバック(retry、submit、toggle)を呼ぶ\n\nbuild() に非同期呼び出しを置かないでください。リビルドごとに複数回走る原因になります。
実装前に画面で想定される状態を言葉で整理してください。一般的なパターン:\n\n- Loading: スピナーやスケルトンを表示\n- Empty: メッセージ+アクション(例:Retry)\n- Error: インラインエラー+Retry\n- Success: コンテンツを描画\n\nそして、それらの間を移動させるイベント(refresh、retry、submit、edit)を列挙します。コードは書かれたルールと比較しやすくなります。
ストーリーごとに小さなフローマップを書いてください:\n\n- Entry(入り口):どこから来るか(タブ、通知、deep link)\n- Next(次):主要な前進パス\n- Cancel(キャンセル):中断したらどこに行くか\n- Back(戻る):戻るは前の画面か、セーフなホームか\n- Fallback(フォールバック):必須データがない場合の遷移\n\n画面間で渡すパラメータ(productId、orderId、フィルタ、ドラフト)を明確にしておくと、グローバルに状態を詰め込む悪習を避けられます。
機能ごとのフォルダー構成をデフォルトにすると変更が分散しません。例:\n\n- lib/features/<feature>/screens/\n- lib/features/<feature>/widgets/\n- lib/features/<feature>/state/\n- lib/features/<feature>/routes.dart\n\n各反復は、1 つの機能フォルダーにフォーカスし、余計なドライブバイリファクタを避けてください。
インターフェース(公開 API)を安定させ、内部は自由に動かしてください。ルール:\n\n- 公開ウィジェットの prop は小さく型を明確に\n- 値+コールバックを渡すことを優先し、グローバル状態への直接参照を避ける\n- ルート引数は明示的に(単一の args オブジェクトが良い)\n- 同じ箇所を何度も変えなくなったら抽出して再利用する\n\nレビュアーは、レイアウトが動いても入力/出力が安定していることを重視します。
2 分ほどで次をチェックしてください:\n\n- 読み込み/空/エラー/成功 の各状態はトリガーして見られるか\n- 戻る が期待通りに動くか(不自然なジャンプはないか)\n- 変更は責務が明確な少数のファイルに限定されているか\n- 一時フラグやプレースホルダが残っていないか\n\nワークフローがスナップショットやロールバックをサポートするなら、レイアウト大改修前にスナップショットを取っておくと安全です。