
主要特点
- 多轮聊天:自动维护聊天历史,保持多轮交互的语义连贯性
- 流式响应渲染:实时逐字显示 AI 回复,提升交互体验
- 富文本显示:支持 Markdown 解析、代码块高亮、链接识别等
- 语音输入:使用语音输入prompt。
- 多媒体输入:支持发送图片、文件等附件,AI 可识别处理
- 自定义样式:提供自定义样式,以匹配App设计。
- 聊天序列化/反序列化:存储和检索App会话之间的对话。
- 自定义响应Widget:引入专门的UI组件来呈现LLM响应。
- 可插拔LLM支持:实现一个简单的界面来插入自定义LLM。
- 跨平台支持:兼容Android、iOS、Web和macOS平台。
demo效果

源代码可在GitHub上找到
安装
依赖项添加到pubspec.yaml文件中
dependencies:
flutter_ai_toolkit: ^latest_version
google_generative_ai: ^latest_version # 使用Gemini
firebase_core: ^latest_version # 使用Firebase Vertex AI
Gemini AI配置
要使用Google Gemini AI,请从Google Gemini AI Studio获取API密钥。
还需要选择一个Gemini model。
import 'package:google_generative_ai/google_generative_ai.dart';
import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart';
class ChatPage extends StatelessWidget {
const ChatPage({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text(App.title)),
body: LlmChatView(
provider: GeminiProvider( // Gemini 服务提供商
model: GenerativeModel( // Gemini model
model: 'gemini-2.0-flash',
apiKey: 'GEMINI-API-KEY', // Gemini API Key
),
),
),
);
}
GenerativeModel类来自google_generative_ai
软件包。GeminiProvider将Gemini AI插入到LlmChatView
,LlmChatView是顶级Widget,与您的用户提供基于LLM的聊天对话。
Vertex AI configuration
另外一个AI服务是Firebase的Vertex AI。不需要API密钥,并用更安全的Firebase取代它。要在项目中使用Vertex AI,请按照 Get started with the Gemini API using the Vertex AI in Firebase SDKs 中描述的步骤进行操作。
完成后,使用flutterfire CLI工具将新的Firebase项目集成到您的Flutter App中,如Add Firebase to your Flutter app文档中所述。
按照这些说明操作后,您就可以在Flutter App中使用Firebase Vertex AI了。
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_vertexai/firebase_vertexai.dart';
import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart';
// ... other imports
import 'firebase_options.dart'; // from `flutterfire config`
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
runApp(const App());
}
在Flutter App中正确初始化Firebase后,可以创建Vertex provider的实例了:
class ChatPage extends StatelessWidget {
const ChatPage({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text(App.title)),
// create the chat view, passing in the Vertex provider
body: LlmChatView(
provider: VertexProvider(
chatModel: FirebaseVertexAI.instance.generativeModel(
model: 'gemini-2.0-flash',
),
),
),
);
}
FirebaseVertexAI类来自firebase_vertex ai软件包。构建VertexProvider类,将Vertex AI暴露给LlmChatView。不需要提供API密钥。这些都作为Firebase项目自动处理了。
LlmChatView
LlmChatView Widget 是AI Toolkit提供的互动聊天组件。支持如下功能
- 多行文本输入:允许用户在输入prompt时粘贴长文本或插入新行。
- 语音输入:允许用户使用语音输入prompt
- 多媒体输入:允许用户拍照和发送图像和其他文件类型prompt。
- 图像缩放:允许用户放大图像缩略图。
- 复制到剪贴板:允许用户将消息或LLM响应的文本复制到剪贴板。
- 消息编辑:允许用户编辑最新的消息以重新提交到LLM。
- 支持Material 和 Cupertino两种设计样式
多行文本输入

语音输入



多媒体输入



图片缩放
点击能缩放图片

复制到剪贴板


文字编辑
长按文字, 弹出编辑菜单


支持Material and Cupertino两种设计样式

