
こんにちは。株式会社アドグローブ ソリューション事業部の山川です。
今回はタイトルの通り、Claude CodeとFigma MCPを使ってFlutterアプリで色々と試してみましたので、その内容をブログでご紹介します。
はじめに
最近は生成AIのニュースが多く、実際に触ってみている方も多いのではないでしょうか。
私もその一人で、これだけ話題になっているので、やっと3ヶ月ぐらい前から少しずつ触り始めました。(遅い笑)
触ってみると生成AIが生成するコードに凄さを感じましたが、実際に自分の業務にどう活かしていくのかイメージできていませんでした。
しかし、2025年の6月にデザインツールのFigmaがMCPを提供しているということを知りました。
私は普段業務でFlutterアプリ開発を行っているのですが、デザインツールとしてFigmaがよく採用されていたのでこれは自分の業務にも今後使えるのでは?と思い今回調べてみました。
本記事の内容
FigmaとClaude CodeをMCPで連携させ、Flutterアプリ開発で使えそうなことを色々と試してみたので紹介します。
Figma MCPとは
Figmaを生成AIと連携し、生成AIがFigmaのデータを直接取得・操作できるようにする仕組みです。 これによって、Figmaのデザインからコードの生成やFigmaデザインとのデザインチェック、生成AIからのデザイン作成などが行えるようになります。
※Figma MCPを使うには有料プランのDevシート以上のプランが必要となります。
Figma MCPサーバーの紹介ブログ
Design Context, Everywhere You Build | Figma Blog
Figma プラン
プランと料金 | Figma
Figma MCPの設定
- Figmaデスクトップアプリの画面右側に表示される以下の部分で「Figma MCPを設定」をクリックする
※表示されていない場合はDev Modeになっていない可能性があるため、Macであれば「Shift + D」でDev Modeに変わります。
- Claude Code側で必要な作業が表示されるので1のコマンドを実行する

- 以下のコマンドで設定されているかを確認する
> claude mcp list
以下のようにConnectedが表示されていればOK

とりあえず画面を作ってみてもらう
ここまででFigma MCPとClaude Codeを繋げることはできているので、早速簡単な画面を作ってもらいましょう。
今回使用するデザインはFigma公式が提供しているテンプレートのMobile UI Kitです。

スマホアプリ開発で必要そうなコンポーネントが一通り揃っています。
ひとまずこの中で比較的画面がシンプルなログイン画面を作ってみてもらいます。

作業指示はFigmaのリンクを貼り付けて簡単に指示してみます。

実際に出来上がった画面はこちらです。

