Flutter三方库适配OpenHarmony【flutter_speech】— 示例应用开发
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net前面十几篇我们一直在讲原生端的实现,今天换个视角——从Dart层看flutter_speech是怎么被使用的。示例应用(example/lib/main.dart)是插件的"门面",也是开发者第一次接触插件时看到的东西。一个好的示例应用应该做到:功能完整、代码清晰、交互友好。
前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
前面十几篇我们一直在讲原生端的实现,今天换个视角——从Dart层看flutter_speech是怎么被使用的。
示例应用(example/lib/main.dart)是插件的"门面",也是开发者第一次接触插件时看到的东西。一个好的示例应用应该做到:功能完整、代码清晰、交互友好。flutter_speech的示例App虽然只有188行代码,但覆盖了插件的所有功能——语言选择、语音识别、实时显示、停止取消、错误处理。
我在做OpenHarmony适配的时候,示例App也做了一些调整,主要是增加了平台判断逻辑——在OpenHarmony上限制语言选择,避免用户选了不支持的语言。
💡 本文对应源码:
example/lib/main.dart,完整188行。
一、example/lib/main.dart 完整代码解析
1.1 文件结构概览
// 导入
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_speech/flutter_speech.dart';
// 入口
void main() { ... }
// 语言数据
const languages = [ ... ];
class Language { ... }
// 主应用
class MyApp extends StatefulWidget { ... }
class _MyAppState extends State<MyApp> {
// 状态变量
// 生命周期方法
// UI构建方法
// 交互方法
// 回调处理方法
}
1.2 应用入口
void main() {
debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia;
runApp(MyApp());
}
debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia这行代码是Flutter-OHOS的一个约定——OpenHarmony在Flutter中被映射为Fuchsia平台。设置这个覆盖后,Flutter的Material组件会使用Fuchsia的默认样式。
📌 为什么是Fuchsia:Flutter最初设计时就预留了Fuchsia平台的支持。OpenHarmony的Flutter适配复用了这个平台标识,避免了修改Flutter框架核心代码。
1.3 语言数据定义
const languages = const [
const Language('中文', 'zh_CN'),
const Language('English', 'en_US'),
const Language('Francais', 'fr_FR'),
const Language('Pусский', 'ru_RU'),
const Language('Italiano', 'it_IT'),
const Language('Español', 'es_ES'),
];
class Language {
final String name;
final String code;
const Language(this.name, this.code);
}
6种语言,每种语言有显示名称和locale代码。Language类非常简单——两个final字段加一个const构造函数。
| 语言 | 显示名称 | Locale代码 | OpenHarmony支持 |
|---|---|---|---|
| 中文 | 中文 | zh_CN | ✅ |
| 英文 | English | en_US | ❌ |
| 法文 | Francais | fr_FR | ❌ |
| 俄文 | Pусский | ru_RU | ❌ |
| 意大利文 | Italiano | it_IT | ❌ |
| 西班牙文 | Español | es_ES | ❌ |
二、语言选择器 UI 实现与 PopupMenuButton
2.1 PopupMenuButton
语言选择器放在AppBar的actions中:
AppBar(
title: Text('SpeechRecognition'),
actions: [
PopupMenuButton<Language>(
onSelected: _selectLangHandler,
itemBuilder: (BuildContext context) => _buildLanguagesWidgets,
)
],
),
点击后会弹出一个下拉菜单,显示所有可选语言。
2.2 菜单项构建
List<CheckedPopupMenuItem<Language>> get _buildLanguagesWidgets => languages
.map((l) => CheckedPopupMenuItem<Language>(
value: l,
checked: selectedLang == l,
child: Text(l.name),
))
.toList();
使用CheckedPopupMenuItem,当前选中的语言会显示一个勾选标记。这是一个很好的UX细节——用户一眼就能看到当前选的是哪种语言。
2.3 语言选择处理
void _selectLangHandler(Language lang) {
if (Platform.isOhos &&
!lang.code.startsWith('zh')) {
_messengerKey.currentState?.showSnackBar(
const SnackBar(content: Text('当前设备仅支持中文语音识别')),
);
return;
}
setState(() => selectedLang = lang);
}
这里有一个平台判断:如果是OpenHarmony设备,且用户选择了非中文语言,会弹出SnackBar提示"当前设备仅支持中文语音识别",并且不会切换语言。
💡 用户体验考量:与其让用户选了不支持的语言后在activate时报错,不如在选择时就拦截。这种"前置拦截"的体验更好——用户立刻就知道为什么不能选,不需要等到点击"开始识别"才发现问题。
2.4 Platform.isOhos
if (Platform.isOhos && !lang.code.startsWith('zh')) {
Platform.isOhos是Flutter-OHOS SDK提供的平台判断属性。在标准Flutter SDK中没有这个属性,所以这段代码只在Flutter-OHOS环境下才能编译。
| 平台判断 | 属性 | 说明 |
|---|---|---|
| Android | Platform.isAndroid | 标准Flutter |
| iOS | Platform.isIOS | 标准Flutter |
| macOS | Platform.isMacOS | 标准Flutter |
| OpenHarmony | Platform.isOhos | Flutter-OHOS扩展 |
三、语音识别状态管理:Available、Listening、Result
3.1 状态变量
class _MyAppState extends State<MyApp> {
late SpeechRecognition _speech;
bool _speechRecognitionAvailable = false; // 引擎是否可用
bool _isListening = false; // 是否正在监听
String transcription = ''; // 识别结果文本
Language selectedLang = languages.first; // 当前选择的语言
}
四个状态变量,各有职责:
| 变量 | 类型 | 初始值 | 控制什么 |
|---|---|---|---|
| _speechRecognitionAvailable | bool | false | Listen按钮是否可点击 |
| _isListening | bool | false | 按钮状态切换、动画显示 |
| transcription | String | ‘’ | 显示区域的文本内容 |
| selectedLang | Language | 中文 | 语言选择器的当前值 |
3.2 状态转换
初始状态:
_speechRecognitionAvailable = false
_isListening = false
transcription = ''
activate成功后:
_speechRecognitionAvailable = true ← 引擎就绪
listen后:
_isListening = true ← 开始监听
识别过程中:
transcription = "你好" ← 实时更新
transcription = "你好世界"
识别完成:
_isListening = false ← 停止监听
transcription = "你好世界" ← 最终结果
错误发生:
_speechRecognitionAvailable = false ← 引擎不可用
_isListening = false
→ 自动重新activate
3.3 状态与UI的映射
// Listen按钮:引擎可用且未在监听时可点击
_buildButton(
onPressed: _speechRecognitionAvailable && !_isListening
? () => start()
: null,
label: _isListening ? 'Listening...' : 'Listen (${selectedLang.code})',
),
// Cancel按钮:正在监听时可点击
_buildButton(
onPressed: _isListening ? () => cancel() : null,
label: 'Cancel',
),
// Stop按钮:正在监听时可点击
_buildButton(
onPressed: _isListening ? () => stop() : null,
label: 'Stop',
),
| 状态 | Listen按钮 | Cancel按钮 | Stop按钮 |
|---|---|---|---|
| 未初始化 | 灰色 | 灰色 | 灰色 |
| 引擎就绪 | 可点击 | 灰色 | 灰色 |
| 正在监听 | 灰色(显示"Listening…") | 可点击 | 可点击 |
| 识别完成 | 可点击 | 灰色 | 灰色 |
四、Platform.isOhos 平台判断与功能限制提示
4.1 使用场景
示例App中Platform.isOhos只在一个地方使用——语言选择处理:
void _selectLangHandler(Language lang) {
if (Platform.isOhos && !lang.code.startsWith('zh')) {
_messengerKey.currentState?.showSnackBar(
const SnackBar(content: Text('当前设备仅支持中文语音识别')),
);
return;
}
setState(() => selectedLang = lang);
}
4.2 为什么不在其他地方也做平台判断
你可能会想:为什么不在start方法中也加平台判断?
因为原生端已经做了语言校验。即使Dart层没有拦截,原生端的isSupportedLocale也会返回错误。Dart层的拦截只是为了更好的用户体验——提前告诉用户,而不是等到调用失败。
Dart层拦截(UX优化):
用户选择English → SnackBar提示 → 不切换语言
原生端拦截(兜底保护):
用户调用activate("en_US") → ERROR_LANGUAGE_NOT_SUPPORTED → catchError
两层拦截,双重保险。
4.3 SnackBar提示
_messengerKey.currentState?.showSnackBar(
const SnackBar(content: Text('当前设备仅支持中文语音识别')),
);
使用ScaffoldMessengerState的showSnackBar方法,在屏幕底部显示一个临时提示。这比弹窗(AlertDialog)更轻量,不会打断用户的操作流程。
📌 注意:这里用的是
_messengerKey.currentState而不是ScaffoldMessenger.of(context),因为_selectLangHandler的调用上下文可能不在Scaffold内部。通过GlobalKey可以在任何地方访问ScaffoldMessenger。
五、按钮交互逻辑:Listen、Cancel、Stop
5.1 start方法
void start() => _speech.activate(selectedLang.code).then((_) {
return _speech.listen().then((result) {
print('_MyAppState.start => result $result');
setState(() {
_isListening = result;
});
});
});
start方法做了两件事:
- activate:用当前选择的语言激活引擎
- listen:开始监听
注意这是一个链式调用——先activate,成功后再listen。如果activate失败(比如语言不支持),listen不会被调用。
🤔 设计思考:每次start都重新activate,这意味着每次识别都会重新申请权限和创建引擎。对于频繁使用的场景,可以优化为只在第一次或切换语言时activate。但对于示例App来说,这种简单的实现足够了。
5.2 cancel方法
void cancel() =>
_speech.cancel().then((_) => setState(() => _isListening = false));
调用_speech.cancel()取消识别,然后将_isListening设为false。
5.3 stop方法
void stop() => _speech.stop().then((_) {
setState(() => _isListening = false);
});
调用_speech.stop()停止识别,然后将_isListening设为false。
5.4 cancel和stop的区别(从UI角度)
| 操作 | 用户看到的效果 |
|---|---|
| Cancel | 按钮恢复,识别文本保持为部分结果(不会更新为最终结果) |
| Stop | 按钮恢复,识别文本更新为最终结果(通过onRecognitionComplete回调) |
5.5 按钮构建
Widget _buildButton({required String label, VoidCallback? onPressed}) => Padding(
padding: EdgeInsets.all(12.0),
child: ElevatedButton(
onPressed: onPressed,
child: Text(
label,
style: const TextStyle(color: Colors.white),
),
));
统一的按钮构建方法,接受label和onPressed两个参数。当onPressed为null时,按钮自动变为灰色不可点击状态。
六、回调处理方法详解
6.1 activateSpeechRecognizer
void activateSpeechRecognizer() {
print('_MyAppState.activateSpeechRecognizer... ');
_speech = SpeechRecognition();
_speech.setAvailabilityHandler(onSpeechAvailability);
_speech.setRecognitionStartedHandler(onRecognitionStarted);
_speech.setRecognitionResultHandler(onRecognitionResult);
_speech.setRecognitionCompleteHandler(onRecognitionComplete);
_speech.setErrorHandler(errorHandler);
_speech.activate('zh_CN').then((res) {
setState(() => _speechRecognitionAvailable = res);
}).catchError((e) {
print('_MyAppState.activateSpeechRecognizer error: $e');
setState(() => _speechRecognitionAvailable = false);
});
}
这是初始化方法,做了以下事情:
- 创建
SpeechRecognition单例 - 设置5个回调处理器
- 用中文激活引擎
- 根据结果更新UI状态
⚠️ 回调必须在activate之前设置。因为
SpeechRecognition的回调属性是late声明的,如果在回调触发时还没设置,会抛出LateInitializationError。
6.2 五个回调方法
// 1. 引擎可用性变化
void onSpeechAvailability(bool result) =>
setState(() => _speechRecognitionAvailable = result);
// 2. 识别开始
void onRecognitionStarted() {
setState(() => _isListening = true);
}
// 3. 识别结果(实时)
void onRecognitionResult(String text) {
print('_MyAppState.onRecognitionResult... $text');
setState(() => transcription = text);
}
// 4. 识别完成
void onRecognitionComplete(String text) {
print('_MyAppState.onRecognitionComplete... $text');
setState(() => _isListening = false);
}
// 5. 错误处理
void errorHandler() => activateSpeechRecognizer();
| 回调 | 触发时机 | 状态更新 | UI效果 |
|---|---|---|---|
| onSpeechAvailability | activate成功/失败 | _speechRecognitionAvailable | Listen按钮可用性 |
| onRecognitionStarted | 引擎开始采集音频 | _isListening = true | 显示"Listening…" |
| onRecognitionResult | 收到部分/最终结果 | transcription = text | 文本区域实时更新 |
| onRecognitionComplete | 识别结束 | _isListening = false | 按钮恢复 |
| errorHandler | 发生错误 | 重新初始化 | 自动恢复 |
6.3 errorHandler的自动恢复策略
void errorHandler() => activateSpeechRecognizer();
错误处理的策略非常简单粗暴——重新初始化。调用activateSpeechRecognizer()会重新创建SpeechRecognition实例、设置回调、重新activate。
这种策略的优缺点:
| 优点 | 缺点 |
|---|---|
| 实现简单 | 可能导致无限重试(如果错误持续发生) |
| 能恢复大多数临时错误 | 用户体验不够好(没有错误提示) |
| 不需要区分错误类型 | 每次都重新申请权限(虽然已授权不会弹窗) |
🤦 潜在问题:如果错误持续发生(比如设备不支持语音识别),
errorHandler会不断调用activateSpeechRecognizer,形成无限循环。实际使用中应该加一个重试次数限制。
七、UI布局分析
7.1 整体布局
┌──────────────────────────┐
│ AppBar: SpeechRecognition [≡] ← 语言选择菜单
├──────────────────────────┤
│ │
│ ┌────────────────────┐ │
│ │ │ │
│ │ 识别结果文本区域 │ │ ← Expanded,占据剩余空间
│ │ (灰色背景) │ │
│ │ │ │
│ └────────────────────┘ │
│ │
│ ┌────────────────────┐ │
│ │ Listen (zh_CN) │ │ ← 开始按钮
│ └────────────────────┘ │
│ │
│ ┌────────────────────┐ │
│ │ Cancel │ │ ← 取消按钮
│ └────────────────────┘ │
│ │
│ ┌────────────────────┐ │
│ │ Stop │ │ ← 停止按钮
│ └────────────────────┘ │
│ │
└──────────────────────────┘
7.2 文本显示区域
Expanded(
child: Container(
padding: const EdgeInsets.all(8.0),
color: Colors.grey.shade200,
child: Text(transcription))),
Expanded让文本区域占据按钮之外的所有空间- 灰色背景区分文本区域和按钮区域
transcription变量绑定到Text组件,实时更新
7.3 按钮区域
三个按钮垂直排列,每个按钮有12px的padding:
_buildButton(
onPressed: _speechRecognitionAvailable && !_isListening ? () => start() : null,
label: _isListening ? 'Listening...' : 'Listen (${selectedLang.code})',
),
_buildButton(
onPressed: _isListening ? () => cancel() : null,
label: 'Cancel',
),
_buildButton(
onPressed: _isListening ? () => stop() : null,
label: 'Stop',
),
Listen按钮的label会动态变化:
- 未监听时:
Listen (zh_CN)— 显示当前语言 - 监听中:
Listening...— 提示正在监听
八、完整的用户交互流程
8.1 正常识别流程
1. App启动
→ activateSpeechRecognizer()
→ activate('zh_CN')
→ 权限弹窗(首次)→ 用户允许
→ 引擎创建成功
→ onSpeechAvailability(true)
→ Listen按钮变为可点击
2. 用户点击Listen
→ start()
→ activate(selectedLang.code)
→ listen()
→ onRecognitionStarted()
→ 按钮显示"Listening...",Cancel和Stop可点击
3. 用户说话
→ onRecognitionResult("你好")
→ 文本区域显示"你好"
→ onRecognitionResult("你好世界")
→ 文本区域更新为"你好世界"
4. 用户停止说话(VAD超时)
→ onRecognitionComplete("你好世界")
→ 按钮恢复,文本保持"你好世界"
8.2 用户手动Stop
(识别进行中)
用户点击Stop
→ stop()
→ _isListening = false → 按钮恢复
→ (稍后) onRecognitionComplete("你好世界")
→ 文本更新为最终结果
8.3 用户手动Cancel
(识别进行中)
用户点击Cancel
→ cancel()
→ _isListening = false → 按钮恢复
→ (不会收到onRecognitionComplete)
→ 文本保持为最后一次的部分结果
8.4 错误恢复
(识别过程中网络断开)
→ onError触发
→ errorHandler()
→ activateSpeechRecognizer() → 重新初始化
→ activate('zh_CN') → 重新激活
→ 恢复到就绪状态
九、示例App的改进建议
9.1 当前的不足
| 问题 | 影响 | 改进方向 |
|---|---|---|
| errorHandler无限重试 | 可能死循环 | 加重试次数限制 |
| 没有loading状态 | activate时UI无反馈 | 加CircularProgressIndicator |
| 没有错误提示 | 用户不知道出了什么错 | 显示错误信息 |
| 每次start都activate | 性能浪费 | 缓存引擎状态 |
| 文本区域无滚动 | 长文本显示不全 | 用SingleChildScrollView |
9.2 加loading状态
// 改进示例
bool _isActivating = false;
void start() {
setState(() => _isActivating = true);
_speech.activate(selectedLang.code).then((_) {
setState(() => _isActivating = false);
return _speech.listen().then((result) {
setState(() => _isListening = result);
});
}).catchError((e) {
setState(() => _isActivating = false);
// 显示错误
});
}
9.3 加重试限制
int _retryCount = 0;
static const int _maxRetries = 3;
void errorHandler() {
if (_retryCount < _maxRetries) {
_retryCount++;
activateSpeechRecognizer();
} else {
setState(() {
_speechRecognitionAvailable = false;
});
// 显示"语音识别不可用"的提示
}
}
这些改进不影响插件本身的功能,只是让示例App的用户体验更好。
总结
本文从Dart层的角度完整解析了flutter_speech的示例应用:
- 语言选择:PopupMenuButton + CheckedPopupMenuItem,OpenHarmony上限制只能选中文
- 状态管理:四个状态变量控制UI的所有状态变化
- Platform.isOhos:平台判断实现功能限制提示
- 交互逻辑:start(activate+listen)、cancel、stop三个操作
- 回调处理:五个回调方法分别处理可用性、开始、结果、完成、错误
- 错误恢复:errorHandler自动重新初始化
下一篇是本系列第1-20篇的收官之作——跨平台差异对比,我们将把Android、iOS、OpenHarmony三个平台的实现做一次全面的横向对比。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- Flutter StatefulWidget文档
- PopupMenuButton文档
- Flutter-OHOS Platform API
- flutter_speech示例源码
- Flutter状态管理指南
- Material Design按钮规范
- 开源鸿蒙跨平台社区
- SnackBar使用指南

更多推荐



所有评论(0)