[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-integrare-lai-generativa-in-flutter-con-gemini-guida-a-firebase-ai":3,"comments-article-integrare-lai-generativa-in-flutter-con-gemini-guida-a-firebase-ai":26},{"id":4,"title":5,"slug":6,"excerpt":7,"body":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"status":16,"published_at":17,"meta_title":18,"meta_description":19,"category":20,"author":24},83,"Integrare l'AI generativa in Flutter con Gemini: guida a firebase_ai","integrare-lai-generativa-in-flutter-con-gemini-guida-a-firebase-ai","Come aggiungere funzionalità di AI generativa a un'app Flutter usando i modelli Gemini: setup con firebase_ai, risposte in streaming, chat multimodale, output strutturato JSON e best practice su sicurezza e costi.","## Perché portare l'AI generativa dentro l'app\n\nRiassumere un testo, generare descrizioni di prodotto, classificare un'immagine scattata dall'utente, costruire un assistente conversazionale: sono funzionalità che oggi si implementano con poche decine di righe grazie ai modelli Gemini. Il punto delicato non è la chiamata all'API, ma **come** la si effettua da un'app mobile, dove il codice è ispezionabile e una chiave API in chiaro è un rischio concreto.\n\nIn questa guida vediamo l'approccio consigliato oggi per Flutter: il pacchetto `firebase_ai`, che espone i modelli Gemini tramite Firebase AI Logic, con supporto a **Firebase App Check** per proteggere l'endpoint. Confronteremo anche l'alternativa `google_generative_ai` e capiremo quando ha senso usarla.\n\n## Client SDK o backend proprio?\n\nPrima del codice, una decisione architetturale:\n\n- **Chiamata diretta dal client con chiave API** (`google_generative_ai`): semplice, ma la chiave finisce nel binario. Va bene solo per prototipi, demo interne o app desktop\u002FCLI in cui la chiave è dell'utente stesso.\n- **Client SDK con Firebase AI Logic** (`firebase_ai`): non richiede chiave nel codice, l'accesso passa da un proxy Google autenticato e può essere protetto con App Check. È la scelta standard per app in produzione senza backend.\n- **Backend proprio** (Cloud Functions, server Dart\u002FNode): massimo controllo su rate limiting, logging, prompt injection e costi. Necessario se hai logica di business complessa o dati sensibili da filtrare.\n\nQui usiamo la seconda opzione, che copre la maggior parte dei casi reali.\n\n## Setup del progetto\n\nDopo aver collegato l'app a Firebase con la FlutterFire CLI (`flutterfire configure`), aggiungi le dipendenze:\n\n```yaml\ndependencies:\n  firebase_core: ^3.8.0\n  firebase_ai: ^2.0.0\n  firebase_app_check: ^0.3.2\n```\n\nAttiva quindi Firebase AI Logic dalla console Firebase (sezione AI Logic) scegliendo il provider: **Gemini Developer API** (piano gratuito disponibile, ideale per iniziare) oppure **Vertex AI** (fatturazione GCP, regioni configurabili, requisiti enterprise).\n\nInizializzazione tipica:\n\n```dart\nimport 'package:firebase_core\u002Ffirebase_core.dart';\nimport 'package:firebase_app_check\u002Ffirebase_app_check.dart';\nimport 'firebase_options.dart';\n\nFuture\u003Cvoid> main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n  await Firebase.initializeApp(\n    options: DefaultFirebaseOptions.currentPlatform,\n  );\n  await FirebaseAppCheck.instance.activate(\n    androidProvider: AndroidProvider.playIntegrity,\n    appleProvider: AppleProvider.appAttest,\n  );\n  runApp(const MyApp());\n}\n```\n\nSenza App Check chiunque potrebbe estrarre la configurazione Firebase e usare la tua quota: consideralo un requisito, non un extra.\n\n## La prima richiesta\n\n```dart\nimport 'package:firebase_ai\u002Ffirebase_ai.dart';\n\nfinal model = FirebaseAI.googleAI().generativeModel(\n  model: 'gemini-2.5-flash',\n  systemInstruction: Content.system(\n    'Sei un assistente che risponde in italiano, con tono conciso. '\n    'Se non conosci la risposta, dillo esplicitamente.',\n  ),\n  generationConfig: GenerationConfig(\n    temperature: 0.4,\n    maxOutputTokens: 512,\n  ),\n);\n\nFuture\u003CString> riassumi(String testo) async {\n  final response = await model.generateContent([\n    Content.text('Riassumi in 3 punti elenco:\\n\\n$testo'),\n  ]);\n  return response.text ?? 'Nessuna risposta generata.';\n}\n```\n\nAlcune note pratiche:\n\n- **`systemInstruction`** è il posto giusto per definire ruolo, lingua e vincoli del modello: è più stabile che ripetere le istruzioni in ogni prompt.\n- **`temperature`** bassa (0.2–0.4) per compiti deterministici (estrazione dati, classificazione), più alta (0.8–1.0) per contenuti creativi.\n- **`maxOutputTokens`** è la tua prima difesa contro risposte chilometriche e costi imprevisti.\n- `response.text` può essere `null` se la risposta è stata bloccata dai filtri di sicurezza: gestisci sempre il caso.\n\n## Streaming: la UX che fa la differenza\n\nAspettare 8 secondi con uno spinner è la ricetta perfetta per far chiudere l'app. Lo streaming mostra il testo mentre viene generato e cambia radicalmente la percezione della latenza.\n\n```dart\nStream\u003CString> riassumiStream(String testo) async* {\n  final buffer = StringBuffer();\n  final stream = model.generateContentStream([\n    Content.text('Riassumi in 3 punti elenco:\\n\\n$testo'),\n  ]);\n\n  await for (final chunk in stream) {\n    final text = chunk.text;\n    if (text != null) {\n      buffer.write(text);\n      yield buffer.toString();\n    }\n  }\n}\n```\n\nLato UI, un semplice `StreamBuilder` basta:\n\n```dart\nStreamBuilder\u003CString>(\n  stream: _stream,\n  builder: (context, snapshot) {\n    if (snapshot.hasError) {\n      return Text('Errore: ${snapshot.error}');\n    }\n    final text = snapshot.data ?? '';\n    return SelectableText(\n      text.isEmpty ? 'Sto pensando…' : text,\n    );\n  },\n)\n```\n\nRicorda di **annullare la sottoscrizione** quando l'utente lascia la schermata: conserva la `StreamSubscription` in un controller\u002Fnotifier e chiamane `cancel()` nel `dispose()`, altrimenti continui a pagare token per una risposta che nessuno leggerà.\n\n## Chat con memoria della conversazione\n\nPer un assistente conversazionale non serve ricostruire a mano la history: l'SDK offre una sessione di chat.\n\n```dart\nlate final ChatSession _chat = model.startChat(\n  history: [\n    Content.text('Mi chiamo Luca e sviluppo app Flutter.'),\n    Content.model([TextPart('Ciao Luca! Come posso aiutarti?')]),\n  ],\n);\n\nStream\u003CString> invia(String messaggio) async* {\n  final buffer = StringBuffer();\n  await for (final chunk in _chat.sendMessageStream(Content.text(messaggio))) {\n    buffer.write(chunk.text ?? '');\n    yield buffer.toString();\n  }\n}\n```\n\nAttenzione: la history cresce a ogni turno e **ogni token in input viene fatturato**. In conversazioni lunghe conviene troncare i messaggi più vecchi o mantenere un riassunto rolling generato dal modello stesso.\n\n## Input multimodale: testo + immagini\n\nGemini accetta immagini, audio e PDF come parte del prompt. Un caso d'uso classico: l'utente fotografa uno scontrino e l'app estrae i dati.\n\n```dart\nimport 'dart:typed_data';\n\nFuture\u003CString> analizzaImmagine(Uint8List bytes) async {\n  final response = await model.generateContent([\n    Content.multi([\n      TextPart('Descrivi il contenuto di questa immagine in una frase.'),\n      InlineDataPart('image\u002Fjpeg', bytes),\n    ]),\n  ]);\n  return response.text ?? '';\n}\n```\n\nPrima di inviare, **comprimi**: un'immagine da 8 MP consuma banda, tempo e token. Ridimensionare il lato lungo a 1024–1536 px con `image` o `flutter_image_compress` è quasi sempre sufficiente per la comprensione del modello.\n\n## Output strutturato: JSON invece di prosa\n\nSe devi popolare un modello Dart, chiedere \"rispondi in JSON\" nel prompt non è affidabile. Usa invece lo schema di risposta, che vincola il modello a un formato valido.\n\n```dart\nfinal jsonSchema = Schema.object(\n  properties: {\n    'titolo': Schema.string(description: 'Titolo breve del documento'),\n    'importo': Schema.number(description: 'Totale in euro'),\n    'categorie': Schema.array(items: Schema.string()),\n  },\n);\n\nfinal extractor = FirebaseAI.googleAI().generativeModel(\n  model: 'gemini-2.5-flash',\n  generationConfig: GenerationConfig(\n    responseMimeType: 'application\u002Fjson',\n    responseSchema: jsonSchema,\n  ),\n);\n\nFuture\u003CScontrino> estrai(Uint8List foto) async {\n  final res = await extractor.generateContent([\n    Content.multi([\n      TextPart('Estrai i dati dello scontrino.'),\n      InlineDataPart('image\u002Fjpeg', foto),\n    ]),\n  ]);\n  final map = jsonDecode(res.text!) as Map\u003CString, dynamic>;\n  return Scontrino.fromJson(map);\n}\n```\n\nAbbinato a `json_serializable` o `freezed`, questo pattern trasforma l'AI in una sorgente dati come un'altra, tipizzata e testabile. Continua comunque a proteggere il `jsonDecode` con un try\u002Fcatch: il modello può restituire campi mancanti o valori fuori range.\n\n## Function calling: far agire il modello\n\nQuando l'assistente deve consultare dati reali (meteo, catalogo, database locale), si dichiarano delle funzioni che il modello può richiedere di eseguire.\n\n```dart\nfinal meteoTool = FunctionDeclaration(\n  'getMeteo',\n  'Restituisce il meteo attuale per una città italiana',\n  parameters: {\n    'citta': Schema.string(description: 'Nome della città'),\n  },\n);\n\nfinal agent = FirebaseAI.googleAI().generativeModel(\n  model: 'gemini-2.5-flash',\n  tools: [Tool.functionDeclarations([meteoTool])],\n);\n\nFuture\u003CString> chiedi(String domanda) async {\n  final chat = agent.startChat();\n  var response = await chat.sendMessage(Content.text(domanda));\n\n  for (final call in response.functionCalls) {\n    if (call.name == 'getMeteo') {\n      final result = await meteoRepository.get(call.args['citta'] as String);\n      response = await chat.sendMessage(\n        Content.functionResponse(call.name, result),\n      );\n    }\n  }\n  return response.text ?? '';\n}\n```\n\nIl modello **non esegue nulla**: propone la chiamata, la tua app decide se e come eseguirla. Non delegare mai operazioni distruttive (pagamenti, cancellazioni) senza conferma esplicita dell'utente.\n\n## Errori, filtri e resilienza\n\nLe chiamate AI falliscono più spesso delle normali REST: rete instabile, quota esaurita, contenuto bloccato. Isola tutto dietro un repository e mappa gli errori in tipi tuoi.\n\n```dart\nFuture\u003CString> chiediSicuro(String prompt) async {\n  try {\n    final res = await model.generateContent([Content.text(prompt)])\n        .timeout(const Duration(seconds: 30));\n    final text = res.text;\n    if (text == null || text.isEmpty) {\n      throw const AiException('Risposta bloccata o vuota');\n    }\n    return text;\n  } on FirebaseAIException catch (e) {\n    throw AiException('Servizio AI non disponibile: ${e.message}');\n  } on TimeoutException {\n    throw const AiException('Il modello non ha risposto in tempo');\n  }\n}\n```\n\nPuoi anche modulare i `safetySettings` per categoria (molestie, contenuti pericolosi, ecc.), ma verifica sempre `promptFeedback` e `candidates.first.finishReason` per capire se una risposta è stata troncata (`maxTokens`) o filtrata.\n\n## Testabilità\n\nNon fare mai chiamate reali nei test: sono lente, costose e non deterministiche. Definisci un'astrazione minima:\n\n```dart\nabstract interface class AiClient {\n  Future\u003CString> complete(String prompt);\n  Stream\u003CString> completeStream(String prompt);\n}\n```\n\nL'implementazione con `firebase_ai` vive in un solo file; nei test di widget e unit inietti una fake che restituisce risposte fisse. Come bonus, cambiare provider (Gemini, un modello on-device, un tuo backend) diventa una modifica localizzata.\n\n## Costi e prestazioni: regole pratiche\n\n- Usa i modelli **Flash** per la maggior parte dei casi: sono molto più economici e rapidi dei modelli Pro, con qualità sufficiente per riassunti, classificazioni ed estrazioni.\n- **Metti in cache** le risposte deterministiche (per esempio la descrizione generata per un prodotto) su Isar\u002FDrift o Firestore: rigenerare lo stesso testo a ogni apertura è denaro buttato.\n- Fai **debounce** degli input: nessuna chiamata a ogni battitura in un campo di testo.\n- Imposta limiti lato utente (numero di richieste al giorno) e monitora l'uso in console prima di andare in produzione.\n- Comprimi le immagini e limita la lunghezza della history: gli input pesano quanto gli output.\n\n## Conclusione\n\nCon `firebase_ai` l'integrazione di Gemini in Flutter è questione di poche righe, ma il valore reale sta nei dettagli: streaming per la percezione di velocità, output strutturato per avere dati e non testo, App Check per non regalare la quota, astrazione per poter testare e cambiare provider. Parti da una singola funzionalità ben delimitata — un riassunto, una classificazione, un'estrazione da foto — misurane costi e utilità, e solo dopo estendi l'AI al resto dell'app.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F333415aa-275e-4341-9259-45046521950c.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1695978918891-1bd213a44966?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODgxNDg4OTd8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"NSYS Group","https:\u002F\u002Funsplash.com\u002F@nsys_group","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-woman-holding-a-smart-phone-next-to-another-cell-phone-V-gklyRuFhQ",null,"published","2026-08-31T04:01:37+00:00","AI generativa in Flutter con Gemini e firebase_ai","Guida pratica per integrare i modelli Gemini in Flutter con firebase_ai: setup, streaming, chat multimodale, output JSON, function calling e sicurezza.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1789120584681]