Flutter genuiを使ってインタラクティブなUI生成を実現する
株式会社GENDA モバイル開発部の名取です。
この記事は GENDA Advent Calendar 2025 シリーズ1 Day 4 の記事です。
この記事の要約
- Flutter公式が開発を進めているgenuiについての紹介記事です
- genuiの使い方をサンプルコードと共に紹介します
flutter_genui
genuiはJSONやDSLで記述したUI構造定義ファイルを元に、Flutterのウィジェットコード(Dartコード)を自動生成するためのライブラリです。
Status: Highly Experimental となっておりgenuiはまだ実験段階であることに注意が必要です。
Our goal for the GenUI SDK for Flutter is to help you replace static "walls of text" from your LLM with dynamic, interactive, graphical UI
LLMとの会話を動的でインタラクティブなグラフィカルUIに置き換え、よりリッチなユーザー体験を実現することがこのパッケージの目的と言えます。
サンプルを動かしてみる
Gitリポジトリには複数のサンプルがあります。試しにtravel_appというサンプルを動かしてみると下記のような画面が表示されます。
![]() |
![]() |
![]() |
ChatGPTをはじめとする多くの会話型AIサービスはテキストでのやり取りがベースとなりますが、このサンプルは非常にグラフィカルなUIであることがわかります。
実装方法
サンプルコードがどのように実装されているか見ていきましょう。
Firebaseの初期化
現状はFirebase AIもしくはGoogle Generative AIの使用が可能となっているようです。
/// Enum for selecting which AI backend to use.
enum AiBackend {
/// Use Firebase AI
firebase,
/// Use Google Generative AI
googleGenerativeAi,
}
Firebase AIを使用する場合のみ初期化処理を行います。
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Only initialize Firebase if we are using the Firebase backend.
if (aiBackend == AiBackend.firebase) {
await Firebase.initializeApp(
// UNCOMMENT_FOR_FIREBASE (See top of file for details)
// options: DefaultFirebaseOptions.currentPlatform,
);
}
// ...
runApp(const TravelApp());
}
カタログの定義
カタログとはLLMが使用できるウィジェットの一覧です。LLMはこのウィジェットの一覧をもとにJSONを生成します。
あらかじめ CoreCatalogItems が用意されているのでこれをそのまま使うこともできますし、サンプルのtravel_appのように自分でカスタマイズすることもできます。
当然カスタマイズした方がLLMによって出力されるウィジェットを絞れるので精度向上やドメインに特化した出力が期待できます。
コアカタログをそのまま使う場合
final genUiManager = GenUiManager(
catalog: CoreCatalogItems.asCatalog(), // そのまま使う
);
コアカタログをカスタマイズする場合
final Catalog travelAppCatalog = CoreCatalogItems.asCatalog()
.copyWithout([ // 使わないものを除外
CoreCatalogItems.audioPlayer,
CoreCatalogItems.card,
// ...
])
.copyWith([ // 使うものを追加
travelCarousel,
itinerary,
// ...
]);
CatalogItemの実装
カタログをカスタマイズしたい場合は、CatalogItemを作成してCoreCatalogItemsにcopyWithで追加します。
具体的な実装例は下記になります。
final button = CatalogItem(
// ① 名前: AIがJSONで指定する名前
name: 'Button',
// ② スキーマ: どんなパラメータを受け取るかの定義
dataSchema: _schema,
// ③ ウィジェットビルダー: JSONからFlutter Widgetを生成する関数
widgetBuilder: (itemContext) {
// JSONデータを取得
final buttonData = _ButtonData.fromMap(itemContext.data as JsonMap);
// Flutter Widgetを返す
return ElevatedButton(
onPressed: () {
// ユーザーアクションをAIに送信
itemContext.dispatchEvent(UserActionEvent(...));
},
child: itemContext.buildChild(buttonData.child),
);
},
// ④ サンプルデータ: デバッグ用のJSON例
exampleData: [() => '{ ... }'],
);
ユーザーの指示からWidgetが生成されるまでの流れ
- ユーザー: 「ボタンを表示して」
- LLM: カタログを確認「Button というCatalogItemがある。スキーマを見るとchild と action が必要」
- AI: JSONを生成
{
"Button": {
"child": "text_1",
"action": {"name": "clicked"}
}
}
- GenUI: CatalogItem の widgetBuilder を呼び出し
- Flutter: ElevatedButton がレンダリングされる
GenUiConversationのセットアップ
void initState() {
super.initState();
// 1. GenUiManagerを作成(カタログを渡す)
final genUiManager = GenUiManager(
catalog: travelAppCatalog,
configuration: const GenUiConfiguration(
actions: ActionsConfig(
allowCreate: true,
allowUpdate: true,
allowDelete: true,
),
),
);
// 2. ContentGenerator(AIバックエンド)を作成
final contentGenerator = switch (aiBackend) {
AiBackend.googleGenerativeAi => GoogleGenerativeAiContentGenerator(
catalog: travelAppCatalog,
systemInstruction: prompt,
apiKey: getApiKey(),
),
AiBackend.firebase => FirebaseAiContentGenerator(
catalog: travelAppCatalog,
systemInstruction: prompt,
),
};
// 3. GenUiConversation(ファサード)を作成
_uiConversation = GenUiConversation(
genUiManager: genUiManager,
contentGenerator: contentGenerator,
onSurfaceAdded: (update) => _scrollToBottom(),
onSurfaceUpdated: (update) => _scrollToBottom(),
onTextResponse: (text) => _scrollToBottom(),
);
}
UIの構築
ValueListenableBuilderでメッセージが追加される度にUIを更新しています。
// ...
Widget build(BuildContext context) {
super.build(context);
// ...
child: ValueListenableBuilder<List<ChatMessage>>(
valueListenable: _uiConversation.conversation,
builder: (context, messages, child) {
return Conversation(
messages: messages,
manager: _uiConversation.genUiManager,
scrollController: _scrollController,
);
},
),
// ...
ChatMessageには下記のようにさまざまなものがありますが特に重要なのはAiUiMessageです。
- UserMessage: ユーザーが入力したテキスト
- AiTextMessage: AIが返したテキスト
- AiUiMessage: AIが生成したUI
内部的には下記のようになっておりGenUiSuerfaceが実装されています。
case AiUiMessage():
return Padding(
padding: const EdgeInsets.all(16.0),
child: GenUiSurface(
key: message.uiKey,
host: manager,
surfaceId: message.surfaceId,
),
);
GenUiSurfaceの内部の構造は下記のようになっていました。
- GenUiSurface → genUiManager.renderSurface(surfaceId)を呼ぶ
- UiDefinitionをCatalogItem.widgetBuilderで再帰的にWidget化
- レイアウトが組み上がった状態のFlutter Widgetが返る
システムプロンプトの設定
最後にそのアプリに適したプロンプトを設定して完了です。
サンプルアプリのプロンプトは下記のようにセクション分けがされているようでした。
| セクション | 内容 |
|---|---|
# Instructions |
AIの役割定義(旅行代理店アシスタント) |
## Conversation flow |
会話の4段階フロー(インスピレーション→目的地→旅程→予約) |
### Side journeys |
メインフローから外れた場合の処理 |
## Controlling the UI |
surfaceUpdateとbeginRenderingツールの使い方 |
## UI style |
UI表示のルール(カルーセルは4項目以上など) |
## Images |
使用可能な画像リスト(${_imagesJson}で動的挿入) |
## Example |
JSONの具体例 |
GenUIではUIをLLMが制御するため、プロンプト設計がUI品質に大きく影響します。
travel_appのように、会話フロー・UIルール・画像リストなどを細かく与えることで、より安定した UI生成が期待できます。
これは普段私たちが使うClaude CodeやCodexでも同じことが言えるかと思います。
まとめ
まだ試験的なフェーズなので実プロダクトへの導入は難しい印象ですが、この1-2ヶ月でgenuiのリポジトリが大きく進捗したと思います。少し前はここまでREADMEも充実しておらずサンプルも今ほど綺麗には動きませんでした。
実用できる日も近そうです。



Discussion