适合谁看
-
想先看 Flutter 侧封装而不是原生实现的人
-
正在设计鸿蒙语音识别边界 API 的人
-
想把页面层保持干净的人
问题背景
鸿蒙语音识别能力的原生复杂度其实不低。
以当前这个 HarmonyOS 插件实现为例,原生层已经要负责:
-
麦克风权限申请
-
识别引擎创建
-
监听器注册
-
会话结束和错误处理
-
引擎释放
如果 Flutter 侧封装不主动收口,页面层就会逐渐开始知道:
-
权限失败是什么
-
引擎什么时候初始化
-
原生回调什么时候算结束
-
鸿蒙语音识别会话什么时候被系统主动收掉
这类信息一旦漏进页面层,后面语音识别就不再像一个"输入能力",而更像页面里混进了一半 HarmonyOS 原生逻辑。
项目中的真实场景
当前这个鸿蒙 Flutter 项目的 Flutter 侧语音识别边界非常轻:
app/lib/core/platform/speech_recognition_channel.dart
里面对外暴露的核心方法只有两个:
-
startListening({String language = 'zh-CN'}) -
stopListening()
对应的鸿蒙原生插件则在:
app/ohos/entry/src/main/ets/plugins/SpeechRecognitionPlugin.ets
这组实现很适合拿来说明一个问题:
Flutter 侧边界层不该镜像原生复杂度,而应该先把页面真正需要的语义收出来。
核心实现
先说结论:
SpeechRecognitionChannel当前最大的优点,不是功能很多,而是它先把"页面要什么"和"鸿蒙原生到底有多复杂"切开了。
一、当前 Flutter 侧到底暴露了什么
现在这层封装非常克制,只暴露:
-
startListening -
stopListening
返回值也很明确:
startListening返回Future<String>
从页面角度看,这个语义其实很自然:
-
我要开始识别
-
识别结束后给我一段文本
-
如果需要,我也能主动停止
这已经足够支撑大多数鸿蒙语音输入场景。
二、为什么返回值先收成字符串是合理的
很多人第一次设计语音识别边界时,会很容易想把 HarmonyOS 原生复杂度完整暴露出来,比如:
-
中间态
-
引擎状态
-
会话 ID
-
各类错误码
-
识别阶段事件
但对当前页面需求来说,页面层真正最关心的往往只是:
- 最终识别出来的文本是什么
所以现在的做法是:
-
Flutter 侧把返回值先收成
String -
没结果时返回空字符串
这个设计的价值在于,它优先保住了页面层的简单性。
后面如果真的要加更细粒度的状态,再在边界层扩展也来得及。
从鸿蒙教程写作角度看,这样的第一版接口也更稳,因为:
-
页面语义先站稳
-
原生复杂度先不外溢
-
后续扩展不会一开始就把 API 设计搞重
三、为什么权限和引擎复杂度不该留在 Flutter 层
回头看 SpeechRecognitionPlugin.ets,HarmonyOS 原生层真正做的事情其实很多:
-
requestMicrophonePermission() -
createEngine() -
setupListener() -
startListening() -
shutdownEngine()
这些逻辑如果直接反映到 Flutter 边界 API,页面层就会被迫理解:
-
什么时候申请权限
-
引擎什么时候创建
-
哪个回调才算真正结束
-
鸿蒙权限被拒绝时页面到底应该怎么分流
而现在这层封装把这些细节都挡在原生侧了。
页面层只需要理解:
- 我要拿一段语音输入
这就是边界层最有价值的地方。
四、为什么 stopListening() 依然要单独保留
有人会觉得,如果 startListening() 最终会返回文本,那是不是不用单独 stopListening() 也行。
但从交互层看,这两个语义并不一样:
-
startListening()代表开始一次输入会话 -
stopListening()代表用户主动中断或提前结束
在鸿蒙原生插件里,你也能看到这种区分:
-
startListening负责启动识别会话 -
stopListening通过finish(this.sessionId)结束当前识别
所以保留两个独立方法,本质上是在保护页面交互语义,而不是为了和原生方法名保持一一镜像。
五、这层 Flutter 封装真正承担了什么职责
别看 SpeechRecognitionChannel 文件不长,它其实已经在承担边界层最核心的几件事:
-
定义鸿蒙语音识别这项能力的通道名
-
定义页面层可调用的方法语义
-
定义 Flutter 侧先消费什么结果类型
-
把 HarmonyOS 原生插件复杂度挡在页面层外面
它没有做的事情也很重要:
-
不负责权限流程
-
不负责原生引擎生命周期
-
不负责识别回调细节
-
不负责把 ArkTS 监听器事件原样搬进 Flutter 页面
这说明它是一个"边界类",不是一个"半原生实现类"。
六、如果把这条链路从页面走到鸿蒙原生,顺序是怎样的
把这篇文章和当前项目代码对起来看,完整链路大致是这样:
Flutter 页面
-> SpeechRecognitionChannel.startListening(language)
-> MethodChannel('com.foodvoyage.speech_recognition').invokeMethod(...)
-> SpeechRecognitionPlugin.ets onMethodCall
-> 申请鸿蒙麦克风权限
-> 创建鸿蒙语音识别引擎
-> 监听最终结果 / 错误 / 完成事件
-> result.success(resultText) 或 result.error(...)
-> Flutter Future<String> 完成
只要这条链路先建立清楚,后面无论你是改 Flutter 侧封装,还是改鸿蒙原生插件,都会更知道自己在改哪一层。
七、现在这层封装最适合什么样的鸿蒙页面
当前这种最小封装特别适合下面这类页面:
-
搜索输入页
-
AI 助手输入页
-
表单语音填充页
-
"点一下说一句"式的轻量语音入口
因为这类页面最需要的就是:
-
发起一次识别
-
拿到一段最终文本
-
失败了就兜底成普通输入
它们并不一定需要从一开始就理解:
-
鸿蒙语音引擎是否在线
-
中间识别片段是否持续回推
-
更细的原生状态机
八、后面如果要扩展,应该往哪扩
当前设计最好的地方之一,是它还能平稳扩展。
如果未来这个鸿蒙 Flutter 项目真的需要更复杂的识别体验,比如:
-
返回中间结果
-
增加更多语言选项
-
区分主动停止和自然结束
-
支持更细的状态提示
最合理的扩展位置依然应该先落在:
-
SpeechRecognitionChannel -
原生插件的返回模型
而不是直接把原生细节一股脑冲进页面层。
换句话说,现在这层封装不是"太简单",而是"起点够干净"。
九、什么时候说明这层 Flutter 封装已经该重构了
如果后面开始出现下面这些信号,就说明这层边界可能需要升级:
-
页面开始关心越来越多原生错误码
-
startListening的参数越来越像万能配置对象 -
只返回最终字符串已经撑不住真实交互
-
页面不得不自己判断当前是不是识别中
这时候需要重构的不是页面,而是边界层本身。
也就是说,边界层应该继续演化,但依然不该把鸿蒙原生复杂度直接倾倒给页面层。
关键代码位置
-
app/lib/core/platform/speech_recognition_channel.dart -
app/ohos/entry/src/main/ets/plugins/SpeechRecognitionPlugin.ets
鸿蒙侧实现
从 HarmonyOS 原生侧看,语音识别插件负责的是复杂度本体:
-
权限申请
-
引擎创建
-
监听结果
-
错误处理
-
资源释放
这也是为什么 Flutter 侧封装可以保持很轻。
Flutter 侧实现
从 Flutter 侧看,这层封装的目标只有一个:
- 把鸿蒙语音识别收成页面能自然消费的输入能力
它不是去复制原生插件内部结构,而是去定义页面层真正应该看到的那部分语义。
常见坑
-
页面直接理解原生错误细节
-
一开始就把 API 设计得过重
-
还没形成真实使用场景,就把中间态和底层状态全部暴露出来
-
把
stopListening()理解成只是"顺手补一个方法",而不是独立交互语义 -
让 Flutter 页面知道太多鸿蒙权限和引擎细节
可复用模板
class SpeechRecognitionChannel {
static const _channel = MethodChannel('com.example.speech');
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');
}
}
边界层思路
页面只表达:
- 开始识别
- 停止识别
- 拿最终文本
鸿蒙原生层负责:
- 权限
- 引擎
- 回调
- 释放
本篇总结
SpeechRecognitionChannel 的 Flutter 侧封装思路,重点不在"写了多少代码",而在"收掉了多少本来会泄漏进页面层的复杂度"。
当前这层设计之所以稳,是因为它先把鸿蒙语音识别收成了一个清楚的输入能力,再把权限、引擎和回调复杂度留在 ArkTS 插件层处理。
