Back to All Articles
flutteraigeminimobilearchitecture

Building Generative UI in Flutter with Gemini and the A2UI Protocol

Learn how to render dynamic, AI-driven Flutter widgets on the fly using the GenUI SDK, Gemini, and structured A2UI protocol payloads.

2026-10-13
Building Generative UI in Flutter with Gemini and the A2UI Protocol

Let's look at how to move past rigid text-only chatbots by using the GenUI SDK for Flutter to render dynamic, interactive widgets straight from LLM responses.

What you'll learn:

  • How the GenUI SDK orchestrates LLM prompts and widget catalogs.
  • Setting up a basic Flutter client with the genui package.
  • Deserializing server-side A2UI protocol payloads into real widgets.
  • Handling user interactions that trigger follow-up agent requests.

Setting Up the GenUI SDK in Your Flutter Project

Traditional apps are static because every screen is hardcoded into the binary. When building agentic workflows, you want the backend or the LLM to decide which UI components best answer a user's intent—whether that's a date picker, a card list, or a custom slider.

To get started, add the core package to your pubspec.yaml and initialize your content generator adapter. For local prototyping, the Gemini API adapter gives you rapid feedback loops.

TSX
import 'package:flutter/material.dart';
import 'package:genui/genui.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // Initialize the GenUI runtime with your content generator
  final generativeClient = GenUIClient(
    apiKey: String.fromEnvironment('GEMINI_API_KEY'),
    catalog: DefaultWidgetCatalog.instance,
  );

  runApp(GenUIRootApp(client: generativeClient));
}

class GenUIRootApp extends StatelessWidget {
  final GenUIClient client;

  const GenUIRootApp({super.key, required this.client});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: GenUIScreen(client: client),
    );
  }
}

Speed Note: When shipping production apps, prefer the Firebase AI logic adapter over direct client-side API keys to securely manage your credentials without maintaining a separate proxy server.

Defining Your Custom Widget Catalog

The core safety feature of generative UI is constraint. You don't want an LLM inventing random layout code that breaks your app; you want it to pick from a curated list of brand-compliant components you've already built and tested.

You register these components in a WidgetCatalog so the orchestration layer knows how to map incoming protocol schemas to actual Flutter code.

TSX
import 'package:flutter/material.dart';
import 'package:genui/genui.dart';

final customCatalog = WidgetCatalog(
  components: {
    'trip_summary_card': ComponentDefinition(
      builder: (context, data) {
        return Card(
          elevation: 2,
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(data['destination'] ?? 'Unknown Destination', 
                  style: Theme.of(context).textTheme.titleLarge),
                const SizedBox(height: 8),
                Text('Duration: ${data['duration']} days'),
              ],
            ),
          ),
        );
      },
    ),
  },
);

Gotcha: Always validate incoming JSON properties against null values inside your builders. LLMs can occasionally omit optional fields, which will throw runtime exceptions if you use raw index access.

Orchestrating the Prompt-to-UI Loop

When a user submits a prompt like "Help me plan a trip to Tokyo," your app sends that text alongside your registered widget catalog definition to the LLM.

The model responds with a dual payload: conversational text for context, and structured metadata describing which UI components to render and populate.

TSX
import 'package:flutter/material.dart';
import 'package:genui/genui.dart';

class GenUIScreen extends StatefulWidget {
  final GenUIClient client;

  const GenUIScreen({super.key, required this.client});

  @override
  State<GenUIScreen> createState() =>._GenUIScreenState();
}

class _GenUIScreenState extends State<GenUIScreen> {
  final TextEditingController _controller = TextEditingController();
  List<Widget> _dynamicUiStream = [];

  void _handleSubmit() async {
    final prompt = _controller.text;
    if (prompt.isEmpty) return;

    // Request stream containing both data and UI component configurations
    final responseStream = widget.client.sendPrompt(prompt);

    responseStream.listen((uiComponent) {
      setState(() {
        _dynamicUiStream.add(uiComponent);
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Generative UI Planner')),
      body: Column(
        children: [
          Expanded(
            child: ListView(
              padding: const EdgeInsets.all(16),
              children: _dynamicUiStream,
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Row(
              children: [
                Expanded(child: TextField(controller: _controller)),
                IconButton(
                  icon: const Icon(Icons.send),
                  onPressed: _handleSubmit,
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Handling User Actions Inside Generated Layouts

Generative UIs shouldn't just be read-only billboards. When a user interacts with a dynamically generated button, slider, or form field, that interaction should flow right back into the agent context to fetch the next set of UI blocks.

TSX
import 'package:flutter/material.dart';

class InteractiveBudgetWidget extends StatelessWidget {
  final Map<String, dynamic> data;
  final Function(String actionId, Map<String, dynamic> payload) onActionTriggered;

  const InteractiveBudgetWidget({
    super.key, 
    required this.data, 
    required this.onActionTriggered,
  });

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () {
        // Send user selection back to the agent loop
        onActionTriggered('select_budget_tier', {
          'tier': data['recommendedTier'],
        });
      },
      child: Text('Confirm Budget: \$${data['amount']}'),
    );
  }
}

Accessibility Note: Ensure any dynamic layout components returned by your catalog pass basic screen reader semantics. Pass semantic labels down through your component data definitions so generated accessibility trees don't collapse.

Summary

Building with the GenUI SDK lets you shift from maintaining rigid, static screen definitions to orchestrating fluid, context-aware user interfaces driven by LLMs. By combining structured widget catalogs with the A2UI protocol, you keep full control over your brand guidelines while unlocking deeply personalized application flows.

Next Steps:

  • Pull down the sample apps from the official flutter/genui repository to inspect a working client-server architecture.
  • Define your first custom component catalog tailored to your app's core design system.
  • Experiment with the Firebase AI logic adapter for secure, client-side production deployments.