适合谁看想先看 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 侧到底暴露了什么现在这层封装非常克制只暴露startListeningstopListening返回值也很明确startListening返回FutureString从页面角度看这个语义其实很自然我要开始识别识别结束后给我一段文本如果需要我也能主动停止这已经足够支撑大多数鸿蒙语音输入场景。二、为什么返回值先收成字符串是合理的很多人第一次设计语音识别边界时会很容易想把 HarmonyOS 原生复杂度完整暴露出来比如中间态引擎状态会话 ID各类错误码识别阶段事件但对当前页面需求来说页面层真正最关心的往往只是最终识别出来的文本是什么所以现在的做法是Flutter 侧把返回值先收成String没结果时返回空字符串这个设计的价值在于它优先保住了页面层的简单性。后面如果真的要加更细粒度的状态再在边界层扩展也来得及。从鸿蒙教程写作角度看这样的第一版接口也更稳因为页面语义先站稳原生复杂度先不外溢后续扩展不会一开始就把 API 设计搞重三、为什么权限和引擎复杂度不该留在 Flutter 层回头看SpeechRecognitionPlugin.etsHarmonyOS 原生层真正做的事情其实很多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 FutureString 完成只要这条链路先建立清楚后面无论你是改 Flutter 侧封装还是改鸿蒙原生插件都会更知道自己在改哪一层。七、现在这层封装最适合什么样的鸿蒙页面当前这种最小封装特别适合下面这类页面搜索输入页AI 助手输入页表单语音填充页“点一下说一句”式的轻量语音入口因为这类页面最需要的就是发起一次识别拿到一段最终文本失败了就兜底成普通输入它们并不一定需要从一开始就理解鸿蒙语音引擎是否在线中间识别片段是否持续回推更细的原生状态机八、后面如果要扩展应该往哪扩当前设计最好的地方之一是它还能平稳扩展。如果未来这个鸿蒙 Flutter 项目真的需要更复杂的识别体验比如返回中间结果增加更多语言选项区分主动停止和自然结束支持更细的状态提示最合理的扩展位置依然应该先落在SpeechRecognitionChannel原生插件的返回模型而不是直接把原生细节一股脑冲进页面层。换句话说现在这层封装不是“太简单”而是“起点够干净”。九、什么时候说明这层 Flutter 封装已经该重构了如果后面开始出现下面这些信号就说明这层边界可能需要升级页面开始关心越来越多原生错误码startListening的参数越来越像万能配置对象只返回最终字符串已经撑不住真实交互页面不得不自己判断当前是不是识别中这时候需要重构的不是页面而是边界层本身。也就是说边界层应该继续演化但依然不该把鸿蒙原生复杂度直接倾倒给页面层。关键代码位置app/lib/core/platform/speech_recognition_channel.dartapp/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 FutureString startListening({String language zh-CN}) async { final result await _channel.invokeMethodString(startListening, { language: language, }); return result ?? ; } static Futurevoid stopListening() async { await _channel.invokeMethodvoid(stopListening); } }边界层思路 页面只表达 - 开始识别 - 停止识别 - 拿最终文本 鸿蒙原生层负责 - 权限 - 引擎 - 回调 - 释放本篇总结SpeechRecognitionChannel的 Flutter 侧封装思路重点不在“写了多少代码”而在“收掉了多少本来会泄漏进页面层的复杂度”。当前这层设计之所以稳是因为它先把鸿蒙语音识别收成了一个清楚的输入能力再把权限、引擎和回调复杂度留在 ArkTS 插件层处理。
SpeechRecognitionChannel 的 Flutter 侧封装思路
发布时间:2026/6/11 4:56:14
适合谁看想先看 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 侧到底暴露了什么现在这层封装非常克制只暴露startListeningstopListening返回值也很明确startListening返回FutureString从页面角度看这个语义其实很自然我要开始识别识别结束后给我一段文本如果需要我也能主动停止这已经足够支撑大多数鸿蒙语音输入场景。二、为什么返回值先收成字符串是合理的很多人第一次设计语音识别边界时会很容易想把 HarmonyOS 原生复杂度完整暴露出来比如中间态引擎状态会话 ID各类错误码识别阶段事件但对当前页面需求来说页面层真正最关心的往往只是最终识别出来的文本是什么所以现在的做法是Flutter 侧把返回值先收成String没结果时返回空字符串这个设计的价值在于它优先保住了页面层的简单性。后面如果真的要加更细粒度的状态再在边界层扩展也来得及。从鸿蒙教程写作角度看这样的第一版接口也更稳因为页面语义先站稳原生复杂度先不外溢后续扩展不会一开始就把 API 设计搞重三、为什么权限和引擎复杂度不该留在 Flutter 层回头看SpeechRecognitionPlugin.etsHarmonyOS 原生层真正做的事情其实很多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 FutureString 完成只要这条链路先建立清楚后面无论你是改 Flutter 侧封装还是改鸿蒙原生插件都会更知道自己在改哪一层。七、现在这层封装最适合什么样的鸿蒙页面当前这种最小封装特别适合下面这类页面搜索输入页AI 助手输入页表单语音填充页“点一下说一句”式的轻量语音入口因为这类页面最需要的就是发起一次识别拿到一段最终文本失败了就兜底成普通输入它们并不一定需要从一开始就理解鸿蒙语音引擎是否在线中间识别片段是否持续回推更细的原生状态机八、后面如果要扩展应该往哪扩当前设计最好的地方之一是它还能平稳扩展。如果未来这个鸿蒙 Flutter 项目真的需要更复杂的识别体验比如返回中间结果增加更多语言选项区分主动停止和自然结束支持更细的状态提示最合理的扩展位置依然应该先落在SpeechRecognitionChannel原生插件的返回模型而不是直接把原生细节一股脑冲进页面层。换句话说现在这层封装不是“太简单”而是“起点够干净”。九、什么时候说明这层 Flutter 封装已经该重构了如果后面开始出现下面这些信号就说明这层边界可能需要升级页面开始关心越来越多原生错误码startListening的参数越来越像万能配置对象只返回最终字符串已经撑不住真实交互页面不得不自己判断当前是不是识别中这时候需要重构的不是页面而是边界层本身。也就是说边界层应该继续演化但依然不该把鸿蒙原生复杂度直接倾倒给页面层。关键代码位置app/lib/core/platform/speech_recognition_channel.dartapp/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 FutureString startListening({String language zh-CN}) async { final result await _channel.invokeMethodString(startListening, { language: language, }); return result ?? ; } static Futurevoid stopListening() async { await _channel.invokeMethodvoid(stopListening); } }边界层思路 页面只表达 - 开始识别 - 停止识别 - 拿最终文本 鸿蒙原生层负责 - 权限 - 引擎 - 回调 - 释放本篇总结SpeechRecognitionChannel的 Flutter 侧封装思路重点不在“写了多少代码”而在“收掉了多少本来会泄漏进页面层的复杂度”。当前这层设计之所以稳是因为它先把鸿蒙语音识别收成了一个清楚的输入能力再把权限、引擎和回调复杂度留在 ArkTS 插件层处理。