🤩

Flutter genuiを使ってインタラクティブなUI生成を実現する

に公開

株式会社GENDA モバイル開発部の名取です。
この記事は GENDA Advent Calendar 2025 シリーズ1 Day 4 の記事です。

https://qiita.com/advent-calendar/2025/genda

この記事の要約

  • 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の使用が可能となっているようです。

configuration.dart
/// Enum for selecting which AI backend to use.
enum AiBackend {
  /// Use Firebase AI
  firebase,

  /// Use Google Generative AI
  googleGenerativeAi,
}

Firebase AIを使用する場合のみ初期化処理を行います。

main.dart
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が生成されるまでの流れ

  1. ユーザー: 「ボタンを表示して」
  2. LLM: カタログを確認「Button というCatalogItemがある。スキーマを見るとchild と action が必要」
  3. AI: JSONを生成
   {
     "Button": {
       "child": "text_1",
       "action": {"name": "clicked"}
     }
   }
  1. GenUI: CatalogItem の widgetBuilder を呼び出し
  2. Flutter: ElevatedButton がレンダリングされる

GenUiConversationのセットアップ

ravel_planner_page.dart

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を更新しています。

travel_planner_page.dart
// ...

  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が実装されています。

conversation.dart
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 surfaceUpdatebeginRenderingツールの使い方
## UI style UI表示のルール(カルーセルは4項目以上など)
## Images 使用可能な画像リスト(${_imagesJson}で動的挿入)
## Example JSONの具体例

GenUIではUIをLLMが制御するため、プロンプト設計がUI品質に大きく影響します。
travel_appのように、会話フロー・UIルール・画像リストなどを細かく与えることで、より安定した UI生成が期待できます。
これは普段私たちが使うClaude CodeやCodexでも同じことが言えるかと思います。

まとめ

まだ試験的なフェーズなので実プロダクトへの導入は難しい印象ですが、この1-2ヶ月でgenuiのリポジトリが大きく進捗したと思います。少し前はここまでREADMEも充実しておらずサンプルも今ほど綺麗には動きませんでした。
実用できる日も近そうです。

Discussion