この記事でやったこと
Anthropic Labs が 2026-04-17 にリリースした Claude Design で映画レビューアプリのデザインを作り、Handoff Bundle を Claude Code (Opus 4.7) に渡して Flutter アプリとして実装させてみました。
その過程を備忘録としてまとめてます。
サンプルプロジェクト: Movie Movie(ダークモード映画レビューアプリ)
スタック: Claude Design → Handoff Bundle → Claude Code → Flutter 3.32.8 / Riverpod
成果物: Dart 25 ファイル / Provider 14 個 / fvm flutter analyze クリーン / Android 実機で起動確認済み
まず結果から
Claude Design
Flutter (Claude Code)
| Home | Timeline | Profile |
|---|---|---|
![]() |
![]() |
![]() |
| トレンド一覧(hero / cards / list の 3 レイアウト切替) | フィルタ可能な投稿フィード | 統計 + 月グルーピングされた視聴履歴 |
| Detail | Review | Search |
|---|---|---|
![]() |
![]() |
![]() |
| ヒーロー + タイトル重ね + 配信サービス可用性 | ドラッグでスコア入力(0–10 / 0.5 刻み)+ タグ / ネタバレ | recent + mood ブラウズ / 検索結果 |
うまくHandoff Bundleが機能しFlutterのUIを実装できてます。
ワークフローは以下 2 Stepになります。
| フェーズ | 環境 | 成果物 |
|---|---|---|
| Step 1: デザイン | Claude Design(ブラウザ) | React + Babel の Hi-Fi プロトタイプ / Handoff Bundle (ZIP) |
| Step 2: 実装 | Claude Code(ローカル CLI) | Flutter プロジェクト(Dart 25 ファイル) |
Step 1 と Step 2 での引き渡しの接点が Handoff BundleというClaudeDesignで出力できるファイル群です。
Step 1: Claude Design でデザインする
1.1 最初のドラフトを出す
High fidelityを選択しましょう。
まずプロンプトは 1 行:
モダンでダークモードベースの映画レビュー Flutter アプリをデザインして。ホーム画面にはトレンドの映画一覧、詳細画面には評価とレビュー投稿フォーム、プロフィール画面には視聴履歴を表示して。
すると具体的な設計を詰める質問セットをClaude Design が返してきます。
回答:
| 項目 | 指定 |
|---|---|
app_name |
Movie Movie |
device |
iOS (iPhone) |
screens |
ホーム(トレンド)/ 映画詳細 / レビュー投稿 / プロフィール / 検索 |
vibe |
モダン・ミニマル(落ち着いたダーク、上品) |
accent |
ライム / グリーン |
rating_style |
10 点満点スコア |
interactions |
詳細画面へのヒーロートランジション |
content_density |
情報リッチ |
imagery |
プレースホルダー(striped SVG + タイトル) |
tweaks |
ホームレイアウト(cards / list / hero)/ 言語(日本語 / 英語) |
「Tweaks」は、Claude Designで作成したプロトタイプの右下に表示される浮遊パネルのことのようです。