生成されたソースコードはこちらです。
import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @RoutePage() class LoginScreen extends HookConsumerWidget { const LoginScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final emailController = useTextEditingController(); final passwordController = useTextEditingController(); final isPasswordVisible = useState(false); final emailFocusNode = useFocusNode(); final passwordFocusNode = useFocusNode(); return Scaffold( backgroundColor: Colors.white, appBar: AppBar( backgroundColor: Colors.white, elevation: 0, systemOverlayStyle: SystemUiOverlayStyle.dark, toolbarHeight: 44, ), body: SafeArea( child: GestureDetector( onTap: () { emailFocusNode.unfocus(); passwordFocusNode.unfocus(); }, child: Column( children: [ Expanded( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 16), // Page Header const Text( 'Log In', style: TextStyle( fontFamily: 'Inter', fontSize: 30, fontWeight: FontWeight.w600, color: Colors.black, ), textAlign: TextAlign.center, ), const SizedBox(height: 32), // Email Input Container( height: 50, decoration: BoxDecoration( color: const Color(0xFFF6F6F6), borderRadius: BorderRadius.circular(12), ), child: TextField( controller: emailController, focusNode: emailFocusNode, keyboardType: TextInputType.emailAddress, style: const TextStyle( fontFamily: 'Inter', fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black, ), decoration: const InputDecoration( hintText: 'Email', hintStyle: TextStyle( fontFamily: 'Inter', fontSize: 16, fontWeight: FontWeight.w500, color: Color(0xFFBDBDBD), ), border: InputBorder.none, contentPadding: EdgeInsets.symmetric( horizontal: 16, vertical: 16, ), ), ), ), const SizedBox(height: 16), // Password Input Container( height: 50, decoration: BoxDecoration( color: const Color(0xFFF6F6F6), borderRadius: BorderRadius.circular(12), ), child: TextField( controller: passwordController, focusNode: passwordFocusNode, obscureText: !isPasswordVisible.value, style: const TextStyle( fontFamily: 'Inter', fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black, ), decoration: InputDecoration( hintText: 'Password', hintStyle: const TextStyle( fontFamily: 'Inter', fontSize: 16, fontWeight: FontWeight.w500, color: Color(0xFFBDBDBD), ), border: InputBorder.none, contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 16, ), suffixIcon: GestureDetector( onTap: () { isPasswordVisible.value = !isPasswordVisible.value; }, child: Container( padding: const EdgeInsets.only(right: 16), alignment: Alignment.center, width: 60, child: Text( 'Show', style: TextStyle( fontFamily: 'Inter', fontSize: 16, fontWeight: FontWeight.w500, color: const Color(0xFF5DB075), ), ), ), ), ), ), ), const SizedBox(height: 150), // Log In Button ElevatedButton( onPressed: () { // ログイン処理 }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF5DB075), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( horizontal: 32, vertical: 16, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(100), ), elevation: 0, minimumSize: const Size.fromHeight(50), ), child: const Text( 'Log In', style: TextStyle( fontFamily: 'Inter', fontSize: 16, fontWeight: FontWeight.w600, ), ), ), const SizedBox(height: 16), // Forgot Password TextButton( onPressed: () { // パスワードリセット処理 }, child: const Text( 'Forgot your password?', style: TextStyle( fontFamily: 'Inter', fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF5DB075), ), ), ), ], ), ), ), ], ), ), ), ); } }
みた感じほぼFigma通りのデザインが作成されていて、生成されたコードも特に問題はなさそうでした。 元々auto_routeやriverpodを導入しているアプリのプロジェクトで試していたので、その辺りも考慮されてコードが生成されています。
Figmaのデザインを更新して、差分を取り込ませてみる
次は生成したログイン画面のFigma側でデザインが更新されたケースを想定して差分だけ更新できるかを試してみます。
ログイン画面のデザインをバリデーションエラーありで更新します。

早速指示してみましょう。

その作業のログが以下の通りです。Figmaがどのように更新されたかを具体的に表示してどう修正するかをログで教えてくれます。

出来上がった画面がこちらです。

きちんと更新されていますね。
もちろんこれはデザインだけで、実際のバリデーション処理などは入っていないため、それは別途指示するかデザインを作ってもらう段階で指示しておく必要があります。
ですが、指示するだけでデザインからの自動生成、更新の場合は差分を取得して生成してくれるのは非常に助かりますね。
カラー定義やアセットも自動取得
デザインもですが、Figmaのデザイン上にあるカラー定義や画像を一括で自動取得することも可能です。 もちろん更新も検知してくれます。
4色(red, orange, yellow, green)のカラー定義をしてみたので、これを取り込ませてみましょう。

実際に指示してみた際のログは以下の通りです。

生成されたソースコードはこちらです。
import 'package:flutter/material.dart'; /// アプリケーション全体で使用するカラー定数 /// Figma Design System (Mobile UI kit) から取得 class AppColors { AppColors._(); // インスタンス化を防ぐ /// Colors/Red - #FF3B30 static const Color red = Color(0xFFFF3B30); /// Colors/Orange - #FF9500 static const Color orange = Color(0xFFFF9500); /// Colors/Yellow - #FFCC00 static const Color yellow = Color(0xFFFFCC00); /// Colors/Green - #34C759 static const Color green = Color(0xFF34C759); }
更新も試してみました。mintを追加しています。

実際に指示してみた際のログは以下の通りです。

更新も問題なくされて、何のカラーが追加されたかなどもわかりやすくログに表示してくれてますね。
次は画像の取得を行ってみましょう。Figmaに以下の4つの図形の画像を用意しました。

指示は以下の通りです。このアプリプロジェクトではflutter_genを導入しているので画像を追加後にbuild_runnerを実行してもらうようにしています。
![]()

こちらも問題なく取り込めていますね。
(赤い四角形は「Flutterのソースコードで簡単に描画できます」みたいな文言がログに出ていて取り込まれていませんでした)
Figmaデザインと実装された画面をチェックする
Figma MCPを使えば実装された画面とFigmaデザインとの差異をチェックしてFigmaのデザイン通りに実装されているかを確認することもできます。 こちらも試していきましょう。
ログイン画面に先ほど追加した円の画像を表示してみました。この状態でデザインチェックをかけてみます。

指示は以下の通りです。

その作業のログが以下の通りでFigmaとの差異と修正箇所を教えてくれます。
色のチェックやスペーシングのチェックもしてくれるので人の目でわかりにくいところもこれなら拾えるようになると思います。

もう少し複雑な画面でも試してみる
今はログイン画面という簡素な画面でしたので問題なかったですが、以下のようなカレンダーがある少し複雑な画面でも生成できるか試してみます。
今回はデザイン通り+αで画面の仕様も少し指示に含めてみました。

指示は以下の通りです。

実際に出来上がった画面はこちらです。

カレンダーに関しては問題ないですが、下のアイテムリストが表示されていないです。
恐らくこれはClaude Codeに指示する際に表示する条件・契機を指示していなかったからだと思われます。
なので追加で以下のように指示してみます。

修正後の画面はこちらです。


問題なく修正されて、表示されていますね。
生成されたソースコードはこちらです。
import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:table_calendar/table_calendar.dart'; /// カレンダーアイテムのデータモデル class CalendarItem { CalendarItem({ required this.id, required this.title, required this.duration, }); final String id; final String title; final String duration; } /// カレンダー画面 /// Figma Design: https://www.figma.com/design/rEIu3UlYW9DoBGrJIwJFpx/Mobile-UI-kit--Community-?node-id=179-750 @RoutePage() class CalendarScreen extends HookConsumerWidget { const CalendarScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final focusedDay = useState(DateTime.now()); final selectedDay = useState<DateTime?>(null); // 固定の5件のアイテムデータ final items = useState<List<CalendarItem>>([ CalendarItem(id: '1', title: 'Item', duration: '30min.'), CalendarItem(id: '2', title: 'Item', duration: '60min.'), CalendarItem(id: '3', title: 'Item', duration: '30min.'), CalendarItem(id: '4', title: 'Item', duration: '15min.'), CalendarItem(id: '5', title: 'Item', duration: '45min.'), ]); return Scaffold( backgroundColor: Colors.white, body: Column( children: [ // ヘッダー部分(緑色背景) _buildHeader(context, focusedDay.value), // カレンダー部分 Expanded( child: SingleChildScrollView( child: Column( children: [ _buildCalendar( focusedDay: focusedDay, selectedDay: selectedDay, ), // 日付が選択されている場合、アイテムリストを表示 if (selectedDay.value != null) _buildItemList( items: items, ), ], ), ), ), ], ), ); } /// ヘッダー部分(ステータスバー + 月名 + 曜日行)を構築 Widget _buildHeader(BuildContext context, DateTime focusedDay) { final monthNames = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ]; return Container( color: const Color(0xFF5DB075), child: SafeArea( bottom: false, child: Column( children: [ // 月名 Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: Text( monthNames[focusedDay.month - 1], style: const TextStyle( fontFamily: 'Inter', fontSize: 30, fontWeight: FontWeight.w600, color: Colors.white, ), ), ), // 曜日行 _buildDaysOfWeekHeader(), const SizedBox(height: 16), ], ), ), ); } /// 曜日ヘッダーを構築(土曜は青、日曜は赤) Widget _buildDaysOfWeekHeader() { final days = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']; final colors = [ Colors.white, // Mo Colors.white, // Tu Colors.white, // We Colors.white, // Th Colors.white, // Fr Colors.blue, // Sa - 土曜日は青色 Colors.red, // Su - 日曜日は赤色 ]; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: List.generate(7, (index) { return Expanded( child: Text( days[index], textAlign: TextAlign.center, style: TextStyle( fontFamily: 'Inter', fontSize: 14, fontWeight: FontWeight.w400, color: colors[index], ), ), ); }), ), ); } /// カレンダーを構築 Widget _buildCalendar({ required ValueNotifier<DateTime> focusedDay, required ValueNotifier<DateTime?> selectedDay, }) { final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); return TableCalendar( firstDay: DateTime.utc(2020, 1, 1), lastDay: DateTime.utc(2030, 12, 31), focusedDay: focusedDay.value, selectedDayPredicate: (day) { return selectedDay.value != null && isSameDay(selectedDay.value, day); }, onDaySelected: (selected, focused) { selectedDay.value = selected; focusedDay.value = focused; }, onPageChanged: (focused) { focusedDay.value = focused; }, // 6週間固定表示 calendarFormat: CalendarFormat.month, availableCalendarFormats: const {CalendarFormat.month: 'Month'}, sixWeekMonthsEnforced: true, // 月曜始まり startingDayOfWeek: StartingDayOfWeek.monday, // ヘッダーを非表示(カスタムヘッダーを使用) headerVisible: false, // 曜日行を非表示(カスタム曜日行を使用) daysOfWeekVisible: false, // 横スワイプで月を切り替え pageJumpingEnabled: true, // スタイル設定 calendarStyle: CalendarStyle( // 外部の日付(前月・翌月) outsideDaysVisible: true, outsideTextStyle: const TextStyle( fontFamily: 'Inter', fontSize: 14, fontWeight: FontWeight.w400, color: Color(0xFFBDBDBD), ), // デフォルトの日付スタイル defaultTextStyle: const TextStyle( fontFamily: 'Inter', fontSize: 14, fontWeight: FontWeight.w400, color: Colors.black, ), // 週末のスタイル(土日) weekendTextStyle: const TextStyle( fontFamily: 'Inter', fontSize: 14, fontWeight: FontWeight.w400, color: Colors.black, ), // 今日のスタイル todayDecoration: const BoxDecoration( color: Colors.transparent, ), todayTextStyle: const TextStyle( fontFamily: 'Inter', fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF5DB075), // 緑色 ), // 選択日のスタイル selectedDecoration: BoxDecoration( color: const Color(0xFF5DB075).withValues(alpha: 0.3), shape: BoxShape.circle, ), selectedTextStyle: const TextStyle( fontFamily: 'Inter', fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF5DB075), ), // セルのマージン cellMargin: const EdgeInsets.all(4), ), // 日付のカスタムビルダー calendarBuilders: CalendarBuilders( // デフォルトの日付(当月の平日・週末含む) defaultBuilder: (context, day, focusedDay) { return _buildDayCell(day, today); }, // 今日 todayBuilder: (context, day, focusedDay) { return _buildDayCell(day, today, isToday: true); }, // 外部の日付(前月・翌月) outsideBuilder: (context, day, focusedDay) { return Center( child: Text( '${day.day}', style: const TextStyle( fontFamily: 'Inter', fontSize: 14, fontWeight: FontWeight.w400, color: Color(0xFFBDBDBD), ), ), ); }, ), ); } /// 日付セルを構築 Widget _buildDayCell(DateTime day, DateTime today, {bool isToday = false}) { Color textColor; FontWeight fontWeight = FontWeight.w400; if (isToday) { // 今日は緑色(曜日より優先) textColor = const Color(0xFF5DB075); fontWeight = FontWeight.w600; } else if (day.weekday == DateTime.saturday) { // 土曜日は青色 textColor = Colors.blue; } else if (day.weekday == DateTime.sunday) { // 日曜日は赤色 textColor = Colors.red; } else { // 平日は黒色 textColor = Colors.black; } return Center( child: Text( '${day.day}', style: TextStyle( fontFamily: 'Inter', fontSize: 14, fontWeight: fontWeight, color: textColor, ), ), ); } /// アイテムリストを構築 Widget _buildItemList({ required ValueNotifier<List<CalendarItem>> items, }) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: items.value.map((item) { return _buildItemRow( item: item, onDelete: () { items.value = items.value.where((i) => i.id != item.id).toList(); }, ); }).toList(), ), ); } /// アイテム行を構築(スワイプで削除ボタン表示) Widget _buildItemRow({ required CalendarItem item, required VoidCallback onDelete, }) { return ClipRect( child: Dismissible( key: Key(item.id), direction: DismissDirection.endToStart, confirmDismiss: (direction) async { // 削除確認なしで削除ボタンを表示するだけ return false; }, background: Container( alignment: Alignment.centerRight, color: Colors.red, child: const Padding( padding: EdgeInsets.only(right: 16), child: Text( 'Delete', style: TextStyle( fontFamily: 'Inter', fontSize: 16, fontWeight: FontWeight.w500, color: Colors.white, ), ), ), ), child: _SwipeableItemRow( item: item, onDelete: onDelete, ), ), ); } } /// スワイプ可能なアイテム行ウィジェット class _SwipeableItemRow extends HookWidget { const _SwipeableItemRow({ required this.item, required this.onDelete, }); final CalendarItem item; final VoidCallback onDelete; @override Widget build(BuildContext context) { final dragOffset = useState(0.0); const deleteButtonWidth = 80.0; return GestureDetector( onHorizontalDragUpdate: (details) { dragOffset.value = (dragOffset.value + details.delta.dx) .clamp(-deleteButtonWidth, 0.0); }, onHorizontalDragEnd: (details) { // スワイプ量が半分を超えたらボタンを表示したままにする if (dragOffset.value < -deleteButtonWidth / 2) { dragOffset.value = -deleteButtonWidth; } else { dragOffset.value = 0.0; } }, child: Stack( children: [ // 削除ボタン(背景) Positioned.fill( child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ GestureDetector( onTap: onDelete, child: Container( width: deleteButtonWidth, color: Colors.red, alignment: Alignment.center, child: const Text( 'Delete', style: TextStyle( fontFamily: 'Inter', fontSize: 16, fontWeight: FontWeight.w500, color: Colors.white, ), ), ), ), ], ), ), // アイテムコンテンツ(スライド) AnimatedContainer( duration: const Duration(milliseconds: 100), transform: Matrix4.translationValues(dragOffset.value, 0, 0), child: Container( color: Colors.white, height: 35, child: Row( children: [ // 緑色の丸アイコン Container( width: 16, height: 16, decoration: const BoxDecoration( color: Color(0xFF5DB075), shape: BoxShape.circle, ), ), const SizedBox(width: 16), // アイテム名 Expanded( child: Text( item.title, style: const TextStyle( fontFamily: 'Inter', fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black, ), ), ), // 所要時間 Text( item.duration, style: const TextStyle( fontFamily: 'Inter', fontSize: 14, fontWeight: FontWeight.w400, color: Colors.black, ), ), ], ), ), ), // 区切り線 Positioned( left: 0, right: 0, bottom: 0, child: Container( height: 1, color: const Color(0xFFE8E8E8), ), ), ], ), ); } }
これで少し複雑な画面でもある程度思った通りに画面を生成できていると思います。
まとめ
Claude CodeとFigma MCPを使って、デザインからコード生成・更新チェック・カラー定義および画像の取得・デザインチェックとFlutterアプリで必要そうなことを一通り試してみました。
正直使ってみる前は生成されたコードに結構修正とかいるのかと思いましたが、思った以上に精度が高く、コードの修正もほぼいらないものが出来上がったので驚きました。
これらを活用することで以下のことが期待できると思います。
- 画面開発の工数削減
- カラー定義、画像取り込みなどの手間の削減
- 更新箇所の確認および取り込み(デザイナーに確認してもらわなくても分かる)
- デザインチェックの自動化(カラーやスペーシングなど)
もちろんデザインに関する全てに対応できているわけではなく、アニメーションなどのFigma上に存在しないものは開発者が実装する必要がまだありますし、生成されたコードの妥当性などは開発者がチェックする必要もあります。
なので、生成AIに頼るだけではなく開発者自身が上手く生成AIを使って工数削減や作業および品質の向上に繋げられたらと思います。
そしてこの記事が、生成AIを業務や自身のアプリ開発に導入するきっかけになれたら嬉しいです。
最後までご覧いただきありがとうございました。
アドグローブでは、さまざまなポジションで一緒に働く仲間を募集しています!
詳細については下記からご確認ください。みなさまからのご応募お待ちしております。