はじめに
「UIのラフなイメージはあるけど、コードに起こすのが面倒…」
そんなとき、手書きのメモ画像をローカルパスごとClaude Codeに渡すだけでFlutterのUIコードが生成できます。
というような記事はすでにたくさんあるかもしれないですが、自分の目で確かめてみたく、兎にも角にもやってみました。
claude.aiのチャットではなく、Claude MCP(Dart & Flutter MCP Server)経由のClaude Codeを使っているので、プロジェクトのファイル構造を把握した状態で画像を読み取り、そのままコードまで書いてくれます。
使ったもの
- Claude Code(ターミナル)
- Dart & Flutter MCP Server(
dart mcp-server) - 手書きのラフスケッチ(スマホで撮影してローカルに保存)
- Flutter 3.x / Dart 3.x
Dart & Flutter MCP Serverのセットアップ手順は下記を参照してください。
やったこと
① 手書きでラフを描く
こんな感じのメモです。丁寧に描く必要はありません。
構成としては:
- AppBar(ハンバーガーメニュー・ロゴ・通知ベル)
- カルーセル画像エリア
- ページインジケーター(ドット)
- お知らせセクション(リスト形式)
② スマホで撮影してローカルに保存
撮影した画像をMacに転送して、Flutterプロジェクトのルートに置きました。
your_flutter_project/
├── lib/
├── assets/
│ └── wireframe.jpeg ← ここに置く
└── ...
③ Flutterプロジェクトで Claude Code を起動
cd your_flutter_project
claude
④ プロンプトは1行だけ
/path/to/wireframe.jpegを参考にUIを構築してください
⑤ Claude Codeの動作ログ
⏺ 画像を読み取ります。
Read 1 file (wireframe.jpeg)
⏺ 手書きスケッチから以下の構成を読み取りました。
- AppBar: ハンバーガー(左)、ロゴ(中央)、通知ベル(右)
- カルーセル画像エリア(PageView)
- ページインジケーター(ドット7個)
- お知らせセクション(ヘッダー+リスト3件)
lib/main.dart を生成します。
⏺ Write(lib/main.dart)
⎿ Wrote 178 lines
画像を読み取って要素を列挙し、そのままファイルを書いてくれました。
⑥ 生成されたコード
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Wireframe UI',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final PageController _pageController = PageController();
int _currentPage = 0;
final List<Color> _carouselColors = [
Colors.blue.shade200,
Colors.green.shade200,
Colors.orange.shade200,
Colors.purple.shade200,
Colors.red.shade200,
Colors.teal.shade200,
Colors.pink.shade200,
];
final List<Map<String, String>> _notices = [
{'title': 'hogehogehoge...'},
{'title': 'hogehogehoge'},
{'title': 'hogehogehoge'},
];
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
drawer: const _AppDrawer(),
appBar: AppBar(
centerTitle: true,
title: const Text(
'ロゴ',
style: TextStyle(fontWeight: FontWeight.bold),
),
actions: [
IconButton(
icon: const Icon(Icons.notifications_outlined),
onPressed: () {},
),
],
),
body: ListView(
children: [
_CarouselSection(
pageController: _pageController,
colors: _carouselColors,
currentPage: _currentPage,
onPageChanged: (index) => setState(() => _currentPage = index),
),
const Divider(height: 1),
_NoticeSection(notices: _notices),
],
),
);
}
}
class _AppDrawer extends StatelessWidget {
const _AppDrawer();
@override
Widget build(BuildContext context) {
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
DrawerHeader(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
),
child: const Text(
'メニュー',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
ListTile(
leading: const Icon(Icons.home),
title: const Text('ホーム'),
onTap: () => Navigator.pop(context),
),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('設定'),
onTap: () => Navigator.pop(context),
),
],
),
);
}
}
class _CarouselSection extends StatelessWidget {
const _CarouselSection({
required this.pageController,
required this.colors,
required this.currentPage,
required this.onPageChanged,
});
final PageController pageController;
final List<Color> colors;
final int currentPage;
final ValueChanged<int> onPageChanged;
@override
Widget build(BuildContext context) {
return Column(
children: [
SizedBox(
height: 200,
child: PageView.builder(
controller: pageController,
itemCount: colors.length,
onPageChanged: onPageChanged,
itemBuilder: (context, index) {
return Container(
color: colors[index],
child: Center(
child: Text(
'カルーセル画像 ${index + 1}',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
);
},
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(colors.length, (index) {
final isActive = index == currentPage;
return AnimatedContainer(
duration: const Duration(milliseconds: 250),
margin: const EdgeInsets.symmetric(horizontal: 4),
width: isActive ? 20 : 8,
height: 8,
decoration: BoxDecoration(
color: isActive
? Theme.of(context).colorScheme.primary
: Colors.grey.shade300,
borderRadius: BorderRadius.circular(4),
),
);
}),
),
),
],
);
}
}
class _NoticeSection extends StatelessWidget {
const _NoticeSection({required this.notices});
final List<Map<String, String>> notices;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Text(
'お知らせ',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
const Divider(height: 1),
...notices.map((notice) {
return Column(
children: [
ListTile(
title: Text(notice['title']!),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
),
const Divider(height: 1),
],
);
}),
],
);
}
}
手書きメモとの対応
Claude MCPが手書きスケッチを読み取って、以下の対応でWidgetを選んでくれました。
| 手書きの要素 | 生成されたWidget |
|---|---|
| ≡(ハンバーガー) |
Scaffold の drawer(自動でアイコン表示) |
| ロゴ・ベルアイコン |
AppBar の title と actions
|
| カルーセル画像枠 | PageView.builder |
| ドットのインジケーター |
AnimatedContainer を Row で並べる |
| お知らせ(ヘッダー) |
Text + Divider
|
| hogehogehoge › |
ListTile + Icon(Icons.chevron_right)
|
やってみて感じたこと
- 手書きが雑でも要素を読み取ってくれる。文字が読めれば十分
- Widgetの選定・ファイルへの書き込みまで全部やってくれるので、実装方針を考える時間がゼロになる
- ダミーデータで形を作ってくれるので、後から実データに差し替えるだけ
- プロジェクト構造をMCPが把握しているので、既存コードと整合性が取れた状態で生成される
成果物
まとめ
- 手書きでラフを描く(雑でOK)
- スマホで撮影してローカルに保存
- Flutterプロジェクトで
claudeを起動 -
[画像パス]を参考にUIを構築してくださいの1行を送る - コードがプロジェクトに直接書き込まれる
プロンプト1行で、画像の読み取りからファイルへの書き込みまで全部やってくれます。 チャットにコードをコピペする手間すらありません。ぜひ試してみてください!

