鸿蒙语音播报功能 的 Flutter 侧封装思路
适合谁看
-
正在给 Flutter 接鸿蒙 TTS 的开发者
-
想先从页面调用角度理解 TTS 封装的人
-
想保持平台边界清晰的人
问题背景
鸿蒙 TTS 最容易被低估的地方在于,它的表面动作太简单了:
-
传一段文字
-
播出来
但一旦你真的去看 HarmonyOS 原生侧实现,就会发现里面至少还藏着:
-
引擎创建
-
播报监听
-
停止逻辑
-
错误处理
-
引擎释放
如果 Flutter 侧不主动把这些复杂度收掉,页面层很快就会开始知道太多“播报系统是怎么工作的”细节。
这对内容型应用来说通常没有必要。
项目中的真实场景
当前这个鸿蒙 Flutter 项目的 Flutter 侧 TTS 边界在:
-
app/lib/core/platform/text_to_speech_channel.dart
对外暴露的方法只有:
-
speak(String text) -
stop()
对应的鸿蒙原生插件在:
-
app/ohos/entry/src/main/ets/plugins/TextToSpeechPlugin.ets
这组实现很适合拿来说明一个问题:
Flutter 页面真正需要的是“播报语义”,而不是“播报引擎结构”。
核心实现
先说结论:
TextToSpeechChannel当前的优点,不是它功能很多,而是它先把鸿蒙 TTS 页面最需要的播报动作收成了最小接口。
一、当前 Flutter 侧只暴露了两个动作
现在这层封装非常直接:
-
speak(text) -
stop()
从页面语义看,这两个动作已经覆盖了绝大多数内容型应用会遇到的第一阶段需求:
-
我有一段文本需要播报
-
如果用户不想听了,我要能停掉
这种收法的价值在于:
-
页面层不用先理解 HarmonyOS 播报状态机
-
页面调用点非常清楚
-
原生复杂度不会直接扩散到 Flutter 页面里
二、为什么 speak(String text) 不是“太简单”,而是“刚好”
很多人设计鸿蒙 TTS 边界 API 时,会下意识想一口气把下面这些都暴露出来:
-
音色
-
语速
-
音量
-
pitch
-
queue mode
但回到当前原生实现就会发现,这些参数虽然都存在于 HarmonyOS 原生层,比如:
-
speed -
volume -
pitch -
queueMode
可它们现在并不是页面层的核心需求。
页面真正要表达的,依然只是:
-
把这段文本播出来
所以当前 Flutter 封装没有急着把底层参数全放出来,而是先保留最小语义。
这是一种更稳的工程选择,而不是“偷懒”。
三、为什么 stop() 必须和 speak() 一样是一级方法
很多人会把停止播报当成一个附属能力,觉得:
-
先能播出来再说
但从真实页面交互看,停止播报和开始播报一样重要。
尤其在内容型应用里,用户很可能会:
-
播到一半想停
-
切换页面时需要中断
-
再次点击时需要覆盖当前播报
而 HarmonyOS 原生侧的 TextToSpeechPlugin.ets 里也明确保留了:
-
handleSpeak -
handleStop
这说明 TTS 在系统层本来就不是“只有开始,没有停止”的模型。
Flutter 侧把它平等暴露出来,是在保护交互语义完整性。
四、为什么 Flutter 页面不该直接理解引擎状态
回头看原生插件,你会看到里面有很多对页面层来说并不适合直接暴露的内容:
-
createEngine() -
setListener(speakListener) -
onStart -
onComplete -
onStop -
onError -
shutdownEngine()
这些东西都是真实存在的,也都很重要。
但它们的重要性主要属于:
-
HarmonyOS 原生实现层
-
Flutter 边界层内部设计
不是页面层本身该承担的认知负担。
页面层真正更关心的是:
-
现在要不要播
-
用户中断时要不要停
-
播报结束后页面要不要继续别的动作
所以 Flutter 边界层如果一开始就让页面直接感知太多原生状态,反而会让本来应该很清晰的播报动作变复杂。
五、为什么这层封装特别适合鸿蒙内容型应用起步
当前这个项目不是一个纯工具型应用,它更接近内容探索型场景。
在这种场景里,鸿蒙 TTS 的第一价值通常不是“展示声音技术”,而是:
-
帮助用户听内容
-
帮助页面补足另一种消费方式
所以当前封装把它收成:
-
speak -
stop
本质上是在优先服务真实产品语义,而不是在优先暴露 HarmonyOS 原生控制面板。
六、如果把这条链路从 Flutter 页面走到鸿蒙原生,顺序是怎样的
把这篇文章和当前代码对起来看,完整链路大致是这样:
Flutter 页面
-> TextToSpeechChannel.speak(text)
-> MethodChannel('com.foodvoyage.text_to_speech').invokeMethod(...)
-> TextToSpeechPlugin.ets onMethodCall
-> 创建鸿蒙 TTS 引擎
-> 注册播报监听器
-> 调用 speak
-> onComplete / onStop / onError
-> result.success(null) 或 result.error(...)
-> Flutter Future<void> 完成
只要这条链路先建立清楚,后面你再看页面侧封装,或者再看鸿蒙原生插件,就不会把两层职责混在一起。
七、以后如果要扩展,最自然的方向是什么
现在这层封装并不是终点,但它是一个很好的起点。
如果未来真的需要更细粒度控制,例如:
-
传入更多播报配置
-
增加播报状态监听
-
区分自然结束和主动停止
-
增加队列播报和覆盖策略
最自然的扩展位置应该仍然是:
-
先扩
TextToSpeechChannel -
再扩对应鸿蒙原生插件
而不是直接让页面层越过边界层去碰原生播报细节。
这也是当前最小封装最有价值的地方:
-
它没有把后续扩展堵死
-
但也没有过早把复杂度引进来
八、什么时候说明这层 Flutter 封装已经该重构了
如果后面开始出现下面这些信号,就说明这层边界可能需要升级:
-
页面开始关心越来越多 HarmonyOS 原生错误码
-
speak的参数越来越像万能配置对象 -
页面不得不自己判断当前是不是正在播报
-
不同页面开始各自维护一套播报控制策略
这时候需要重构的不是页面,而是边界层本身。
也就是说,边界层应该继续演化,但依然不该把 HarmonyOS 原生复杂度直接倾倒给页面层。
关键代码位置
-
app/lib/core/platform/text_to_speech_channel.dart -
app/ohos/entry/src/main/ets/plugins/TextToSpeechPlugin.ets
鸿蒙侧实现
从 HarmonyOS 原生侧看,TTS 的真实复杂度已经被插件层承接了:
-
引擎创建
-
监听器注册
-
播报完成与停止回调
-
错误处理
-
引擎释放
这正是 Flutter 侧可以保持轻量的前提。
Flutter 侧实现
从 Flutter 侧看,这层封装的目标很明确:
-
把鸿蒙 TTS 先收成页面能自然调用的播报能力
-
不让页面直接理解原生引擎生命周期
所以这不是“把原生简单包一层”,而是在主动做边界设计。
常见坑
-
页面直接持有太多原生播报细节
-
还没弄清语义就先做复杂状态机
-
一开始就把速度、音色、队列等底层参数全塞进 Flutter API
-
把
stop()当成次要能力,导致交互链路不完整 -
让 Flutter 页面知道太多 HarmonyOS 引擎细节
可复用模板
class TextToSpeechChannel {
static const _channel = MethodChannel('com.example.tts');
static Future<void> speak(String text) async {
await _channel.invokeMethod<void>('speak', {'text': text});
}
static Future<void> stop() async {
await _channel.invokeMethod<void>('stop');
}
}
页面只表达
- 播报这段文本
- 停止当前播报
鸿蒙原生层负责
- 引擎
- 回调
- 错误
- 释放
本篇总结
TextToSpeechChannel 的 Flutter 侧封装思路,重点不是把所有鸿蒙原生播报细节搬到 Dart,而是先把页面真正需要的“播报语义”收出来。
当前这层设计之所以稳,是因为它先把 TTS 变成了一个简单、明确、可继续扩展的鸿蒙内容消费能力,而不是一组过早暴露的底层控制参数。
更多推荐





所有评论(0)