Saltar al contenido principal

Primeros pasos con el GenUI SDK para Flutter

Aprende cómo usar GenUI SDK para Flutter y agregarlo a tu aplicación Flutter existente.

Esta guía explica cómo dar los primeros pasos con el GenUI SDK para Flutter y su serie de paquetes. Los componentes clave del SDK se describen en la página de componentes principales.

Usa las siguientes instrucciones para agregar genui a tu aplicación Flutter. Los ejemplos de código muestran cómo realizar las instrucciones en una aplicación completamente nueva creada al ejecutar flutter create, pero puedes seguir los mismos pasos para tu aplicación Flutter existente.

Configurar tu proveedor de agentes

#

El paquete genui puede conectarse a una variedad de proveedores de agentes. Los proveedores disponibles incluyen los siguientes:

Firebase AI Logic

Útil para aplicaciones de producción donde las interacciones con el LLM se realizan por completo en tu cliente de Flutter, sin necesidad de un servidor. Firebase también facilita la entrega de tus funciones de IA de forma segura, ya que Firebase se encarga de la gestión de tu clave de API de Gemini.

GenUI A2UI

Útil para arquitecturas cliente/servidor donde tu agente se está ejecutando en el servidor.

Construir el tuyo propio

También puedes construir tu propio adaptador para conectarte a tu proveedor de LLM preferido. Espera más de nosotros y de la comunidad pronto.

Para conectarte a Gemini usando el Vertex AI para Firebase SDK, sigue estas instrucciones:

  1. Crea un nuevo proyecto de Firebase usando la consola de Firebase.

  2. Habilita la Gemini API para ese proyecto.

  3. Sigue los primeros tres pasos en la guía de configuración de Firebase para Flutter para agregar Firebase a tu aplicación.

  4. Utiliza dart pub add para agregar genui y firebase_ai como dependencias en tu archivo pubspec.yaml:

    dart pub add genui firebase_ai
    
  5. En el método main de tu aplicación, asegúrate de que los bindings de widgets estén inicializados y luego inicializa Firebase:

    dart
    import 'package:flutter/material.dart';
    import 'package:firebase_core/firebase_core.dart';
    import 'firebase_options.dart';
    
    void main() async {
      WidgetsFlutterBinding.ensureInitialized();
      await Firebase.initializeApp(
        options: DefaultFirebaseOptions.currentPlatform,
      );
      runApp(const MyApp());
    }
    
  6. Crea una instancia del modelo generativo Vertex AI for Firebase y envuélvela con tu SurfaceController y A2uiTransportAdapter:

    dart
    import 'package:genui/genui.dart';
    import 'package:firebase_ai/firebase_ai.dart';
    
    final catalog = Catalog([
      // ...
    ]);
    final catalogs = [catalog];
    
    final surfaceController = SurfaceController(catalogs: catalogs);
    
    final promptBuilder = PromptBuilder.chat(
      catalog: catalog,
      systemPromptFragments: ['You are a helpful assistant.'],
    );
    
    final model = FirebaseAI.vertexAI().generativeModel(
      model: 'gemini-3.5-flash',
      systemInstruction: Content.system(promptBuilder.systemPromptJoined()),
    );
    
    // The Conversation wires transport -> controller internally.
    late final A2uiTransportAdapter transportAdapter;
    transportAdapter = A2uiTransportAdapter(onSend: (message) async {
      // final stream = model.generateContentStream(...);
      // await for (final chunk in stream) {
      //   transportAdapter.addChunk(chunk.text ?? '');
      // }
    });
    
    final conversation = Conversation(
      controller: surfaceController,
      transport: transportAdapter,
    );
    

Un paquete de integración para genui y el A2UI Streaming UI Protocol. Este paquete permite a las aplicaciones de Flutter conectarse a un servidor de Agente a Agente (A2UI) y renderizar interfaces de usuario dinámicas generadas por un agente de IA utilizando el framework genui.

Los componentes principales de este paquete incluyen:

  • A2uiAgentConnector: Maneja la comunicación de web sockets de bajo nivel con el servidor A2A, incluyendo el envío de mensajes y el análisis de eventos de flujo.
  • AgentCard: Una clase de datos que contiene metadatos sobre el agente de IA conectado.

