Flutter Catania · 17 ottobre 2026

L’AI sa la risposta.
Ma te la sa solo scrivere.

Generative UI in Flutter con genui e il protocollo A2UI
Andrea Valenzano Senior Mobile Dev @ Volumio srl
Atto 0 · Il problema

Stessi datiChiedi all’AI di una scossa sull’Etna

Quello che ricevi oggi
Il 14 ottobre 2026 alle 04:12 UTC l’INGV ha registrato un evento di magnitudo 3.4 nell’area sommitale dell’Etna, a 2.1 km di profondità, con epicentro 1.8 km a nord-est del Cratere di Bocca Nuova, nel comune di Castiglione di Sicilia. Nelle sei ore precedenti tre scosse minori, tra magnitudo 1.4 e 2.0. Nessun danno a persone o cose.
Un muro di prosa. Per capire dov’è devi aprire un’altra app.
Quello che potresti ricevere
Etna · area sommitale Mar Ionio 5 km
3.4
Castiglione di Sicilia (CT)
14 ott 2026 · 04:12 UTC · INGV
Profondità2.1 km
Distanza1.8 km NE
Precedenti 6h3 eventi
Vedi i 3 eventi precedenti →
Stessi identici dati. Stesso modello. Stessa API. Cambia solo chi decide la forma della risposta.
Demo

Quello che ricevi oggi

Dati INGV veri, ma il modello risponde solo a parole.

▶ menu Risposte → Solo testo
> dimmi della scossa più forte di questa settimana
attesa: tool call → INGV → un muro di prosa, come nella slide 2
Atto 1 · Il perché

Il panoramaTre modi di mettere insieme UI e AI

1

Chat pura

Una sola UI possibile: la bolla di testo. Tutto diventa prosa.

markdown · liste · tabelle ASCII

2

UI generativa, catalogo base

L’AI sceglie anche la forma, con i mattoncini generici di genui.

Text · Card · Column · Button

3 · stasera

UI generativa custom

Catalogo arricchito con i tuoi widget. Componenti pensati per i tuoi dati.

EventCard · MapCard · TimelineChart

Dal secondo in poi l’AI sceglie anche la forma. Il terzo è quello che costruiamo stasera.
Atto 1 · Il perché

DefinizioneChe cos’è, in una riga

L’agente sceglie anche la presentazione, non solo il contenuto.

LLM “normale”

Decide che cosa dire.

risposta : String

Agente GenUI

Decide anche come mostrarlo.

risposta : albero di widget

La risposta non è una stringa. È una descrizione di interfaccia che il tuo client sa rendere.
Atto 1 · Il perché

⚠ Se vi ricordate una sola slide, questaMettiamo subito a posto un equivoco

✗
L’AI genera codice Dart a runtime
// non succede. mai. final src = await ai.generate(); final w = eval(src); runApp(w); // ?!

Niente eval. Niente compilatore a bordo. Non esiste in Dart, e non lo vuoi.

✓
L’AI compone widget da un catalogo che scrivi tu
// A2UI vero, dai test della demo {"version":"v0.9","updateComponents":{ "surfaceId":"s1","components":[{ "id":"root","component":"EventCard", "event":{…,"magnitude":4.3,…}}]}}

È dati, non codice. EventCard esiste perché l’hai scritta tu e l’hai messa nel catalogo.

Il catalogo è il vocabolario che concedi al modello.
Ed è anche, nello stesso momento, la sua sandbox.
Atto 1 · Il perché

Il protocolloA2UI — il formato che il modello parla

  • Protocollo aperto, non una libreria: descrive come si descrive una UI, non come la disegni.
  • Versione v0.9, portato avanti da Google. a2ui.org
  • Renderer: Flutter (genui), Angular. React in arrivo, SwiftUI e Jetpack Compose in roadmap.
Perché JSON

Sicuro: sono dati, non c’è niente da eseguire.

Veloce: gli LLM lo scrivono bene, i client lo leggono subito.

