鸿蒙 + Flutter 如何把 AI 助手嵌进应用页面里——以食界探味为
适合谁看
-
想在鸿蒙端把 AI 助手接进 Flutter 应用的人
-
不想把 AI 功能做成"孤立聊天页"的开发者
-
想理解鸿蒙原生能力(语音识别、TTS)如何通过 Platform Channel 被 Flutter AI 页面消费的人
-
想理解页面、协调器、工具调用怎么串起来的人
问题背景
很多 AI 页面一开始都很好做:
-
放一个输入框
-
调一下模型
-
展示返回文本
但到了真实项目里,很快就会出现更难的问题:
-
用户从别的页面带着问题进来怎么办
-
AI 回复和业务卡片要不要一起展示
-
页面退出时语音播报要不要停
-
对话状态、语音状态和页面状态怎么配合
-
鸿蒙端的语音识别和 TTS 能力怎么被 Flutter 页面调用(Android/iOS 各有一套,鸿蒙也有一套)
-
权限申请、引擎生命周期管理在鸿蒙侧怎么处理
这也是为什么"能不能聊天"并不是关键,关键是 AI 助手怎么被嵌进原有应用结构里——尤其是鸿蒙跨语言桥接这一层。
项目中的真实场景
食界探味当前的 AI 助手相关代码主要在:
Flutter 侧(AI 页面 + 协调器):
-
app/lib/features/ai_assistant/screens/ai_assistant_screen.dart— AI 对话页面 -
app/lib/core/ai/ai_explore_coordinator.dart— AI 流程编排协调器 -
app/lib/core/ai/agent_service.dart— 模型调用统一封装 -
app/lib/core/ai/models/ai_session_state.dart— 会话状态模型 -
app/lib/core/ai/tools/— 工具层(搜索菜品、菜品详情、随机推荐等)
Flutter 侧(Platform Channel 封装):
-
app/lib/core/platform/speech_recognition_channel.dart— 语音识别通道 -
app/lib/core/platform/text_to_speech_channel.dart— TTS 通道
鸿蒙侧(原生插件):
-
app/ohos/entry/src/main/ets/plugins/SpeechRecognitionPlugin.ets— 鸿蒙语音识别插件 -
app/ohos/entry/src/main/ets/plugins/TextToSpeechPlugin.ets— 鸿蒙 TTS 插件
路由入口:
-
app/lib/app.dart
同时它还会从别的页面被带起,比如:
-
搜索页把 query 带到
/ai-assistant -
菜品详情页带着"推荐类似吃法"的问题进来
-
探索页有 AI 入口卡片
这说明当前 AI 助手不是单点功能,而是已经进入了应用路由和页面流转主线。
核心实现
先说结论:
食界探味的 AI 助手不是"页面里直接调模型",而是"Flutter 页面 + 协调器 + AgentService + 工具调用 + 鸿蒙原生语音能力 + 菜品卡片渲染"这一整条组合链。
一、鸿蒙侧:原生能力是怎么暴露给 Flutter 的
在讲 Flutter 页面之前,先把鸿蒙侧的基础打好。AI 助手最依赖的两个鸿蒙原生能力是语音识别和文本转语音,它们都来自鸿蒙的 @kit.CoreSpeechKit。
1.1 语音识别插件(SpeechRecognitionPlugin.ets)
鸿蒙侧的语音识别插件实现了 FlutterPlugin 接口,通过 MethodChannel 与 Flutter 通信:
// app/ohos/entry/src/main/ets/plugins/SpeechRecognitionPlugin.ets
import { speechRecognizer } from '@kit.CoreSpeechKit';
import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';
export default class SpeechRecognitionPlugin implements FlutterPlugin, MethodCallHandler {
private channel: MethodChannel | null = null;
private asrEngine: speechRecognizer.SpeechRecognitionEngine | null = null;
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(
binding.getBinaryMessenger(),
'com.foodvoyage.speech_recognition'
);
this.channel.setMethodCallHandler(this);
}
关键流程:
-
权限申请 — 鸿蒙必须先动态申请麦克风权限:
private async requestMicrophonePermission(): Promise<boolean> {
const atManager = abilityAccessCtrl.createAtManager();
const permissions: Permissions[] = ['ohos.permission.MICROPHONE'];
const context = getContext(this);
const grantResult = await atManager.requestPermissionsFromUser(context, permissions);
return grantResult.authResults.every(
status => status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
);
}
-
创建识别引擎 — 使用在线模式,语言设为
zh-CN:
private createEngine(): Promise<void> {
return new Promise((resolve, reject) => {
const initParams: speechRecognizer.CreateEngineParams = {
language: 'zh-CN',
online: 1,
extraParams: { 'locate': 'CN', 'recognizerMode': 'short' }
};
speechRecognizer.createEngine(initParams, (err, engine) => {
if (!err) {
this.asrEngine = engine;
resolve();
} else {
reject(err);
}
});
});
}
-
开始识别 — 设置监听器后发起识别:
private startListening(): void {
if (!this.asrEngine) return;
const audioParam: speechRecognizer.AudioInfo = {
audioType: 'pcm',
sampleRate: 16000,
soundChannel: 1,
sampleBit: 16
};
const recognizerParams: speechRecognizer.StartParams = {
sessionId: this.sessionId,
audioInfo: audioParam,
extraParams: {
'recognitionMode': 0,
'vadBegin': 2000,
'vadEnd': 3000,
'maxAudioDuration': 20000
}
};
this.asrEngine.startListening(recognizerParams);
}
-
结果回传 — 识别到最终结果时,通过 MethodChannel 把文本传回 Flutter:
onResult: (sessionId, result) => {
if (result.isLast && this.pendingResult) {
this.pendingResult.success(result.result); // 回传给 Flutter
this.pendingResult = null;
this.shutdownEngine();
}
}
-
引擎清理 — 识别完成后立即 shutdown,避免资源泄漏:
private shutdownEngine(): void {
if (this.asrEngine) {
this.asrEngine.shutdown();
this.asrEngine = null;
}
}
1.2 TTS 插件(TextToSpeechPlugin.ets)
TTS 插件同样实现了 FlutterPlugin,使用鸿蒙的 textToSpeech 能力:
// app/ohos/entry/src/main/ets/plugins/TextToSpeechPlugin.ets
import { textToSpeech } from '@kit.CoreSpeechKit';
export default class TextToSpeechPlugin implements FlutterPlugin, MethodCallHandler {
private ttsEngine: textToSpeech.TextToSpeechEngine | null = null;
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(
binding.getBinaryMessenger(),
'com.foodvoyage.text_to_speech'
);
this.channel.setMethodCallHandler(this);
}
TTS 的创建和播报:
private createEngine(): Promise<void> {
return new Promise((resolve, reject) => {
if (this.ttsEngine) { resolve(); return; }
const initParams: textToSpeech.CreateEngineParams = {
language: 'zh-CN',
person: 0,
online: 1,
extraParams: {
'style': 'interaction-broadcast',
'locate': 'CN',
'name': 'EngineName'
}
};
textToSpeech.createEngine(initParams, (err, engine) => {
if (!err) {
this.ttsEngine = engine;
resolve();
} else {
reject(err);
}
});
});
}
播报时设置回调监听,播报完成后回传结果给 Flutter:
private setupListenerAndSpeak(text: string): void {
if (!this.ttsEngine) return;
const speakListener: textToSpeech.SpeakListener = {
onStart: (requestId, response) => { /* 开始 */ },
onComplete: (requestId, response) => {
if (this.pendingResult) {
this.pendingResult.success(null); // 通知 Flutter 播报完成
this.pendingResult = null;
}
},
onStop: (requestId, response) => { /* 停止 */ },
onData: (requestId, audio, response) => { /* 音频数据 */ },
onError: (requestId, errorCode, errorMessage) => {
if (this.pendingResult) {
this.pendingResult.error('TTS_ERROR', errorMessage, null);
this.pendingResult = null;
}
}
};
this.ttsEngine.setListener(speakListener);
this.ttsEngine.speak(text, {
requestId: `tts_${Date.now()}`,
extraParams: {
'queueMode': 0,
'speed': 1,
'volume': 2,
'pitch': 1,
'languageContext': 'zh-CN',
'audioType': 'pcm',
'soundChannel': 3,
'playType': 1
}
});
}
1.3 Flutter 侧的通道封装
鸿蒙原生插件注册好之后,Flutter 侧只需要通过 MethodChannel 调用即可:
// app/lib/core/platform/speech_recognition_channel.dart
class SpeechRecognitionChannel {
static const _channel = MethodChannel('com.foodvoyage.speech_recognition');
static Future<String> startListening({String language = 'zh-CN'}) async {
final result = await _channel.invokeMethod<String>(
'startListening',
{'language': language},
);
return result ?? '';
}
static Future<void> stopListening() async {
await _channel.invokeMethod<void>('stopListening');
}
}
// app/lib/core/platform/text_to_speech_channel.dart
class TextToSpeechChannel {
static const _channel = MethodChannel('com.foodvoyage.text_to_speech');
static Future<void> speak(String text) async {
await _channel.invokeMethod<void>('speak', {'text': text});
}
static Future<void> stop() async {
await _channel.invokeMethod<void>('stop');
}
}
这里的关键设计是:Flutter 侧的 Channel 封装是平台无关的。同一套 SpeechRecognitionChannel / TextToSpeechChannel 在鸿蒙端走鸿蒙的 CoreSpeechKit,在 Android 端走 Android SpeechRecognizer,在 iOS 端走 AVSpeechSynthesizer。AI 页面完全不感知底层平台差异。
二、路由层先给 AI 助手一个正式入口
在 app/lib/app.dart 里,当前已经有:
GoRoute(
path: '/ai-assistant',
redirect: (context, state) {
if (!AppConfig.enableAi) {
return '/explore';
}
return null;
},
builder: (context, state) {
final query = state.uri.queryParameters['q'];
return AiAssistantScreen(initialQuery: query);
},
),
这一步非常关键。因为它说明 AI 助手在产品里不是弹窗式实验能力,而是正式页面入口。更重要的是,它还支持 initialQuery 参数,意味着 AI 助手页面不是只能靠用户手打输入开始,而是可以承接外部页面传来的问题。
三、多入口如何把问题带进 AI 助手
食界探味的 AI 助手不是只有一个入口,而是从多个页面都能自然地进入:
搜索页:当搜索无结果时,引导用户用 AI 探索
// app/lib/features/search/screens/search_screen.dart
if (AppConfig.enableAi && _looksLikeNaturalLanguage(state.query))
_AiSearchHint(
query: state.query,
onTap: () => context.push(
'/ai-assistant?q=${Uri.encodeComponent(state.query)}',
),
),
这里有个智能判断:_looksLikeNaturalLanguage 检查搜索词是否像自然语言(而不是食材关键词)。如果是"今晚想吃点辣的"这类话,搜索页会提示"试试 AI 探味助手",点击后带着原始 query 跳转到 AI 助手。
菜品详情页:带着"推荐类似吃法"的问题进来
// app/lib/features/dish_detail/screens/dish_detail_screen.dart
onSimilar: () => context.push(
'/ai-assistant?q=${Uri.encodeComponent(
"帮我推荐和${dish.name}类似的${dish.ingredientName}吃法"
)}',
),
用户在看一道菜时,可以点击"类似吃法"按钮,系统会自动构造一个精准的问题带进 AI 助手。这种"带着上下文进入 AI"的体验比"空聊"好得多。
探索页:AI 入口卡片
// app/lib/features/explore/screens/explore_screen.dart
onTap: () => context.push('/ai-assistant'),
探索页有一个 AI 入口卡片,点击后直接进入 AI 助手页面。
四、页面层本身负责的是"对话体验容器"
回到 ai_assistant_screen.dart,你会发现它真正承担的是页面体验组织,而不是 AI 推理本身。
这个页面主要做了几件事:
class _AiAssistantScreenState extends ConsumerState<AiAssistantScreen> {
final List<_ChatEntry> _history = []; // 对话历史
String? _lastStreamingText; // 流式输出缓冲
bool _isSpeaking = false; // 语音播报状态
// 提交问题 → 委托给协调器
void _handleSubmit(String text) {
setState(() {
_history.add(_ChatEntry(isUser: true, text: text));
_lastStreamingText = null;
});
ref.read(aiExploreCoordinatorProvider.notifier).submitQuery(text);
}
页面上真正的组件层也已经拆出来了:
-
AiInputBar— 底部输入栏(文本 + 语音按钮) -
AiMessageBubble— 消息气泡(支持流式动画) -
AiDishCardList— 菜品卡片列表 -
AiDishCard— 单个菜品卡片(可点击跳转详情)
这说明它不是一个把所有逻辑塞进单文件的聊天页,而是一个已经开始组件化的 AI 对话页面。
页面还有一个重要的初始 query 处理逻辑:
// 当有初始查询时自动提交
if (widget.initialQuery != null &&
widget.initialQuery!.isNotEmpty &&
!_hasSubmittedInitial) {
_hasSubmittedInitial = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
_handleSubmit(widget.initialQuery!);
});
}
这段代码让 AI 助手页面从其他页面带着问题进来时,能够自动开始回答,而不是等用户再手动输入一次。
五、AI 页面不是直接调模型,而是先调协调器
在这套结构里,页面真正依赖的是:
final sessionState = ref.watch(aiExploreCoordinatorProvider);
final coordinator = ref.read(aiExploreCoordinatorProvider.notifier);
也就是说,页面层提交问题时,走的是:
-
_handleSubmit(text) -
coordinator.submitQuery(text)
而不是页面里直接写模型调用、工具执行、状态机处理。这一步的价值很大,因为它把页面展示职责和 AI 流程编排职责分开了。
六、协调器才是 AI 助手的真正工作台
app/lib/core/ai/ai_explore_coordinator.dart 是这套 AI 页面真正的主心骨。
它负责的事情包括:
class AiExploreCoordinator extends StateNotifier<AiSessionState> {
// 初始化 Agent(带真实业务工具)
void _ensureAgent() {
_agentService.createAgent(
systemPrompt: _systemPrompt,
tools: [
SearchDishesTool(_foodRepository, onDishesFound: _onDishesFound),
GetDishDetailTool(_foodRepository),
GetRandomDishTool(_foodRepository, onDishesFound: _onDishesFound),
GetDishesByIngredientTool(_foodRepository, onDishesFound: _onDishesFound),
],
enableAutoToolExecution: true,
maxMessages: 30,
);
}
协调器把 4 个业务工具注册到了 Agent 里:
|
工具 |
功能 |
|---|---|
|
|
根据关键词搜索菜品 |
|
|
获取某道菜的详细信息 |
|
|
随机推荐一道菜 |
|
|
按食材查同食材的其他吃法 |
协调器的流式对话处理:
Future<void> submitQuery(String text) async {
state = state.copyWith(
status: AiSessionStatus.parsing,
inputText: text,
streamingText: '',
);
_ensureAgent();
final buffer = StringBuffer();
await _agentService.chatWithToolsStream(
message: text,
onContent: (chunk) {
buffer.write(chunk);
state = state.copyWith(
status: AiSessionStatus.responding,
streamingText: buffer.toString(),
);
},
onToolCall: (toolCall) {
state = state.copyWith(status: AiSessionStatus.searching);
},
onComplete: (full) {
state = state.copyWith(
status: AiSessionStatus.idle,
streamingText: full,
);
},
);
}
协调器还直接接上了鸿蒙的语音能力:
// 语音输入 → 走鸿蒙 SpeechRecognitionPlugin
Future<void> startVoiceInput() async {
state = state.copyWith(status: AiSessionStatus.listening);
final text = await SpeechRecognitionChannel.startListening();
if (text.isNotEmpty) {
await submitQuery(text);
}
}
// TTS 播报 → 走鸿蒙 TextToSpeechPlugin(自动清理 Markdown 格式)
Future<void> speakText(String text) async {
final cleaned = _stripForTts(text);
await TextToSpeechChannel.speak(cleaned);
}
这里有个细节:_stripForTts 函数会清理 AI 回复中的 Markdown 格式、emoji、表格符号等,确保鸿蒙 TTS 引擎播报出来的声音是干净的自然语言。
如果只看页面,你会误以为这是一个"聊天 UI"。但只要看到协调器,就会发现它其实是:AI 交互状态机 + 工具调用编排器 + 语音输入输出协调器。
七、AgentService 负责把模型能力收成统一接口
再往下走,AgentService 负责的是更底层的模型交互:
class AgentService {
AIAgent? _currentAgent;
AIAgent createAgent({
required String systemPrompt,
List<Tool>? tools,
int maxMessages = 50,
bool enableAutoToolExecution = false,
}) {
final agent = AIAgent(
provider: _provider,
config: AIAgentConfig(
systemPrompt: systemPrompt,
enableAutoToolExecution: enableAutoToolExecution,
),
memoryManager: ConversationMemory(maxMessages: maxMessages),
);
if (tools != null) {
for (final tool in tools) {
agent.addTool(tool);
}
}
_currentAgent = agent;
return agent;
}
也就是说,页面不直接碰 Provider,协调器也不直接碰底层 Provider。中间先由 AgentService 把 agent 创建、memoryManager、工具注册、流式输出这些底层能力统一收了起来。
八、AI 助手真正的差异化在于"文本 + 菜品卡片"一起出现
这套页面结构特别适合食界探味的原因,不只是它能对话,而是它没有把 AI 回复只做成一段文字。
在协调器里,工具调用拿到的菜品结果会通过回调更新到状态里:
void _onDishesFound(List<Dish> dishes) {
state = state.copyWith(matchedDishes: dishes);
}
页面层最终通过 AiDishCardList 把这些结果变成真正可点击的菜品卡片。这意味着 AI 在这里承担的并不是"替代页面",而是:生成推荐语义 + 驱动已有业务卡片展示。这比纯聊天页稳得多。
九、语音输入输出为什么也能无缝嵌进页面
这一点也很关键。
协调器已经直接接上了鸿蒙的语音能力:
-
SpeechRecognitionChannel.startListening()→ 鸿蒙speechRecognizer -
TextToSpeechChannel.speak(...)→ 鸿蒙textToSpeech
所以页面层最终能得到的是:
AiInputBar(
onSubmit: _handleSubmit,
onVoiceStart: () => coordinator.startVoiceInput(),
onVoiceEnd: () => coordinator.stopVoiceInput(),
isListening: sessionState.status == AiSessionStatus.listening,
)
输入栏的语音按钮采用"按住说话"交互:按下时触发 startVoiceInput(),松开时触发 stopVoiceInput()。鸿蒙侧会自动处理 VAD(语音端点检测),用户停止说话后自动结束识别。
语音播报方面,页面层通过 _toggleSpeak 控制:
void _toggleSpeak(String text) async {
if (_isSpeaking) {
await TextToSpeechChannel.stop(); // 鸿蒙 TTS 停止
setState(() => _isSpeaking = false);
} else {
setState(() => _isSpeaking = true);
await TextToSpeechChannel.speak(text); // 鸿蒙 TTS 播报
}
}
更重要的是,页面退出时会自动停止 TTS:
@override
void dispose() {
if (_isSpeaking) {
TextToSpeechChannel.stop().catchError((_) {});
}
super.dispose();
}
这保证了用户退出 AI 页面后不会出现"声音还在后台播"的问题。
状态机:AI 会话的完整生命周期
食界探味的 AI 助手使用了一套清晰的状态机:
enum AiSessionStatus {
idle, // 空闲
listening, // 正在语音识别
parsing, // 正在理解用户意图
searching, // 正在搜索菜品(工具调用中)
responding, // 正在流式生成回复
speaking, // 正在 TTS 播报
error, // 出错
}
状态流转:
idle → listening(用户按住语音按钮)
listening → parsing(语音识别完成,文本提交)
parsing → searching(模型决定调用工具)
searching → responding(工具结果返回,模型生成回复)
responding → idle(回复完成)
idle → speaking(用户点击播报按钮)
speaking → idle(播报完成或手动停止)
任何状态 → error(出错)
error → parsing(用户点击重试)
页面根据当前状态展示不同的 UI:
switch (sessionState.status) {
case AiSessionStatus.listening:
return AiMessageBubble(text: '正在聆听...', isStreaming: true);
case AiSessionStatus.parsing:
return AiMessageBubble(text: '正在理解你的需求...', isStreaming: true);
case AiSessionStatus.searching:
return AiMessageBubble(text: '正在探索全球美食...', isStreaming: true);
case AiSessionStatus.responding:
return AiMessageBubble(text: sessionState.streamingText, isStreaming: true);
default:
return SizedBox.shrink();
}
关键代码位置
|
文件 |
作用 |
|---|---|
|
|
路由配置,AI 助手入口 |
|
|
AI 对话页面 |
|
|
底部输入栏(文本+语音) |
|
|
消息气泡 |
|
|
菜品卡片列表 |
|
|
AI 流程编排协调器 |
|
|
模型调用统一封装 |
|
|
会话状态模型 |
|
|
工具层 |
|
|
语音识别通道 |
|
|
TTS 通道 |
|
|
鸿蒙语音识别插件 |
|
|
鸿蒙 TTS 插件 |
鸿蒙侧与 Flutter 侧的协作关系
从整体架构看,AI 助手的双端协作可以这样理解:
┌─────────────────────────────────────────────────┐
│ Flutter 侧 │
│ │
│ AiAssistantScreen (页面层) │
│ │ │
│ ▼ │
│ AiExploreCoordinator (协调器) │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ AgentService SpeechChannel TtsChannel │
│ │ │ │ │
│ ▼ │ │ │
│ 工具层/模型层 │ │ │
│ │ │ │
├──────────────────┼───────────┼────────────────────┤
│ MethodChannel │ │
│ │ │ │
├──────────────────┼───────────┼────────────────────┤
│ 鸿蒙侧 │
│ │ │ │
│ SpeechRecognitionPlugin TextToSpeechPlugin │
│ │ │ │
│ CoreSpeechKit CoreSpeechKit │
└─────────────────────────────────────────────────┘
核心要点:
-
AI 推理完全在 Flutter 侧完成 — 通过 AgentService 调用云端模型,鸿蒙侧不参与 AI 推理
-
语音能力完全由鸿蒙侧提供 — 通过 CoreSpeechKit 的 speechRecognizer 和 textToSpeech
-
Flutter 侧只看到统一的 Channel 接口 — 不感知底层是鸿蒙还是 Android
-
鸿蒙插件需要管理引擎生命周期 — 创建、使用、shutdown,避免资源泄漏
-
鸿蒙插件需要处理权限 — 麦克风权限必须在鸿蒙侧动态申请
常见坑
-
页面里直接调模型,导致 UI、状态和工具调用混成一团 → 一定要抽出协调器
-
AI 回复只做文本,不接业务卡片 → 利用工具回调把菜品数据带回页面层
-
从别的页面进 AI 助手时,没有设计初始 query 入口 → 用
initialQuery参数承接上下文 -
页面退出时不处理 TTS 停止,导致体验很差 → 在
dispose()里调TextToSpeechChannel.stop() -
鸿蒙引擎不 shutdown,导致内存泄漏 → 每次识别/TTS 完成后必须调
shutdown() -
鸿蒙麦克风权限未申请,导致语音识别直接失败 → 在
startListening前先requestMicrophonePermission -
TTS 播报时把 Markdown 格式一起读出来 → 协调器里
_stripForTts先清理再播报
可复用模板
如果你要在自己的鸿蒙 + Flutter 项目里做类似的 AI 助手嵌入,可以参考这个结构:
页面层(AiAssistantScreen)
├─ 输入栏(文本 + 语音按钮)
├─ 消息列表(用户消息 + AI 回复 + 菜品卡片)
└─ 错误提示条
协调器(AiExploreCoordinator)
├─ 状态机(idle → parsing → searching → responding → idle)
├─ Agent 管理(创建、重置、工具注册)
├─ 流式对话(chatWithToolsStream)
├─ 语音输入(startVoiceInput → SpeechRecognitionChannel)
└─ TTS 播报(speakText → TextToSpeechChannel)
模型服务(AgentService)
├─ createAgent(systemPrompt + tools)
├─ chatWithToolsStream(流式 + 工具回调)
└─ chatWithTools(非流式)
工具层(Tools)
├─ SearchDishesTool
├─ GetDishDetailTool
├─ GetRandomDishTool
└─ GetDishesByIngredientTool
鸿蒙原生插件
├─ SpeechRecognitionPlugin(CoreSpeechKit speechRecognizer)
└─ TextToSpeechPlugin(CoreSpeechKit textToSpeech)
Flutter 侧协调器的 Riverpod Provider 模板:
final aiExploreCoordinatorProvider =
StateNotifierProvider.autoDispose<AiExploreCoordinator, AiSessionState>(
(ref) {
final agentService = ref.watch(agentServiceProvider);
final foodRepo = ref.watch(foodRepositoryProvider);
return AiExploreCoordinator(
agentService: agentService,
foodRepository: foodRepo,
);
});
页面中使用协调器的模板:
final sessionState = ref.watch(aiExploreCoordinatorProvider);
final coordinator = ref.read(aiExploreCoordinatorProvider.notifier);
// 提交问题
coordinator.submitQuery(text);
// 语音输入
coordinator.startVoiceInput();
// TTS 播报
coordinator.speakText(text);
// 重置会话
coordinator.reset();
本篇总结
食界探味的 AI 助手之所以能自然嵌进鸿蒙 + Flutter 页面里,关键不在"有一个聊天页面",而在于它已经形成了:
-
鸿蒙原生能力层 — 语音识别和 TTS 通过 CoreSpeechKit 提供,经由 MethodChannel 暴露给 Flutter
-
正式路由入口 — 支持
initialQuery,可从搜索页、详情页、探索页多入口进入 -
协调器状态层 — 管理 AI 会话生命周期、工具调用编排、语音输入输出
-
模型服务层 — AgentService 统一封装模型调用细节
-
工具调用层 — 4 个业务工具让 AI 能检索真实菜品数据
-
业务卡片回填层 — AI 推荐直接驱动菜品卡片展示,不只是一段文字
这套结构让 AI 助手不再是孤立演示页,而是真正进入了应用主体验链路——在鸿蒙设备上,用户可以用语音和 AI 聊美食,AI 推荐的菜品卡片可以直接点击查看详情,整个过程流畅自然,鸿蒙原生能力和 Flutter UI 无缝协作。
更多推荐





所有评论(0)