Sigue estas instrucciones:

  1. Configurar dependencias: Utiliza dart pub add para agregar genui y genui_a2a como dependencias en tu archivo pubspec.yaml.

    dart pub add genui genui_a2a
    
  2. Inicializar SurfaceController: Configura SurfaceController con tus Catalogs de widgets.

  3. Crear A2uiTransportAdapter: Instancia A2uiTransportAdapter para analizar los mensajes.

  4. Crear A2uiAgentConnector: Instancia A2uiAgentConnector, proporcionando la URI del servidor A2A.

  5. Crear Conversation: Pasa el adaptador y el controlador a Conversation.

  6. Renderizar con Surface: Utiliza widgets Surface en tu interfaz de usuario para mostrar el contenido generado por el agente.

  7. Enviar mensajes: Utiliza connector.connectAndSend o Conversation.sendMessage para enviar las entradas del usuario al contenido generado por el agente.

    dart
    import 'package:flutter/material.dart';
    import 'package:genui/genui.dart';
    import 'package:genui_a2a/genui_a2a.dart';
    import 'package:logging/logging.dart';
    
    void main() {
      // Setup logging.
      Logger.root.level = Level.ALL;
      Logger.root.onRecord.listen((record) {
        print('${record.level.name}: ${record.time}: ${record.message}');
        if (record.error != null) {
          print(record.error);
        }
        if (record.stackTrace != null) {
          print(record.stackTrace);
        }
      });
    
      runApp(const GenUIExampleApp());
    }
    
    class GenUIExampleApp extends StatelessWidget {
      const GenUIExampleApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'A2UI Example',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: const ChatScreen(),
        );
      }
    }
    
    class ChatScreen extends StatefulWidget {
      const ChatScreen({super.key});
    
      @override
      State<ChatScreen> createState() => _ChatScreenState();
    }
    
    class _ChatScreenState extends State<ChatScreen> {
      final TextEditingController _textController = TextEditingController();
      final SurfaceController _surfaceController =
          SurfaceController(catalogs: [BasicCatalogItems.asCatalog()]);
      late final A2uiTransportAdapter _transportAdapter;
      late final Conversation _uiAgent;
      late final A2uiAgentConnector _connector;
      final List<ChatMessage> _messages = [];
    
      @override
      void initState() {
        super.initState();
    
        // The Conversation wires transport -> controller internally.
        _transportAdapter = A2uiTransportAdapter(onSend: (message) async {
          // Implement sending to LLM if needed, or handled by connector
        });
    
        _connector = A2uiAgentConnector(
          // TODO: Replace with your A2A server URL.
          url: Uri.parse('http://localhost:8080'),
        );
        _uiAgent = Conversation(
          controller: _surfaceController,
          transport: _transportAdapter,
        );
    
        // Listen for messages from the remote agent.
        _connector.stream.listen(_surfaceController.handleMessage);
    
      }
    
      @override
      void dispose() {
        _textController.dispose();
        _uiAgent.dispose();
        _transportAdapter.dispose();
        _surfaceController.dispose();
        _connector.dispose();
        super.dispose();
      }
    
      void _handleSubmitted(String text) async {
        if (text.isEmpty) return;
        _textController.clear();
        final message = ChatMessage.user(text);
        setState(() {
          _messages.insert(0, message);
        });
    
        final responseText = await _connector.connectAndSend(message);
    
        // Handling response depends on your app's logic
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('A2UI Example'),
          ),
          body: Column(
            children: <Widget>[
              Expanded(
                child: ListView.builder(
                  padding: const EdgeInsets.all(8.0),
                  reverse: true,
                  itemBuilder: (_, int index) =>
                      _buildMessage(_messages[index]),
                  itemCount: _messages.length,
                ),
              ),
              const Divider(height: 1.0),
              Container(
                decoration: BoxDecoration(color: Theme.of(context).cardColor),
                child: _buildTextComposer(),
              ),
              // Surface for the main AI-generated UI:
              SizedBox(
                height: 300,
                child: Surface(
                  surfaceContext: _surfaceController.contextFor('main_surface'),
                ),
              ),
            ],
          ),
        );
      }
    
      Widget _buildMessage(ChatMessage message) {
        return Container(
          margin: const EdgeInsets.symmetric(vertical: 10.0),
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Container(
                margin: const EdgeInsets.only(right: 16.0),
                child: CircleAvatar(child: Text(message.role == ChatMessageRole.user ? 'U' : 'A')),
              ),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Text(message.role == ChatMessageRole.user ? 'User' : 'Agent',
                        style: const TextStyle(fontWeight: FontWeight.bold)),
                    Container(
                      margin: const EdgeInsets.only(top: 5.0),
                      child: Text(message.parts.whereType<TextPart>().map((e) => e.text).join('\n')),
                    ),
                  ],
                ),
              ),
            ],
          ),
        );
      }
    
      Widget _buildTextComposer() {
        return IconTheme(
          data: IconThemeData(color: Theme.of(context).colorScheme.secondary),
          child: Container(
            margin: const EdgeInsets.symmetric(horizontal: 8.0),
            child: Row(
              children: <Widget>[
                Flexible(
                  child: TextField(
                    controller: _textController,
                    onSubmitted: _handleSubmitted,
                    decoration:
                        const InputDecoration.collapsed(hintText: 'Send a message'),
                  ),
                ),
                Container(
                  margin: const EdgeInsets.symmetric(horizontal: 4.0),
                  child: IconButton(
                    icon: const Icon(Icons.send),
                    onPressed: () => _handleSubmitted(_textController.text),
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }
    

El directorio example en pub.dev contiene una aplicación completa que demuestra cómo utilizar este paquete.

Para usar genui con otro proveedor de agentes, sigue la documentación del SDK de ese proveedor para implementar una conexión y transmitir sus resultados a un A2uiTransportAdapter.

Crear la conexión con un agente

#

Si compilas tu proyecto Flutter para iOS o macOS, agrega esta clave a tu(s) archivo(s) {ios,macos}/Runner/*.entitlements para habilitar las solicitudes de red salientes:

xml
<dict>
...
<key>com.apple.security.network.client</key>
<true/>
</dict>

A continuación, utiliza las siguientes instrucciones para conectar tu aplicación al proveedor de agentes elegido.

  1. Crea un SurfaceController y proporciónale los catálogos de widgets que deseas poner a disposición del agente. Crea un A2uiTransportAdapter para analizar los mensajes y conéctalo.

  2. Crea un PromptBuilder y proporciónale una instrucción del sistema y las herramientas (funciones que deseas que el agente pueda invocar). Siempre debes incluir las herramientas proporcionadas por SurfaceController, pero no dudes en incluir otras. Agrega esto al prompt del sistema de tu LLM.

  3. Crea una clase Conversation utilizando las instancias de SurfaceController y A2uiTransportAdapter. Tu aplicación interactuará principalmente con este objeto para realizar las tareas.

    Por ejemplo:

    dart
    class _MyHomePageState extends State<MyHomePage> {
      late final SurfaceController _surfaceController;
      late final A2uiTransportAdapter _transportAdapter;
      late final Conversation _conversation;
    
      @override
      void initState() {
        super.initState();
    
        // Create a SurfaceController with a widget catalog.
        // The BasicCatalogItems contain basic widgets for text, markdown, and images.
        _surfaceController = SurfaceController(catalogs: [BasicCatalogItems.asCatalog()]);
    
        // The Conversation wires transport -> controller internally.
        _transportAdapter = A2uiTransportAdapter(onSend: (message) async {
          // Implement sending to LLM and pipe chunks back.
        });
    
        final catalog = BasicCatalogItems.asCatalog();
        final promptBuilder = PromptBuilder.chat(
          catalog: catalog,
          systemPromptFragments: [
            '''
            You are an expert in creating funny riddles. Every time I give you a word,
            you should generate UI that displays one new riddle related to that word.
            Each riddle should have both a question and an answer.
            '''
          ],
        );
    
        // ... initialize your LLM Client of choice using promptBuilder.systemPromptJoined()
    
        // Create the Conversation to orchestrate everything.
        _conversation = Conversation(
          controller: _surfaceController,
          transport: _transportAdapter,
        );
    
        // Listen for surface lifecycle events:
        _conversation.events.listen((event) {
          if (event is ConversationSurfaceAdded) {
            _onSurfaceAdded(event);
          } else if (event is ConversationSurfaceRemoved) {
            _onSurfaceDeleted(event);
          }
        });
      }
    
      @override
      void dispose() {
        _textController.dispose();
        _conversation.dispose();
        _transportAdapter.dispose();
    
        super.dispose();
      }
    }
    

Enviar mensajes y mostrar las respuestas del agente

#

Envía una solicitud al agente utilizando el método sendRequest en la clase Conversation, o transmitiendo directamente a tu cliente de LLM e introduciendo el flujo de resultados en el adaptador mediante el uso de _transportAdapter.addChunk.

Para recibir y mostrar la interfaz de usuario generada:

  1. Escucha el flujo de events en Conversation para rastrear la adición y eliminación de superficies de interfaz de usuario a medida que se generan. Estos eventos incluyen un ID de superficie para cada superficie.

  2. Construye un widget Surface para cada superficie activa utilizando los ID de superficie recibidos en el paso anterior.

    Por ejemplo:

    dart
    class _MyHomePageState extends State<MyHomePage> {
      // ...
    
      final _textController = TextEditingController();
      final _surfaceIds = <String>[];
    
      // Send a request containing the user's [text] to the agent.
      void _sendMessage(String text) async {
        if (text.trim().isEmpty) return;
        // await _conversation.sendRequest(ChatMessage.user(TextPart(text)));
      }
    
      // Invoked by the events stream listener when a new
      // UI surface is generated. Here, the ID is stored so the
      // build method can create a Surface to display it.
      void _onSurfaceAdded(ConversationSurfaceAdded update) {
        setState(() {
          _surfaceIds.add(update.surfaceId);
        });
      }
    
      // Invoked by the events stream listener when a UI surface is removed.
      void _onSurfaceDeleted(ConversationSurfaceRemoved update) {
        setState(() {
          _surfaceIds.remove(update.surfaceId);
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: Text(widget.title),
          ),
          body: Column(
            children: [
              Expanded(
                child: ListView.builder(
                  itemCount: _surfaceIds.length,
                  itemBuilder: (context, index) {
                    // For each surface, create a Surface to display it.
                    final id = _surfaceIds[index];
                    return Surface(surfaceContext: _surfaceController.contextFor(id));
                  },
                ),
              ),
              SafeArea(
                child: Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 16.0),
                  child: Row(
                    children: [
                      Expanded(
                        child: TextField(
                          controller: _textController,
                          decoration: const InputDecoration(
                            hintText: 'Enter a message',
                          ),
                        ),
                      ),
                      const SizedBox(width: 16),
                      ElevatedButton(
                        onPressed: () {
                          // Send the user's text to the agent.
                          _sendMessage(_textController.text);
                          _textController.clear();
                        },
                        child: const Text('Send'),
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
        );
      }
    }
    

Agregar tus propios widgets al catálogo

#

Para tu comodidad, puedes usar el catálogo principal de widgets proporcionado. Sin embargo, la mayoría de las aplicaciones de producción querrán definir un catálogo personalizado de widgets.

Para agregar tus propios widgets, sigue las siguientes instrucciones.

  1. Depender del paquete json_schema_builder

    Utiliza dart pub add para agregar json_schema_builder como una dependencia en tu archivo pubspec.yaml:

    dart pub add json_schema_builder
    
  2. Crear el esquema del nuevo widget

    Cada elemento del catálogo necesita un esquema que defina los datos requeridos para poblarlo. Utilizando el paquete json_schema_builder, define uno para el nuevo widget.

    dart
    import 'package:json_schema_builder/json_schema_builder.dart';
    import 'package:flutter/material.dart';
    import 'package:genui/genui.dart';
    
    final _schema = S.object(
      properties: {
        'question': S.string(description: 'The question part of a riddle.'),
        'answer': S.string(description: 'The answer part of a riddle.'),
      },
      required: ['question', 'answer'],
    );
    
  3. Crear un CatalogItem

    Cada CatalogItem representa un tipo de widget que el agente tiene permitido generar. Para hacerlo, combina un nombre, un esquema y una función constructora (builder) que produce los widgets que componen la interfaz de usuario generada.

    dart
    final riddleCard = CatalogItem(
      name: 'RiddleCard',
      dataSchema: _schema,
      widgetBuilder:
          (itemContext) {
            final json = itemContext.data as Map<String, Object?>;
            final question = json['question'] as String;
            final answer = json['answer'] as String;
    
            final context = itemContext.buildContext;
    
            return Container(
              constraints: const BoxConstraints(maxWidth: 400),
              decoration: BoxDecoration(border: Border.all()),
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(question, style: Theme.of(context).textTheme.headlineMedium),
                  const SizedBox(height: 8.0),
                  Text(answer, style: Theme.of(context).textTheme.headlineSmall),
                ],
              ),
            );
          },
    );
    
  4. Agregar el CatalogItem al catálogo

    Incluye los elementos de tu catálogo al instanciar SurfaceController.

    dart
    _surfaceController = SurfaceController(
      catalogs: [BasicCatalogItems.asCatalog().copyWith(newItems: [riddleCard])],
    );
    
  5. Actualizar la instrucción del sistema para usar el nuevo widget

    Para asegurarte de que el agente sepa utilizar tu nuevo widget, indícale a la instrucción del sistema cómo y cuándo hacerlo. Proporciona el nombre del CatalogItem al hacerlo.

    dart
    final promptBuilder = PromptBuilder.chat(
      catalog: catalog,
      systemPromptFragments: [
        '''
        You are an expert in creating funny riddles. Every time I give you a word,
        generate a RiddleCard that displays one new riddle related to that word.
        Each riddle should have both a question and an answer.
        '''
      ],
    );
    
    // Pass promptBuilder.systemPromptJoined() to your LLM Config
    

Modelo de datos y vinculación de datos

#

Un concepto central en genui es el DataModel, un almacén centralizado y observable para todo el estado dinámico de la interfaz de usuario. En lugar de que cada widget gestione su propio estado, su estado se almacena en el DataModel.

Los widgets están vinculados a los datos en este modelo. Cuando cambian los datos en el modelo, solo se reconstruyen los widgets que dependen de ese fragmento de datos específico. Esto se logra a través de un objeto DataContext pasado a la función constructora de cada widget.

Vinculación al modelo de datos

#

Para vincular la propiedad de un widget al modelo de datos, especifica un objeto JSON especial en los datos enviados desde la IA. Este objeto puede contener primitivas JSON estándar (para valores estáticos) o un objeto con una propiedad path (para vincularlo a un valor en el modelo de datos).

Por ejemplo, para mostrar el nombre de un usuario en un widget Text, la IA generaría:

json
{
  "id": "welcome-text",
  "component": "Text",
  "text": "Welcome to GenUI",
  "variant": "h1"
}

Imagen

#
json
{
  "id": "image",
  "component": "Image",
  "url": "https://example.com/image.png",
  "variant": "mediumFeature"
}

Actualizar el modelo de datos

#

Los widgets de entrada, como TextField, actualizan el DataModel directamente. Cuando el usuario escribe en un campo de texto que está vinculado a /user/name, el DataModel se actualiza, y cualquier otro widget vinculado a esa misma ruta se reconstruirá automáticamente para mostrar el nuevo valor.

Este flujo de datos reactivo simplifica la gestión del estado y crea un bucle de interacción potente y de gran ancho de banda entre el usuario, la interfaz de usuario y la IA.

Próximos pasos

#

Consulta los ejemplos incluidos en el repositorio de genui. La aplicación travel app muestra cómo definir tu propio catálogo de widgets que el agente puede usar para generar una interfaz de usuario específica de tu dominio.

Si algo no está claro o falta, por favor crea un problema.

Instrucciones del sistema

#

El paquete genui le da al LLM un conjunto de herramientas que puede usar para generar interfaces de usuario. Para lograr que el LLM use estas herramientas, las instrucciones del sistema proporcionadas a través de PromptBuilder deben indicarle explícitamente que lo haga.

Es por esto que el ejemplo anterior incluye una instrucción del sistema para el agente con la línea "Cada vez que te dé una palabra, debes generar una interfaz de usuario que...":

dart
final promptBuilder = PromptBuilder.chat(
  catalog: catalog,
  systemPromptFragments: [
    '''
    You are an expert in creating funny riddles.
    Every time I give you a word, you should generate UI that
    displays one new riddle related to that word.
    Each riddle should have both a question and an answer.
    '''
  ],
);

Resolución de problemas/Preguntas frecuentes

#

¿Cómo puedo configurar el registro?

#

Para observar la comunicación entre tu aplicación y el agente, habilita el registro de log en tu método main.

dart
import 'package:logging/logging.dart';
import 'package:genui/genui.dart';

final logger = configureLogging(level: Level.ALL);

void main() async {
  logger.onRecord.listen((record) {
    debugPrint('${record.loggerName}: ${record.message}');
  });

  // Additional initialization of bindings and Firebase.
}

Recibo errores sobre mi versión mínima de macOS/iOS.

#

Firebase tiene un requisito de versión mínima para las plataformas de Apple, el cual podría ser más alto que el valor predeterminado de Flutter. Revisa tu Podfile (para iOS) y CMakeLists.txt (para macOS) para asegurarte de que estás apuntando a una versión que cumpla o supere los requisitos de Firebase.