Integración de características
Cómo integrarse con otras características de Flutter.
Además de las características que se proporcionan automáticamente por el
LlmChatView, una serie de puntos de integración permiten que tu aplicación se fusione
de manera fluida con otras características para proporcionar funcionalidad adicional:
- Mensajes de bienvenida: Muestra un saludo inicial a los usuarios.
- Prompts sugeridos: Ofrece a los usuarios prompts predefinidos para guiar las interacciones.
- Instrucciones del sistema: Proporciona al LLM entradas específicas para influir en sus respuestas.
- Desactivar archivos adjuntos y entrada de audio: Elimina partes opcionales de la interfaz de usuario del chat.
- Gestionar comportamiento de cancelación o error: Cambia el comportamiento ante la cancelación del usuario o errores del LLM.
- Gestionar historial: Cada proveedor de LLM permite gestionar el historial de chat, lo cual es útil para borrarlo, cambiarlo dinámicamente y almacenarlo entre sesiones.
- Serialización/deserialización de chat: Guarda y recupera conversaciones entre sesiones de la aplicación.
- Widgets de respuesta personalizados: Introduce componentes de interfaz de usuario especializados para presentar las respuestas del LLM.
- Estilo personalizado: Define estilos visuales únicos para que la apariencia del chat coincida con la aplicación en general.
- Chat sin interfaz de usuario: Interactúa directamente con los proveedores de LLM sin afectar la sesión de chat actual del usuario.
- Proveedores de LLM personalizados: Crea tu propio proveedor de LLM para la integración del chat con el backend de tu propio modelo.
- Redireccionar prompts: Depura, registra o redirecciona los mensajes destinados al proveedor para rastrear problemas o enrutar prompts dinámicamente.
Mensajes de bienvenida
#La vista de chat te permite proporcionar un mensaje de bienvenida personalizado para establecer el contexto para el usuario:
Puedes inicializar el LlmChatView con un mensaje de bienvenida configurando el
parámetro welcomeMessage:
class ChatPage extends StatelessWidget {
const ChatPage({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text(App.title)),
body: LlmChatView(
welcomeMessage: 'Hello and welcome to the Flutter AI Toolkit!',
provider: FirebaseProvider(
model: FirebaseAI.geminiAI().generativeModel(
model: 'gemini-2.5-flash',
),
),
),
);
}
Para ver un ejemplo completo de cómo configurar el mensaje de bienvenida, consulta el ejemplo de bienvenida.
Prompts sugeridos
#Puedes proporcionar un conjunto de prompts sugeridos para darle al usuario una idea de para qué se ha optimizado la sesión de chat:
Las sugerencias solo se muestran cuando no hay un historial de chat existente. Al hacer clic en
una, se envía inmediatamente como una solicitud al LLM subyacente. Para configurar la lista de
sugerencias, construye el LlmChatView con el parámetro suggestions:
class ChatPage extends StatelessWidget {
const ChatPage({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text(App.title)),
body: LlmChatView(
suggestions: [
'I\'m a Star Wars fan. What should I wear for Halloween?',
'I\'m allergic to peanuts. What candy should I avoid at Halloween?',
'What\'s the difference between a pumpkin and a squash?',
],
provider: FirebaseProvider(
model: FirebaseAI.geminiAI().generativeModel(
model: 'gemini-2.5-flash',
),
),
),
);
}
Para ver un ejemplo completo de la configuración de sugerencias para el usuario, echa un vistazo al ejemplo de sugerencias.
Instrucciones del LLM
#Para optimizar las respuestas de un LLM en función de las necesidades de tu aplicación, querrás
darle instrucciones. Por ejemplo, la aplicación de ejemplo de recetas
usa el
parámetro systemInstructions de la clase GenerativeModel para adaptar el LLM
para que se enfoque en ofrecer recetas basadas en las instrucciones del usuario:
class _HomePageState extends State<HomePage> {
...
// create a new provider with the given history and the current settings
LlmProvider _createProvider([List<ChatMessage>? history]) => FirebaseProvider(
history: history,
...,
model: FirebaseAI.geminiAI().generativeModel(
model: 'gemini-2.5-flash',
...,
systemInstruction: Content.system('''
You are a helpful assistant that generates recipes based on the ingredients and
instructions provided as well as my food preferences, which are as follows:
${Settings.foodPreferences.isEmpty ? 'I don\'t have any food preferences' : Settings.foodPreferences}
You should keep things casual and friendly. You may generate multiple recipes in a single response, but only if asked. ...
''',
),
),
);
...
}
La configuración de las instrucciones del sistema es exclusiva de cada proveedor; el FirebaseProvider
te permite proporcionarlas a través del parámetro systemInstruction.
Ten en cuenta que, en este caso, estamos introduciendo las preferencias del usuario como parte de la
creación del proveedor de LLM pasado al constructor de LlmChatView. Configuramos las
instrucciones como parte del proceso de creación cada vez que el usuario cambia sus
preferencias. La aplicación de recetas permite al usuario cambiar sus preferencias alimenticias
usando un panel lateral (drawer) en el scaffold:
Cada vez que el usuario cambia sus preferencias alimenticias, la aplicación de recetas crea un nuevo modelo para usar las nuevas preferencias:
class _HomePageState extends State<HomePage> {
...
void _onSettingsSave() => setState(() {
// move the history over from the old provider to the new one
final history = _provider.history.toList();
_provider = _createProvider(history);
});
}
Llamada a funciones
#Para permitir que el LLM realice acciones en nombre del usuario, puedes proporcionar un
conjunto de herramientas (funciones) que el LLM pueda llamar. El FirebaseProvider soporta
la llamada a funciones de manera predeterminada. Maneja el bucle de enviar el prompt del usuario,
recibir una solicitud de llamada a función del LLM, ejecutar la función y enviar el
resultado de vuelta al LLM hasta que se genere una respuesta de texto final.
Para usar la llamada a funciones, necesitas definir tus herramientas y pasarlas al
FirebaseProvider. Consulta el ejemplo de llamada a funciones
para más detalles.
Desactivar archivos adjuntos y entrada de audio
#Si deseas desactivar los archivos adjuntos (el botón +) o la entrada de audio (el botón del
micrófono), puedes hacerlo con los parámetros enableAttachments y enableVoiceNotes
para el constructor de LlmChatView:
class ChatPage extends StatelessWidget {
const ChatPage({super.key});
@override
Widget build(BuildContext context) {
// ...
return Scaffold(
appBar: AppBar(title: const Text('Restricted Chat')),
body: LlmChatView(
// ...
enableAttachments: false,
enableVoiceNotes: false,
),
);
}
}
Ambas banderas tienen el valor predeterminado de true.
Conversión de voz a texto personalizada
#De forma predeterminada, el AI Toolkit usa el LlmProvider que se pasa a la LlmChatView
para proporcionar la implementación de voz a texto. Si deseas proporcionar tu propia
implementación, por ejemplo para usar un servicio específico del dispositivo, puedes hacerlo
implementando la interfaz SpeechToText y pasándola al constructor de LlmChatView:
LlmChatView(
// ...
speechToText: MyCustomSpeechToText(),
)
Consulta el ejemplo de STT personalizado para más detalles.
Gestionar comportamiento de cancelación o error
#De forma predeterminada, cuando el usuario cancela una solicitud de LLM, la respuesta del LLM se complementará con la cadena "CANCEL" y aparecerá un mensaje informando que el usuario ha cancelado la solicitud. Asimismo, en caso de un error del LLM, como una pérdida de conexión de red, la respuesta del LLM se complementará con la cadena "ERROR" y aparecerá un cuadro de diálogo de alerta con los detalles del error.
Puedes anular el comportamiento de cancelación y error con los parámetros cancelMessage,
errorMessage, onCancelCallback y onErrorCallback
de
LlmChatView. Por ejemplo, el siguiente código reemplaza el comportamiento predeterminado de
manejo de cancelación:
class ChatPage extends StatelessWidget {
// ...
void _onCancel(BuildContext context) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Chat cancelled')));
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text(App.title)),
body: LlmChatView(
// ...
onCancelCallback: _onCancel,
cancelMessage: 'Request cancelled',
),
);
}
Puedes anular cualquiera o todos estos parámetros y la LlmChatView usará
sus valores predeterminados para lo que no anules.
Gestionar historial
#La interfaz estándar que define todos los proveedores de LLM que se pueden conectar a la vista de chat incluye la capacidad de obtener y establecer el historial para el proveedor:
abstract class LlmProvider implements Listenable {
Stream<String> generateStream(
String prompt, {
Iterable<Attachment> attachments,
});
Stream<String> sendMessageStream(
String prompt, {
Iterable<Attachment> attachments,
});
Iterable<ChatMessage> get history;
set history(Iterable<ChatMessage> history);
}
Cuando cambia el historial de un proveedor, este llama al método notifyListener
expuesto por la clase base Listenable. Esto significa que puedes suscribirte/desuscribirte
manualmente con los métodos add y remove o usarlo para construir
una instancia de la clase ListenableBuilder.
El método generateStream realiza una llamada al LLM subyacente sin afectar el
historial. Llamar al método sendMessageStream cambia el historial al añadir
dos nuevos mensajes al historial del proveedor —uno para el mensaje del usuario y otro para
la respuesta del LLM— cuando se completa la respuesta. La vista de chat usa
sendMessageStream cuando procesa un prompt de chat de un usuario y generateStream
cuando está procesando la entrada de voz del usuario.
Para ver o establecer el historial, puedes acceder a la propiedad history:
void _clearHistory() => _provider.history = [];
La capacidad de acceder al historial de un proveedor también es útil cuando se trata de recrear un proveedor manteniendo el historial:
class _HomePageState extends State<HomePage> {
...
void _onSettingsSave() => setState(() {
// move the history over from the old provider to the new one
final history = _provider.history.toList();
_provider = _createProvider(history);
});
}
El método _createProvider crea un nuevo proveedor con el historial del
proveedor anterior y las nuevas preferencias del usuario. Es transparente para el usuario;
ellos pueden seguir chateando pero ahora el LLM les da respuestas teniendo en cuenta sus
nuevas preferencias alimenticias. Por ejemplo:
class _HomePageState extends State<HomePage> {
...
// create a new provider with the given history and the current settings
LlmProvider _createProvider([List<ChatMessage>? history]) =>
FirebaseProvider(
history: history,
...
);
...
}
Para ver el historial en acción, consulta la aplicación de ejemplo de recetas y la aplicación de ejemplo de historial.
Serialización/deserialización de chat
#Guardar y restaurar el historial de chat entre sesiones de una aplicación requiere la capacidad
de serializar y deserializar cada prompt del usuario, incluidos los archivos adjuntos, y
cada respuesta de LLM. Ambos tipos de mensajes (los prompts del usuario y las respuestas de LLM)
se exponen en la clase ChatMessage. La serialización se puede lograr
utilizando el método toJson de cada instancia de ChatMessage.
Future<void> _saveHistory() async {
// get the latest history
final history = _provider.history.toList();
// write the new messages
for (var i = 0; i != history.length; ++i) {
// skip if the file already exists
final file = await _messageFile(i);
if (file.existsSync()) continue;
// write the new message to disk
final map = history[i].toJson();
final json = JsonEncoder.withIndent(' ').convert(map);
await file.writeAsString(json);
}
}
Del mismo modo, para deserializar, utiliza el método estático fromJson de la clase ChatMessage:
Future<void> _loadHistory() async {
// read the history from disk
final history = <ChatMessage>[];
for (var i = 0;; ++i) {
final file = await _messageFile(i);
if (!file.existsSync()) break;
final map = jsonDecode(await file.readAsString());
history.add(ChatMessage.fromJson(map));
}
// set the history on the controller
_provider.history = history;
}
Para garantizar una respuesta rápida al serializar, recomendamos escribir cada mensaje del usuario solo una vez. De lo contrario, el usuario debe esperar a que tu aplicación escriba cada mensaje cada vez y, en el caso de archivos adjuntos binarios, eso podría tomar un tiempo.
Para ver esto en acción, consulta la aplicación de ejemplo de historial.
Widgets de respuesta personalizados
#De forma predeterminada, la respuesta del LLM mostrada por la vista de chat es Markdown formateado. Sin embargo, en algunos casos, querrás crear un widget personalizado para mostrar la respuesta del LLM que sea específica e integrada con tu aplicación. Por ejemplo, cuando el usuario solicita una receta en la aplicación de ejemplo de recetas, la respuesta del LLM se usa para crear un widget específico para mostrar recetas tal como lo hace el resto de la aplicación y para proporcionar un botón Add (Añadir) en caso de que al usuario le gustaría añadir la receta a su base de datos:

Esto se logra configurando el parámetro responseBuilder del
constructor de LlmChatView:
LlmChatView(
provider: _provider,
welcomeMessage: _welcomeMessage,
responseBuilder: (context, response) => RecipeResponseView(
response,
),
),
En este ejemplo particular, el widget RecipeResponseView se construye con
el texto de respuesta del proveedor de LLM y lo utiliza para implementar su método build:
class RecipeResponseView extends StatelessWidget {
const RecipeResponseView(this.response, {super.key});
final String response;
@override
Widget build(BuildContext context) {
final children = <Widget>[];
String? finalText;
// created with the response from the LLM as the response streams in, so
// many not be a complete response yet
try {
final map = jsonDecode(response);
final recipesWithText = map['recipes'] as List<dynamic>;
finalText = map['text'] as String?;
for (final recipeWithText in recipesWithText) {
// extract the text before the recipe
final text = recipeWithText['text'] as String?;
if (text != null && text.isNotEmpty) {
children.add(MarkdownBody(data: text));
}
// extract the recipe
final json = recipeWithText['recipe'] as Map<String, dynamic>;
final recipe = Recipe.fromJson(json);
children.add(const Gap(16));
children.add(Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(recipe.title, style: Theme.of(context).textTheme.titleLarge),
Text(recipe.description),
RecipeContentView(recipe: recipe),
],
));
// add a button to add the recipe to the list
children.add(const Gap(16));
children.add(OutlinedButton(
onPressed: () => RecipeRepository.addNewRecipe(recipe),
child: const Text('Add Recipe'),
));
children.add(const Gap(16));
}
} catch (e) {
debugPrint('Error parsing response: $e');
}
...
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
);
}
}
Este código analiza el texto para extraer el texto introductorio y la receta del LLM, agrupándolos junto con un botón Add Recipe para mostrar en lugar del Markdown.
Ten en cuenta que estamos analizando la respuesta del LLM como JSON. Es común configurar el
proveedor en modo JSON y proporcionar un esquema para restringir el formato de sus
respuestas para asegurar que tenemos algo que podemos analizar. Cada proveedor expone
esta funcionalidad a su manera, pero la clase FirebaseProvider habilita
esto con un objeto GenerationConfig que el ejemplo de recetas utiliza de la siguiente manera:
class _HomePageState extends State<HomePage> {
...
// create a new provider with the given history and the current settings
LlmProvider _createProvider([List<ChatMessage>? history]) => FirebaseProvider(
...
model: FirebaseAI.geminiAI().generativeModel(
...
generationConfig: GenerationConfig(
responseMimeType: 'application/json',
responseSchema: Schema(...),
systemInstruction: Content.system('''
...
Generate each response in JSON format
with the following schema, including one or more "text" and "recipe" pairs as
well as any trailing text commentary you care to provide:
{
"recipes": [
{
"text": "Any commentary you care to provide about the recipe.",
"recipe":
{
"title": "Recipe Title",
"description": "Recipe Description",
"ingredients": ["Ingredient 1", "Ingredient 2", "Ingredient 3"],
"instructions": ["Instruction 1", "Instruction 2", "Instruction 3"]
}
}
],
"text": "any final commentary you care to provide",
}
''',
),
),
);
...
}
Este código inicializa el objeto GenerationConfig configurando el
parámetro responseMimeType como 'application/json' y el
responseSchema
parámetro como una instancia de la clase Schema que define la estructura del
JSON que estás preparado para analizar. Además, también es una buena práctica solicitar
JSON y proporcionar una descripción de ese esquema JSON en las instrucciones del
sistema, lo cual hemos hecho aquí.
Para ver esto en acción, consulta la aplicación de ejemplo de recetas.
Estilo personalizado
#La vista de chat viene lista para usar con un conjunto de estilos predeterminados para el
fondo, el campo de texto, los botones, los iconos, las sugerencias, etc.
Puedes personalizar completamente esos estilos configurando los tuyos propios usando el parámetro style
al constructor de LlmChatView:
LlmChatView(
provider: FirebaseProvider(...),
style: LlmChatViewStyle(...),
),
Por ejemplo, la aplicación de ejemplo de estilos personalizados usa esta característica para implementar una aplicación con temática de Halloween:

Para obtener una lista completa de los estilos disponibles en la clase LlmChatViewStyle,
consulta la documentación de referencia. También puedes personalizar la apariencia
del grabador de voz utilizando el parámetro voiceNoteRecorderStyle de la
clase LlmChatViewStyle, lo cual se demuestra en el ejemplo de
estilos.
Para ver los estilos personalizados en acción, además del ejemplo de estilos personalizados y el ejemplo de estilos, consulta el ejemplo de modo oscuro y la aplicación demo.
Chat sin interfaz de usuario
#No tienes que usar la vista de chat para acceder a la funcionalidad del proveedor subyacente. Además de poder llamarlo simplemente con cualquier interfaz propietaria que proporcione, también puedes usarlo con la interfaz LlmProvider.
Como ejemplo, la aplicación de ejemplo de recetas proporciona un botón Magic en la página para editar recetas. El propósito de ese botón es actualizar una receta existente en tu base de datos con tus preferencias alimenticias actuales. Al presionar el botón, puedes previsualizar los cambios recomendados y decidir si deseas aplicarlos o no:
En lugar de usar el mismo proveedor que usa la parte de chat de la aplicación, lo que insertaría mensajes de usuario y respuestas del LLM espurios en el historial de chat del usuario, la página de Editar receta crea su propio proveedor y lo usa directamente:
class _EditRecipePageState extends State<EditRecipePage> {
...
final _provider = FirebaseProvider(...);
...
Future<void> _onMagic() async {
final stream = _provider.sendMessageStream(
'Generate a modified version of this recipe based on my food preferences: '
'${_ingredientsController.text}\n\n${_instructionsController.text}',
);
var response = await stream.join();
final json = jsonDecode(response);
try {
final modifications = json['modifications'];
final recipe = Recipe.fromJson(json['recipe']);
if (!context.mounted) return;
final accept = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(recipe.title),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Modifications:'),
const Gap(16),
Text(_wrapText(modifications)),
],
),
actions: [
TextButton(
onPressed: () => context.pop(true),
child: const Text('Accept'),
),
TextButton(
onPressed: () => context.pop(false),
child: const Text('Reject'),
),
],
),
);
...
} catch (ex) {
...
}
}
}
}
La llamada a sendMessageStream crea entradas en el historial del proveedor, pero
dado que no está asociado con una vista de chat, no se mostrarán. Si
te resulta conveniente, también puedes lograr lo mismo llamando a generateStream,
lo que te permite reutilizar un proveedor existente sin afectar el historial de
chat.
Para ver esto en acción, consulta la página de Editar receta del ejemplo de recetas.
Redireccionar prompts
#Si deseas depurar, registrar o manipular la conexión entre la vista de chat
y el proveedor subyacente, puedes hacerlo con una implementación de una
función LlmStreamGenerator.
Luego pasas esa función a la
LlmChatView en el parámetro messageSender:
class ChatPage extends StatelessWidget {
final _provider = FirebaseProvider(...);
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text(App.title)),
body: LlmChatView(
provider: _provider,
messageSender: _logMessage,
),
);
Stream<String> _logMessage(
String prompt, {
required Iterable<Attachment> attachments,
}) async* {
// log the message and attachments
debugPrint('# Sending Message');
debugPrint('## Prompt\n$prompt');
debugPrint('## Attachments\n${attachments.map((a) => a.toString())}');
// forward the message on to the provider
final response = _provider.sendMessageStream(
prompt,
attachments: attachments,
);
// log the response
final text = await response.join();
debugPrint('## Response\n$text');
// return it
yield text;
}
}
Este ejemplo registra los prompts del usuario y las respuestas de LLM a medida que van de un lado a otro.
Al proporcionar una función como messageSender, es tu responsabilidad llamar
al proveedor subyacente. Si no lo haces, este no recibirá el mensaje. Esta capacidad
te permite hacer cosas avanzadas como enrutar a un proveedor dinámicamente o
la Generación Aumentada por Recuperación (RAG).
Para ver esto en acción, consulta la aplicación de ejemplo de registro.
A menos que se indique lo contrario, la documentación de este sitio refleja Flutter 3.44.0. Página actualizada por última vez el 2026-05-11. Ver código fuente oreportar un problema.