Ovunque: lo legge qualunque linguaggio. Il tuo catalogo resta l’unica cosa specifica della piattaforma.

Modello
→
A2UI (JSON in streaming)
→
genui
→
Widget Flutter
Atto 1 · Il perché

Onestà intellettualeStato dell’arte, senza entusiasmo

Packagegenui — non più flutter_genui
Versione0.10.3  alpha · ultima 0.10.4
Riscritturamaggio 2026, sopra A2UI v0.9
In arrivospezzato in a2ui_core, a2ui_agent, a2ui_flutter
PiattaformeAndroid · iOS · macOS · web
Attenzione

ContentGenerator non esiste più: metà dei tutorial che trovate online è già obsoleta.

Pinnate la versione. In alpha un minor può cambiarvi l’API sotto i piedi. E il prossimo cambio di nome è già annunciato.

Dati verificati il 17 ottobre 2026. Ricontrollateli: a questo ritmo invecchiano in settimane.
Atto 2 · Architettura

Come è fatto dentroTre layer, uno sopra l’altro

1 · ENGINE 2 · TRANSPORT 3 · FACADE SurfaceController tiene lo stato delle surface e compone i widget dal Catalog A2uiTransportAdapter parla col modello: manda il prompt, riceve i chunk A2UI Conversation l’unica API che tocchi dalla UI: sendRequest(), events, state Catalog + CatalogItem lo scrivi tu Gemini via Firebase AI Logic ConversationBloc la tua app
Atto 2 · Architettura

Il concetto centraleChe cos’è una surface

  • Una superficie di rendering con un id.
  • Il modello dice: «sulla surface main metti questo albero».
  • Una conversazione può averne più di una: la risposta principale, un pannello di dettaglio, una barra di azioni.
  • Gli eventi (il tap su una card) risalgono dalla surface e diventano un nuovo turno.
surface
// chat_page.dart — una surface per risposta
for (final surfaceId in turn.surfaceIds)
  Surface(
    key: ValueKey(surfaceId),
    surfaceContext: bloc.agent.controller
        .contextFor(surfaceId),
  ),

// map_card_item.dart — il tap su un epicentro
onEventTap: (event) => itemContext.dispatchEvent(
  UserActionEvent(
    name: 'map_epicenter_tapped',
    surfaceId: itemContext.surfaceId,
    sourceComponentId: itemContext.id,
    context: {'eventId': event.eventId,
              'place': event.place},
  ),
),
          
Atto 2 · Architettura

Il pezzo che scrivi tuCatalog e CatalogItem

Il vocabolario che il modello ha il permesso di usare.

catalog
abstract final class EtnaCatalog {
  static const String catalogId = 'flutter-catania.dev:etna';
  static final List<CatalogItem> items = [
    eventCardItem,
    mapCardItem,
    timelineChartItem,
    comparisonCardItem,
  ];
  static Catalog asCatalog() => BasicCatalogItems.asNoAssetCatalog().copyWith(
    newItems: items, catalogId: catalogId, systemPromptFragments: [_rules]);
}
      
Quattro CatalogItem sopra i basic. Il modello non può nominare nient’altro.
Atto 2 · Architettura

Il contrattoLo schema, con json_schema_builder

schema
final Schema seismicEventSchema = S.object(
  properties: {
    'time': S.string(description: 'Istante dell\'evento in formato '
        'ISO 8601 UTC, es. 2026-09-18T03:14:00Z.'),
    'magnitude': S.number(description: 'Magnitudo, es. 3.4.'),
    'place': S.string(description: 'Località di riferimento, es. '
        '"1 km N Ragalna (CT)".'),
  },  // ... eventId, magnitudeType, latitude, longitude, depthKm
  required: ['eventId', 'time', 'magnitude', 'magnitudeType', 'place',
             'latitude', 'longitude', 'depthKm'],
);
      