额外的功能
- 欢迎信息:向用户显示初始问候。
- prompt建议:向用户提供预定义的提建议prompt,以引导互动。
- 系统指令:让 AI 系统明确 “做什么”“如何做” 以及 “在什么条件下执行”,类似于给 AI 系统下达的 “任务说明书” 或 “操作指南”。
- 管理历史记录:每个LLM Provider都允许管理聊天记录,用于清除、动态更改和在会话之间存储聊天状态。
- 聊天序列化/反序列化:存储和检索App会话之间的对话。
- 自定义响应Widget:引入专门的UI组件来呈现LLM响应。
- 自定义样式:定义独特的视觉样式,以将聊天外观与整个App相匹配。
- 自定义LLM Provider:构建自定义LLM Provider,将聊天与您自己的模型后端集成。
- 重新路由提示:调试、记录或重新路由消息,旨在让Provider动态跟踪问题或路由提示。
欢迎信息
自定义欢迎消息

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!', //初始化LlmChatView的欢迎消息:
provider: GeminiProvider(
model: GenerativeModel(
model: 'gemini-2.0-flash',
apiKey: geminiApiKey,
),
),
),
);
}
prompt建议
没有聊天记录
时,提供一组建议的prompt

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: GeminiProvider(
model: GenerativeModel(
model: 'gemini-2.0-flash',
apiKey: geminiApiKey,
),
),
),
);
}
系统指令
让 AI 明确 “做什么”“如何做” 以及 “在什么条件下执行”,类似于给 AI 系统下达的 “任务说明书” 或 “操作指南”。
例如,食谱示例App使用systemInstructions参数来定制LLM,以专注于根据用户的说明提供食谱:
class _HomePageState extends State<HomePage> {
...
// create a new provider with the given history and the current settings
LlmProvider _createProvider([List<ChatMessage>? history]) => GeminiProvider(
history: history,
...,
model: GenerativeModel(
model: 'gemini-2.0-flash',
apiKey: geminiApiKey,
...,
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. ...
''', /// 系统指令
),
),
);
...
}
历史记录管理
访问history属性查看或设置历史记录:
void _clearHistory() => _provider.history = [];
使用旧的历史来创建新的Provider:
class _HomePageState extends State<HomePage> {
...
void _onSettingsSave() => setState(() {
// 迁移旧的历史记录到新的供应商
final history = _provider.history.toList();
_provider = _createProvider(history);
});
}
_createProvider方法创建了一个具有上一个Provider历史记录和新用户首选项的新Provider。这对用户来说是无缝的;他们可以继续聊天,但现在LLM会考虑他们的新食物偏好,给他们回复
class _HomePageState extends State<HomePage> {
...
// 根据给定的历史记录和当前设置创建一个新的提供者
LlmProvider _createProvider([List<ChatMessage>? history]) =>
GeminiProvider(
history: history,
...
);
...
}
Chat序列化/反序列化
要在App会话之间保存和恢复聊天记录,需要能够对每个用户prompt
(包括附件
)和每个 LLM 响应
进行序列化和反序列化。
两种消息(用户prompt和LLM响应)都暴露在ChatMessage
类中。
序列化可以通过使用每个ChatMessage实例的toJson方法来完成。
Future<void> _saveHistory() async {
// 获取最新的历史
final history = _provider.history.toList();
// 保存历史消息
for (var i = 0; i != history.length; ++i) {
// 文件存在旧忽略
final file = await _messageFile(i);
if (file.existsSync()) continue;
// 新消息保存到磁盘
final map = history[i].toJson();
final json = JsonEncoder.withIndent(' ').convert(map);
await file.writeAsString(json);
}
}
同样,要反序列化,使用ChatMessage fromJson方法:
Future<void> _loadHistory() async {
// 从磁盘读取历史记录
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));
}
/// 设置历史记录
_provider.history = history;
}
自定义响应Widget
默认聊天视图显示的 LLM 响应格式为 Markdown。可以创建一个自定义Widget来显示您的App风格的样式:

设置LlmChatView的responseBuilder参数:
LlmChatView(
provider: _provider,
welcomeMessage: _welcomeMessage,
responseBuilder: (context, response) => RecipeResponseView(
response,
),
),
class RecipeResponseView extends StatelessWidget {
const RecipeResponseView(this.response, {super.key});
final String response;
@override
Widget build(BuildContext context) {
final children = <Widget>[];
String? finalText;
// 收到LLM的回复后即时生成内容,因此目前无法得到完整的回复,添加一个按钮以便将食谱添加到列表中
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));
}
// 提取食谱
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),
],
));
// 添加按钮将食谱添加到列表中。
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,
);
}
}
自定义样式
使用LlmChatView构造函数的style参数来设置自己的样式,包括背景、文本字段、按钮、图标、建议等默认样式:
LlmChatView(
provider: GeminiProvider(...),
style: LlmChatViewStyle(...),
),
万圣节主题演示App

没有UI的聊天
不使用聊天视图也能访问Provider接口。

class _EditRecipePageState extends State<EditRecipePage> {
...
final _provider = GeminiProvider(...);
...
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}',
); // 发送用户偏好食谱设置给llm provider
var response = await stream.join(); // 获取llm推荐的响应
final json = jsonDecode(response);
try {
final modifications = json['modifications'];
final recipe = Recipe.fromJson(json['recipe']);
if (!context.mounted) return;
final accept = await showDialog<bool>( // 只使用了llm服务,没有使用聊天界面
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) {
...
}
}
}
}
重新路由Prompt
设置LlmChatView messageSender来调试、记录或操作聊天视图和底层Provider之间的连接
class ChatPage extends StatelessWidget {
final _provider = GeminiProvider(...);
@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())}');
// 发送消息到provider
final response = _provider.sendMessageStream(
prompt,
attachments: attachments,
);
// log response信息
final text = await response.join();
debugPrint('## Response\n$text');
yield text;
}
}
用于一些高级操作,如动态路由到Provider或检索增强生成(RAG)。
定制LLM Provider
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);
}
任何实现LlmProvider接口的都可以插入聊天视图, 可以是云或本地的
- 提供配置支持
- 处理历史
- 将消息和附件翻译成底层LLM
- 调用底层LLM
配置支持
class GeminiProvider extends LlmProvider ... {
@immutable
GeminiProvider({
required GenerativeModel model,
...
}) : _model = model,
...
final GenerativeModel _model;
...
}
处理历史
历史记录是Provider的重要组成部分
Provider不仅需要允许直接操作历史记录,而且必须在更改时通知Listener。
为了支持序列化和更改Provider参数,必须支持保存历史记录作为构建过程的一部分。
class GeminiProvider extends LlmProvider with ChangeNotifier {
@immutable
GeminiProvider({
required GenerativeModel model,
Iterable<ChatMessage>? history,
...
}) : _model = model,
_history = history?.toList() ?? [],
... { ... }
final GenerativeModel _model;
final List<ChatMessage> _history;
...
/// 设置对话历史记录并重新初始化聊天会话
@override
Stream<String> sendMessageStream(
String prompt, {
Iterable<Attachment> attachments = const [],
}) async* {
final userMessage = ChatMessage.user(prompt, attachments);
final llmMessage = ChatMessage.llm();
_history.addAll([userMessage, llmMessage]); /// 添加到历史记录
final response = _generateStream(
prompt: prompt,
attachments: attachments,
contentStreamGenerator: _chat!.sendMessageStream,
);
yield* response.map((chunk) {
llmMessage.append(chunk);
return chunk;
});
notifyListeners();
}
/// 获取当前的对话历史记录
@override
Iterable<ChatMessage> get history => _history;
/// 设置对话历史记录并重新初始化聊天会话
@override
set history(Iterable<ChatMessage> history) {
_history.clear();
_history.addAll(history);
_chat = _startChat(history);
notifyListeners();
}
...
}
import 'package:google_generative_ai/google_generative_ai.dart';
...
class GeminiProvider extends LlmProvider with ChangeNotifier {
...
static Part _partFrom(Attachment attachment) => switch (attachment) {
(final FileAttachment a) => DataPart(a.mimeType, a.bytes),
(final LinkAttachment a) => FilePart(a.url),
};
static Content _contentFrom(ChatMessage message) => Content(
message.origin.isUser ? 'user' : 'model',
[
TextPart(message.text ?? ''),
...message.attachments.map(_partFrom),
],
);
}
调用LLM
class GeminiProvider extends LlmProvider with ChangeNotifier {
...
@override
Stream<String> generateStream(
String prompt, {
Iterable<Attachment> attachments = const [],
}) =>
_generateStream(
prompt: prompt,
attachments: attachments,
contentStreamGenerator: (c) => _model.generateContentStream([c]),
);
@override
Stream<String> sendMessageStream(
String prompt, {
Iterable<Attachment> attachments = const [],
}) async* {
final userMessage = ChatMessage.user(prompt, attachments);
final llmMessage = ChatMessage.llm();
_history.addAll([userMessage, llmMessage]);
final response = _generateStream(
prompt: prompt,
attachments: attachments,
contentStreamGenerator: _chat!.sendMessageStream,
);
yield* response.map((chunk) {
llmMessage.append(chunk);
return chunk;
});
notifyListeners();
}
Stream<String> _generateStream({
required String prompt,
required Iterable<Attachment> attachments,
required Stream<GenerateContentResponse> Function(Content)
contentStreamGenerator,
}) async* {
final content = Content('user', [
TextPart(prompt),
...attachments.map(_partFrom),
]);
final response = contentStreamGenerator(content);
yield* response
.map((chunk) => chunk.text)
.where((text) => text != null)
.cast<String>();
}
@override
Iterable<ChatMessage> get history => _history;
@override
set history(Iterable<ChatMessage> history) {
_history.clear();
_history.addAll(history);
_chat = _startChat(history);
notifyListeners();
}
}
最终的AI 聊天效果