出てきたのは React + Babel で動く HTML プロトタイプ。primitives.jsx / poster.jsx / ios-frame.jsx / screen-*.jsx に分かれた、そこそこのコンポーネント構成。
「Flutter 用なのに React?」と思うかもしれませんが、Claude Design のプレビューはブラウザ内で動く React/JSX で統一されているので、Flutter に変換するのは後段の Claude Code の仕事。Step 1 の段階では Flutter はまだ 1 行も書きません。
1.2 赤ペン修正:一発では通らない
Claude Design の肝は「生成物にインラインコメントで修正指示を返せる」こと。ここでのやり取りが、Step 1 で一番書いておきたいパート。
誤解されたナビゲーション指示
最初の指示:
Searchの下タブをHomeの上に移動
Claude はタブバー内で Search を Home の左に並べ替えた。
こちらが期待していたのは「下タブから Search を消して、Home のヘッダーに検索ボタンを置く」だったので、完全にすれ違い。言い直しました:
違います。Searchの下タブを削除してHomeのお知らせの横にボタンを追加する
今度は完璧。タブの削除、ヘッダーへのアイコン追加、さらに**「タップしたら検索画面に遷移し、戻るで戻れる」という画面遷移ロジックまで**セットで更新された。
この修正を従来の作業に落とし込むと、以下のような書き換えを行うことになります。
- BottomNavigationBar の items から Search 用のBottomNavigationBarItem を削除
- AppBarの拡張: HomeView の Scaffold 内にある AppBar(またはカスタムヘッダー)の actions プロパティに、IconButton(検索アイコン)を追加
- ルーティングの紐付け: そのボタンの onPressed に、Navigator.push または GoRouter.go で検索画面へ飛ばす処理を記述
これを自分でやると「ウィジェットのツリーを掘り下げて、あちこち書き換える」手間が発生しますが、Claude Designなら会話だけでこのリファクタリングが秒で終わっているのが驚異的で心強いですね。
コメント機能でのマージン調整
ScoreBadge(9.1/10 のバッジ)が右下に張り付きすぎていたので、該当要素にインラインコメントで:
マージンを調整して欲しい
Claude は視覚的にバッジの位置を判定し、ヒーロー内部のパディングを 20 → 24 に変更。数値は一切指定していないです。
| before | after |
|---|---|
![]() |
![]() |
タイトル重複をロジックで解く
ヒーローでタイトルが 2 回描画されていたのでコメント:
タイトルが重複して表示されているのでこちらは表示なしにする
Claude の応答:
Poster コンポーネントは titleSize > 0 のとき {t} をレンダーする。
HomeHero は titleSize={28} なので、Poster のタイトル + Hero overlay のタイトル
の両方が描画されていた。Poster 側を titleSize={0} で非表示にする。
単にテキストを消すのではなく、プロパティベースで解決。他画面で Poster 単体を使うときは titleSize を指定すればタイトルが出る、という再利用性も保たれている。
日本語の改行問題
プロフィールタブの「プロフィール」が折り返して 2 行になっていた:
一行で表示したい
→ whiteSpace: nowrap + 幅拡張で対応。
UX の改善依頼(曖昧なやつ)
詳細画面で上映中か配信サービスで配信されていればそのリンクを表示してUXを改善したい
具体的な指定はなし。Claude は自分で以下を判断:
- 劇場公開中 → ライム色の視認性高めハイライトカード(劇場名・上映館数・上映時間)
- 配信サービス → 横スクロールタイル(サービス名・サブスク / レンタル価格・公開予定日)
- ダミーサービス名:Plexine / Stereofilm / Talaya / StreamRoom(実在しない)
「もっと増やして」と投げたら Vinebox / Kinoscape / Branchcast を追加、4〜6 サービスが並ぶ構成に。UI のパターン(カード vs 横スクロールタイル)を文脈で使い分ける判断が入っている。
1.3 Handoff Bundle を書き出す
デザインが固まったら、右上メニューから「Handoff to Claude Code」を選ぶ。出てくる選択肢は 2 つ:
- コマンドとして、ターミナルに貼り付ける
- ZIP 直ダウンロード
ZIP を開けた中身:
movie-app/
├── README.md ← "# CODING AGENTS: READ THIS FIRST"
├── chats/chat1.md ← デザイン中のやり取り全文
└── project/
├── Movie Movie.html ← React + Babel のプロトタイプ本体
├── data.jsx ← モックデータ
├── primitives.jsx ← ScoreBadge / Chip / TabBar / 色トークン
├── poster.jsx ← ストライプ SVG ポスター
├── availability.jsx ← 劇場 / ストリーミング
├── screen-home.jsx ← hero / cards / list 3 レイアウト
├── screen-detail.jsx
├── screen-review.jsx
├── screen-search.jsx
├── screen-profile.jsx
├── screen-timeline.jsx
└── ios-frame.jsx
README.md の冒頭:
CODING AGENTS: READ THIS FIRST
Recreate them pixel-perfectly. Match the visual output; don't copy internal structure.
デザインの納品物自体が、AI エージェントに対する仕様書として書かれています。
特に注目すべき3点:
-
起点は
Movie Movie.html— ユーザーが開いていた=メインデザインの可能性大。インポート先まで追跡すること - ピクセルパーフェクト、ただし構造はコピーしない — 見た目を一致させる。プロトタイプのHTML構造はそのまま使わず、ターゲット技術(React等)で書き直す
- スクショ禁止・ソース直読 — 寸法や色はHTML/CSSに全部書いてある。レンダリングは無駄
日本語訳
https://github.com/nabetarojp/movie_movie/blob/main/handoff/movie-app/README.ja.md
Step 1 のまとめ
- Claude Design はブラウザ内で完結。プロンプト → 質問 → ドラフト → 赤ペン修正 → Handoff Bundle
- 出力形式は React + Babel の HTML プロトタイプ。Flutter 固有の情報はまだ入っていない
- Bundle には AI エージェント向けの README が同梱される(これが Step 2 の入り口)
Step 2: Claude Code で Flutter に実装する
ここからは環境がガラッと変わりまして、ローカルのターミナルで Claude Code を起動する。Claude Design 側のコンテキストは Claude Code からは見えない。引き継がれるのは Handoff Bundle の中身だけ。
最終的なコードはこちら
2.1 Claude Code への渡し方
ZIP を展開したディレクトリで Claude Code を起動し、以下を渡す:
- Handoff Bundle のパス(
README.mdから読み始める) - 実装側の制約(ここで Flutter 固有の要件を初めて指定する)
- Flutter 3.32.8 / Dart 3.8.1 に
fvmで pin - Riverpod 4 層アーキテクチャ(UI / 状態 / ドメイン / データ)
-
ref.watch/ref.readの使い分けを規約化 - Repository は必ず Provider 経由で注入(テストで override 可能にする)
- Flutter 3.32.8 / Dart 3.8.1 に
アーキテクチャ指定は Claude Design 側ではなく Claude Code 側でやる。デザイン段階では「Flutter で作る」としか言っていないので、Riverpod を使ったアーキテクチャーであったり、Provider の粒度、規約は全部 Step 2 のプロンプトに含める。
2.2 生成されたディレクトリ構造
lib/
├── main.dart
├── core/theme/
│ ├── app_colors.dart ← デザイントークン(Handoff から抽出)
│ └── app_theme.dart
├── data/
│ ├── models/movie.dart ← Movie / HistoryEntry / Review / TimelineEntry / Availability
│ ├── mock_data.dart
│ └── repositories/movie_repository.dart
├── providers/
│ ├── repository_providers.dart
│ ├── app_settings_provider.dart ← 言語 / Home レイアウト
│ ├── navigation_provider.dart ← tab / detailId / reviewing / toast
│ ├── review_draft_provider.dart ← ReviewDraft + SubmitReview(AsyncNotifier)
│ └── movie_providers.dart ← trending / search / genre / timelineFilter / profileTab
├── widgets/
│ ├── score_badge.dart ← 8.5 以上でアクセント塗り (sm/lg/xl)
│ ├── chip_pill.dart
│ ├── section_header.dart
│ ├── poster.dart ← CustomPainter で seed ベースのストライプ描画
│ ├── tab_bar_mm.dart
│ └── ios_device.dart ← デスクトップ時のみフレーム表示
└── features/
├── shell/app_shell.dart ← タブ + 詳細 overlay + レビューシート + FAB + トースト
├── home/home_screen.dart ← hero/cards/list 3 レイアウト
├── detail/
│ ├── detail_screen.dart
│ └── availability_section.dart
├── review/review_screen.dart
├── search/search_screen.dart
├── timeline/timeline_screen.dart
└── profile/profile_screen.dart
画面実装マッピング
| HTML (React) | Flutter 実装 | 対応 |
|---|---|---|
HomeScreen (hero/cards/list) |
HomeScreen |
✅ 3レイアウトすべて |
HomeHero |
_HomeHero |
✅ ポスター + overlay gradient + ScoreBadge(lg) |
HScroll |
_HScroll |
✅ 横スクロールポスター列 |
RankedRow |
_RankedRow |
✅ 順位 + ミニポスター + メタ + ScoreBadge |
DetailScreen |
DetailScreen |
✅ 500pxヒーロー + Transform.translate(-60) でタイトル被せ |
MetaRow, ReviewCard
|
_MetaRow, _ReviewCard
|
✅ |
Availability + TheatersCard + ServiceTile
|
AvailabilitySection + 内部 Widget |
✅ ストリーミング横スクロール |
ReviewScreen (StarRow / Tag / Spoiler toggle) |
ReviewScreen + _ScoreRow + _Toggle
|
✅ 20ドットのドラッグで 0〜10 (0.5 刻み) |
SearchScreen |
SearchScreen |
✅ Recent / Browse by mood / 検索結果 |
TimelineScreen + TimelinePost
|
TimelineScreen + _TimelinePost
|
✅ タグ・アクション行含む |
ProfileScreen + StatBlock + HistoryRow
|
ProfileScreen + _Stat + _HistoryRow
|
✅ 月別グルーピング |
TabBar |
MMTabBar |
✅ 3タブ (Home/Timeline/Profile) |
| React hero/sheet/panel アニメーション | TweenAnimationBuilder |
✅ Detail: scale 1.04→1.0 / Sheet: 下からスライドイン |
| FAB |
AppShell 内 Positioned
|
✅ Home タブのみ表示、シャドウ付き |
| Toast |
AppShell + ref.listen
|
✅ 2.2秒で自動クリア |
| IOSDevice (フレーム) |
IOSDevice (デスクトップ表示のみ) |
✅ モバイル実機では非表示 |
2.3 React → Flutter の翻訳
Handoff README に書いてある「内部構造はコピーせずにピクセルファーフェクトで再現しなさい」がどう実行されたか。
ポスター描画:SVG <pattern> → CustomPainter
React 側:
Flutter 側:
-
movie.idから_hashSeedを計算 -
angle = (seed % 4) * 30 + 15→ 15 / 45 / 75 / 105° のいずれか gap = 8 + (seed % 5)- 背景
#0D0D0Eに accent(movie.tag× α 0.22)のストライプを重ね、下半分は黒グラデでフェード
SVG 要素の DOM 構造は写してこない。Flutter に合わせて CustomPainter で書き直しているようですね。
| SVG 版 | Flutter 版 |
|---|---|
<pattern> の自動タイリング |
for ループで gap*2 間隔に縦帯を手動敷設 |
patternTransform="rotate()" |
canvas.translate(中央) → rotate → translate(-中央) の3点セット |
| タイルはみ出しをブラウザが処理 | 対角線長ぶんの extra padding を上下左右に確保 |
<linearGradient> の下フェード |
DecoratedBox + LinearGradient(stops: [0.55, 1.0]) を Painter 外に分離 |
角マーカー/タイトル/メタの <div>
|
Stack + Positioned + Text (フォントは google_fonts の JetBrainsMono で SF Mono を代替) |
overflow: hidden + borderRadius: 10
|
ClipRRect(borderRadius: 10) |
他の翻訳例
| React 版 | Flutter 版 |
|---|---|
CSS white-space: nowrap
|
Text の maxLines: 1 + overflow: TextOverflow.ellipsis
|
transform: translateY(-60px) でタイトル被せ |
Transform.translate(offset: Offset(0, -60)) |
| React の state lifting | Riverpod の Provider 階層 |
Part 2 のまとめ
- Claude Code は Claude Design のコンテキストを持たない。入り口は Handoff Bundle の
README.mdだけ - Flutter 固有の制約(バージョン / アーキテクチャ / 規約)は Part 2 のプロンプトで指定する
- React → Flutter の翻訳は「見た目を合わせて、構造はネイティブのイディオムに書き直す」
- 生成されたプロジェクトは
CLAUDE.mdで次回セッション用の規約を残す
未対応・次にやるなら
-
Tweaks パネルの移植: React 版の実行時言語切替 / レイアウト切替 UI は未移植。
appSettingsProviderの API は既にあるので、設定画面を 1 つ足せば動く -
実データ接続:
MovieRepositoryはconstの in-memory。REST / GraphQL クライアントを書いて Provider をoverrideWithProviderで差し替えるだけ -
テスト:
ProviderContainer(overrides: [movieRepositoryProvider.overrideWithValue(FakeRepo())])パターンは書きやすい構造になっているが、実テストはまだ 0 件
やってみて分かったこと
- Claude Design と Claude Code は別ツール別責務。前者はデザイン+プロトタイプ、後者はプロジェクトへの実装。引き渡しは Handoff Bundle で行う
- Flutter の知識を入れる場所は Step 2。Step 1 では「ダークで情報リッチ」程度の抽象的な指示で十分。アーキテクチャを前倒しで指定する必要はない
- 一発で意図を伝えるのは諦めていい。誤解したら言い直す。1 ターンの修正が Figma での作業より軽い
- 曖昧な指示(Step 1 側)は AI に任せる価値がある。「UX を改善したい」レベルでも文脈を踏まえた構造を選んでくる
- 指定すべきは「設計」と「規約」(Step 2 側)。アーキテクチャ、状態管理、命名など、人間がピン留めしないと、生成コードは「動くけど保守しにくい」ものになるので、ここはエンジニアのスキル次第
画像の写経、プロパティの読み取り、規約に従ったボイラープレート、この辺の作業時間は確実に消滅してます。
人間が時間を使うのは、「何を作るか」を定義する時間(Step 1)と、「どう作るか」の規約を書く時間(Step 2) により集中できそうですね。





