Lo schema non è documentazione: è l’unica cosa che il modello vede del tuo widget. Se non è qui dentro, non può inventarlo.
Atto 2 · Architettura

Tutto insiemeIl giro completo, dal tap al widget

Utente tocca e digita App ConversationBloc Transport A2uiTransport Adapter Gemini Firebase AI INGV FDSN IngvRepository “cosa è successo?” onSend(message) prompt + tool schema searchEarthquakes() GeoJSON: 47 eventi chunk A2UI → addChunk() surface aggiornata MapCard + TimelineChart
Atto 3 · Codice e demo

I dati veriINGV FDSN — aperta, senza chiave

repository
// lib/data/ingv_repository.dart
static const double minLatitude = 37.4;
static const double maxLatitude = 37.9;
static const double minLongitude = 14.7;
static const double maxLongitude = 15.3;

final uri = Uri.https(_host, _path, {
  'starttime': _formatTime(startTime),
  if (endTime != null)
    'endtime': _formatTime(endTime),
  'minmagnitude': minMagnitude.toString(),
  'minlat': EtnaBounds.minLatitude.toString(),
  'maxlat': EtnaBounds.maxLatitude.toString(),
  'minlon': EtnaBounds.minLongitude.toString(),
  'maxlon': EtnaBounds.maxLongitude.toString(),
  'format': 'geojson',
  'limit': limit.toString(),
});
          
  • API: webservices.ingv.it
    /fdsnws/event/1/query
  • Nessuna API key. Nessun proxy.
  • Access-Control-Allow-Origin: * → funziona anche dal browser.
  • Il bounding box è il riquadro dell’Etna.
Atto 3 · Codice e demo

Function callingLo strumento che dai a Gemini

tool
static final FunctionDeclaration searchEarthquakes = FunctionDeclaration(
  searchEarthquakesName,   // 'searchEarthquakes'
  'Cerca i terremoti registrati dall\'INGV nell\'area dell\'Etna. '
  'Usa questa funzione ogni volta che servono dati sismici reali: '
  'non inventare mai magnitudo, date o località.',
  parameters: {
    'daysBack': Schema.integer(description: 'Quanti giorni indietro cercare a partire da oggi. '
        'Es. 7 per "questa settimana", 30 per "questo mese".'),
    // ... minMagnitude, limit, offsetDays
  },
  optionalParameters: ['minMagnitude', 'limit', 'offsetDays'],
);
// gli handler, nello stesso file, indicizzati per nome:
Map<String, ToolHandler> get handlers => {searchEarthquakesName: _handleSearchEarthquakes};
      
Il modello non sa cos’è l’INGV. Sa che esiste uno strumento con quel nome e quella descrizione, e decide lui quando serve.
Atto 3 · Codice e demo

Il widgetAnatomia di una card — e resta pura

event_card.dart
class EventCard extends StatelessWidget {
  const EventCard({super.key, required this.event, this.onTap});
  final SeismicEvent event;
  /// Tap opzionale: in app lo collega l'adapter, nel previewer resta null.
  final VoidCallback? onTap;
  @override
  Widget build(BuildContext context) {
    /* ... */
  }
}

@Preview(name: 'EventCard — scossa forte', group: 'Catalogo Etna',
    size: Size(460, 220))
Widget eventCardStrong() => EventCard(event: _strongQuake);
      
Il widget non sa nulla di AI, JSON o Firebase. È un vincolo del previewer — ed è comunque buon design.
Live · VS Code

Widget Preview

Un solo pannello con tutto il catalogo: le quattro card, in light, dark e casi limite, senza lanciare l’app e senza Firebase.

▶ apro il pannello Widget Preview
▶ group: 'Catalogo Etna' → 14 anteprime, 4 card
▶ cambio brightness → light / dark a caldo
▶ tocco un colore → hot reload nel pannello
Atto 3 · Codice e demo

L’adapterDa widget a CatalogItem

