SpeechRecognitionChannel 的 Flutter 侧封装思路

适合谁看

  • 想先看 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 插件层处理。

相关推荐
风满城331 小时前
鸿蒙原生应用实战(二):首页开发 —— Grid分类网格与热歌排行榜
harmonyos
UnicornDev1 小时前
【Flutter x HarmonyOS 6】设置页面的逻辑实现
flutter·华为·harmonyos
Swift社区1 小时前
鸿蒙游戏动画系统:架构解析 + Demo实现
游戏·华为·harmonyos
AI_零食2 小时前
HarmonyOS ArkTS 类型转换机制深度解析
学习·华为·harmonyos·鸿蒙
BreezeDove2 小时前
【Android】AndroidStudio+Flutter开发建议环境变量
android·flutter
金启攻2 小时前
鸿蒙原生应用实战(四):我的追剧与统计页 —— 三态Tab与数据可视化
华为·harmonyos
AI_零食2 小时前
HarmonyOS ArkTS 数据格式化技术深度解析
学习·华为·harmonyos·鸿蒙
互联网散修2 小时前
鸿蒙实战:图片编辑器——像素马赛克从卡顿到丝滑的终极优化
华为·编辑器·harmonyos