event_card_item.dart
final CatalogItem eventCardItem = CatalogItem(
  name: 'EventCard',
  dataSchema: S.object(
    description: 'Il dettaglio di UNA singola scossa. Usala quando l\'utente '
        'chiede di un evento specifico, per esempio "la scossa più forte".',
    properties: {'event': seismicEventSchema}, required: ['event']),
  widgetBuilder: (itemContext) {
    final data = itemContext.data as Map<String, Object?>;
    final event = parseSeismicEvent(data['event']! as Map<String, Object?>);
    return EventCard(event: event, onTap: () => itemContext.dispatchEvent(
        UserActionEvent(name: 'event_card_tapped',
            surfaceId: itemContext.surfaceId, /* ... */)));
  },
);
      
name

il nome che il modello può scrivere

dataSchema

il contratto: cosa può passare

widgetBuilder

la traduzione JSON → pixel

Atto 3 · Codice e demo

L’ultimo pezzoIl system prompt

prompt
PromptBuilder.chat(catalog: catalog).systemPromptJoined() // response_mode.dart
systemInstruction: Content.system(mode.systemPrompt),     // etna_agent.dart
// lib/catalog/etna_catalog.dart — le TUE regole, iniettate nel prompt
static const String _rules = '''
Sei l'assistente sismico dell'Etna. Rispondi in italiano.
SCELTA DELLA CARD — è la decisione più importante che prendi:
- EventCard: una singola scossa in dettaglio ("la scossa più forte").
- MapCard: DOVE sono avvenute le scosse, o il quadro di uno sciame.
- TimelineChart: QUANDO sono avvenute, l'andamento nel tempo.
- ComparisonCard: il paragone fra due periodi ("confronta con agosto").
DATI:
- Chiama sempre searchEarthquakes prima di parlare di scosse.
- Non inventare MAI magnitudo, date, località o identificativi: ...
''';
      
Il prompt non descrive i widget: quelli li legge dallo schema. Il prompt dice quando usarli.
Atto 3 · Codice e demo

Cosa ci aspettiamoUna sola app. Tre domande. Tre UI diverse.

Domanda 1

“cosa è successo sull’Etna questa settimana?”

↓

MapCard + TimelineChart

panoramica: dove e quando
Domanda 2

“dimmi della scossa più forte”

↓

EventCard dettagliata

zoom su un singolo evento
Domanda 3

“confronta con il mese scorso”

↓

ComparisonCard

due periodi affiancati
Tre presentazioni diverse dallo stesso catalogo. Zero schermate hardcoded.
Demo

Due cataloghi, stesse domande

Prima con i mattoncini generici di genui, poi con le card Etna.

Atto 4 · La realtà

Senza scontiCosa funziona, e cosa fa male

Funziona
  • Densità: una card dice in un colpo quello che tre paragrafi spiegano.
  • Interazione vera: il tap rientra nella conversazione.
  • Widget normali: Widget Preview, golden test e tema funzionano come sempre.
Fa male
  • Latenza: prima del primo chunk c’è il round-trip del tool. Lo streaming la maschera, non la toglie.
  • Non-determinismo: stessa domanda, UI diversa. Gli utenti lo notano.
  • Costo token: gli schemi del catalogo viaggiano a ogni turno.
Nessuna di queste è un bug da sistemare. Sono il prezzo del modello di interazione.
Atto 4 · La realtà

Il consiglio più utileQuando non usarla

  • Checkout e pagamenti. Il layout non è un’opinione.
  • Form critici: dati sanitari, contratti, consensi.
  • Flussi regolamentati o con accessibilità certificata.
  • Qualunque schermata che deve essere identica ogni volta.

Se il layout è un requisito e non un’opinione,
non è un caso da GenUI.

Il posto giusto è l’esplorazione: dati, ricerca, diagnostica, supporto. Dove non sai in anticipo cosa serve mostrare.

Grazie!

Repogithub.com/andrea689
Queste slideandrea689.github.io
Packagepub.dev/packages/genui
Codelab60 min · genui-intro