Flutter 做语音转文字功能时,真正需要处理的不只是“点击按钮开始识别”这一件事,还涉及麦克风权限、语音识别初始化、监听识别状态、实时文本更新、识别结束、异常处理以及不同平台的兼容性。speech_to_text 插件提供了一套相对完整的语音识别接口,适合快速构建语音输入、语音搜索、会议记录、语音备忘录等功能。
本文以 Flutter 为基础,完整梳理基于 speech_to_text 的语音转文字应用实现方式,并给出一套可以直接用于项目开发的代码结构。
一、speech_to_text插件适合什么场景
speech_to_text 是 Flutter 生态中常见的语音识别插件之一,它主要调用 Android 和 iOS 系统提供的语音识别能力,将用户通过麦克风输入的语音转换成文本。
比较适合以下场景:
-
语音搜索
-
语音输入框
-
语音备忘录
-
简单的会议内容记录
-
客服语音录入
-
表单语音填写
-
智能助手的语音输入
-
无障碍辅助输入
需要注意的是,这类插件主要解决的是“语音识别”,并不是完整的语音识别云服务。如果业务需要长时间连续录音、专业领域词汇识别、批量音频转写或者高度可控的识别模型,通常还需要接入云端语音识别服务。
二、创建Flutter项目并添加依赖
首先创建一个 Flutter 项目:
Bashflutter create speech_demo cd speech_demo
然后在 pubspec.yaml 中加入 speech_to_text:
YAMLdependencies: flutter: sdk: flutter speech_to_text: ^7.0.0
实际项目中建议通过 pub.dev 查看当前稳定版本,并根据项目 Flutter/Dart 版本选择兼容的插件版本。
执行:
Bashflutter pub get
如果项目此前存在旧版本依赖,也可以使用:
Bashflutter pub outdated
检查依赖升级情况。
三、speech_to_text核心工作流程
整个语音转文字流程可以拆分成几个阶段:
创建 SpeechToText ↓ 初始化语音识别 ↓ 检查麦克风/语音识别权限 ↓ 调用 listen() ↓ 持续接收识别结果 ↓ 更新页面文本 ↓ 用户停止或系统结束识别 ↓ 调用 stop()
核心对象是:
dartfinal SpeechToText speechToText = SpeechToText();
初始化:
dartfinal bool available = await speechToText.initialize();
开始识别:
dartawait speechToText.listen( onResult: (result) { print(result.recognizedWords); }, );
停止:
dartawait speechToText.stop();
判断是否正在识别:
dartspeechToText.isListening
这些接口基本构成了 Flutter 语音转文字功能的核心。
四、完整语音转文字页面实现
下面提供一个较完整的 Flutter 页面示例,包括初始化、开始识别、停止识别、实时文字显示和状态提示。
dartimport 'package:flutter/material.dart'; import 'package:speech_to_text/speech_to_text.dart' as stt; class SpeechPage extends StatefulWidget { const SpeechPage({super.key}); @override State<SpeechPage> createState() => _SpeechPageState(); } class _SpeechPageState extends State<SpeechPage> { final stt.SpeechToText _speech = stt.SpeechToText(); bool _isAvailable = false; bool _isListening = false; String _text = ''; String _status = '等待开始'; @override void initState() { super.initState(); _initSpeech(); } Future<void> _initSpeech() async { final available = await _speech.initialize( onStatus: (status) { if (!mounted) return; setState(() { _status = status; _isListening = status == 'listening'; }); }, onError: (error) { if (!mounted) return; setState(() { _status = '识别错误:${error.errorMsg}'; _isListening = false; }); }, ); if (!mounted) return; setState(() { _isAvailable = available; _status = available ? '语音识别已准备好' : '设备不支持语音识别'; }); } Future<void> _startListening() async { if (!_isAvailable) { await _initSpeech(); } if (!_isAvailable) { return; } setState(() { _isListening = true; _status = '正在聆听...'; }); await _speech.listen( localeId: 'zh_CN', listenFor: const Duration(minutes: 1), pauseFor: const Duration(seconds: 5), onResult: (result) { if (!mounted) return; setState(() { _text = result.recognizedWords; }); }, ); } Future<void> _stopListening() async { await _speech.stop(); if (!mounted) return; setState(() { _isListening = false; _status = '识别已停止'; }); } @override void dispose() { _speech.stop(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('语音转文字'), ), body: Padding( padding: const EdgeInsets.all(20), child: Column( children: [ Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: Colors.grey.shade100, ), child: Text( _text.isEmpty ? '请点击下方按钮开始说话' : _text, style: const TextStyle( fontSize: 18, height: 1.6, ), ), ), const SizedBox(height: 20), Text( _status, style: TextStyle( color: _isListening ? Colors.green : Colors.grey.shade700, ), ), const Spacer(), FloatingActionButton.large( onPressed: _isListening ? _stopListening : _startListening, child: Icon( _isListening ? Icons.stop : Icons.mic, size: 36, ), ), const SizedBox(height: 30), ], ), ), ); } }
这段代码已经可以构成一个基础的语音转文字页面。
五、为什么必须调用initialize
很多初学者会直接调用:
dart_speech.listen();
然后发现程序没有反应。
正确流程应该先初始化:
dartfinal available = await _speech.initialize();
初始化阶段主要用于确认当前设备是否具备可用的语音识别能力,同时处理相关权限和底层识别服务状态。
比较推荐将初始化放到 initState():
dart@override void initState() { super.initState(); _initSpeech(); }
初始化函数:
dartFuture<void> _initSpeech() async { final available = await _speech.initialize( onStatus: (status) { debugPrint('status: $status'); }, onError: (error) { debugPrint('error: ${error.errorMsg}'); }, ); setState(() { _isAvailable = available; }); }
这样用户进入页面后,语音识别能力就可以提前准备。
六、Android麦克风权限配置
Android 平台必须声明麦克风权限。
打开:
android/app/src/main/AndroidManifest.xml
加入:
XMLandroid:name="android.permission.RECORD_AUDIO" />
如果项目使用的插件版本对 Android 识别服务还有其他要求,应同时按照插件当前版本的官方配置说明进行调整。
需要特别注意,Manifest 中声明权限并不意味着用户已经授权。
Android 运行时仍然可能弹出:
允许应用使用麦克风吗?
speech_to_text 会参与相关权限处理,但实际项目仍然需要做好权限被拒绝后的页面提示。
例如:
dartfinal available = await _speech.initialize(); if (!available) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('语音识别不可用,请检查麦克风权限'), ), ); }
七、iOS权限配置
iOS 对麦克风以及语音识别权限的要求更加严格。
打开:
ios/Runner/Info.plist
根据插件当前版本要求添加相关权限说明,例如:
XMLNSMicrophoneUsageDescription 需要使用麦克风进行语音输入 NSSpeechRecognitionUsageDescription 需要使用语音识别将您的语音转换为文字
权限描述不是简单的技术配置。
它会直接展示给用户,因此应该明确说明为什么需要使用麦克风和语音识别功能。
八、实现中文语音识别
如果应用主要面向中文用户,可以指定语言:
dartawait _speech.listen( localeId: 'zh_CN', onResult: (result) { setState(() { _text = result.recognizedWords; }); }, );
不同设备支持的语言可能不同,因此实际项目不应该简单假设所有设备都支持固定的 localeId。
可以先获取设备支持的语言:
dartfinal locales = await _speech.locales();
然后输出:
dartfor (final locale in locales) { debugPrint( '${locale.localeId} - ${locale.name}', ); }
也可以根据业务需求让用户选择语言。
例如:
dartString _localeId = 'zh_CN';
启动识别时:
dartawait _speech.listen( localeId: _localeId, onResult: (result) { setState(() { _text = result.recognizedWords; }); }, );
这样就可以进一步扩展中文、英文等多语言语音输入。
九、实时显示识别结果
speech_to_text 的一个重要特点是可以通过 onResult 持续获得识别结果。
dartonResult: (result) { setState(() { _text = result.recognizedWords; }); }
用户说:
今天下午三点开会
识别过程中可能依次得到:
今天
然后:
今天下午
最终:
今天下午三点开会
因此 UI 可以实时更新。
如果想判断当前结果是不是最终结果,可以使用:
dartresult.finalResult
例如:
dartonResult: (result) { debugPrint('文本:${result.recognizedWords}'); debugPrint('最终结果:${result.finalResult}'); setState(() { _text = result.recognizedWords; }); }
这个字段对于需要区分“临时识别结果”和“最终确认文本”的业务非常有用。
十、处理识别状态变化
语音识别并不是简单的开始和结束两个状态。
实际运行时可能经历:
listening notListening done
可以通过:
dartonStatus: (status) { debugPrint('语音状态:$status'); }
监听。
例如:
dart_speech.initialize( onStatus: (status) { if (!mounted) return; setState(() { _isListening = status == 'listening'; }); }, );
这样即使底层识别服务主动结束,UI 状态也能够及时同步。
这比单纯依赖按钮点击事件更加可靠。
十一、处理语音识别异常
实际项目中必须处理异常情况。
例如:
dartonError: (error) { debugPrint( '语音识别异常:${error.errorMsg}', ); }
可以进一步将错误展示给用户:
dartonError: (error) { if (!mounted) return; setState(() { _status = '语音识别失败:${error.errorMsg}'; _isListening = false; }); }
常见原因包括:
-
没有授予麦克风权限
-
系统语音识别服务不可用
-
当前设备不支持目标语言
-
网络环境异常
-
系统语音服务暂时不可用
-
识别时间超过限制
-
用户主动停止识别
因此不要把所有异常都统一显示成“识别失败”,最好根据业务提供具有操作指引的信息。
十二、listenFor与pauseFor的作用
开始识别时可以设置:
dartawait _speech.listen( listenFor: const Duration(minutes: 1), pauseFor: const Duration(seconds: 5), onResult: (result) { setState(() { _text = result.recognizedWords; }); }, );
listenFor 用于设置一次监听允许持续的时间范围。
pauseFor 则用于设置用户停顿后,语音识别服务可以等待多久。
例如用户说:
帮我创建一个明天上午九点的提醒
如果 pauseFor 设置得过短,用户稍微停顿一下就可能导致识别提前结束。
因此参数需要结合实际交互场景调整。
十三、实现清空文本功能
实际语音输入页面通常还需要一个清空按钮:
dartIconButton( onPressed: () { setState(() { _text = ''; }); }, icon: const Icon(Icons.delete_outline), )
如果文本最终要提交给后端,可以直接使用:
dartfinal content = _text.trim();
例如:
dartvoid _submitText() { final content = _text.trim(); if (content.isEmpty) { return; } debugPrint('提交内容:$content'); }
这样就可以把语音识别和业务表单结合起来。
十四、语音识别与TextField结合
如果需求不是单纯展示文本,而是让用户把语音直接输入到输入框,可以使用 TextEditingController。
定义:
dartfinal TextEditingController _controller = TextEditingController();
识别结果:
dartonResult: (result) { setState(() { _controller.text = result.recognizedWords; _controller.selection = TextSelection.fromPosition( TextPosition( offset: _controller.text.length, ), ); }); }
输入框:
dartTextField( controller: _controller, maxLines: 5, decoration: const InputDecoration( hintText: '请输入内容', border: OutlineInputBorder(), ), )
这样就能实现类似聊天应用中的“点击麦克风,说话后自动填充输入框”。
十五、避免重复调用listen
用户快速连续点击按钮时,可能导致多个操作同时发生。
因此按钮状态应该和 _isListening 绑定:
dartonPressed: _isListening ? _stopListening : _startListening,
同时启动方法中增加保护:
dartif (_isListening) { return; }
停止方法同样可以判断:
dartif (!_isListening) { return; }
这种状态保护能够减少重复调用底层语音识别服务造成的问题。
十六、Widget销毁时释放资源
页面销毁时不要忘记停止识别:
dart@override void dispose() { _speech.stop(); _controller.dispose(); super.dispose(); }
如果页面中没有 TextEditingController,则只需要:
dart@override void dispose() { _speech.stop(); super.dispose(); }
尤其是从语音页面快速返回上一页时,如果没有正确处理生命周期,可能出现识别回调仍然尝试更新已经销毁的 Widget。
因此异步回调中建议增加:
dartif (!mounted) return;
十七、如何设计更合理的语音输入UI
一个完整的语音转文字页面通常可以分成四个区域:
┌─────────────────────────┐ │ 语音转文字 │ ├─────────────────────────┤ │ │ │ 识别后的文字内容 │ │ │ │ │ ├─────────────────────────┤ │ 当前状态:正在聆听 │ │ │ │ │ │ │ │ 点击停止识别 │ └─────────────────────────┘
建议至少提供以下状态:
-
未开始
-
正在识别
-
暂停或等待
-
识别完成
-
识别失败
-
权限被拒绝
-
当前设备不支持
不要只通过麦克风图标颜色判断状态,最好同时提供文字提示,这对实际用户体验更加友好。
十八、常见问题:为什么没有识别结果
首先检查初始化结果:
dartfinal available = await _speech.initialize(); debugPrint('available = $available');
如果:
available = false
需要重点检查设备和系统语音服务。
然后检查权限。
Android:
XMLandroid:name="android.permission.RECORD_AUDIO" />
iOS:
XMLNSMicrophoneUsageDescription 需要使用麦克风进行语音输入
同时检查 onError:
dartonError: (error) { debugPrint(error.errorMsg); }
最后确认 localeId 是否适用于当前设备。
十九、常见问题:为什么识别一会儿就停止
这是移动端语音识别应用中比较常见的情况。
系统语音识别通常不是为无限时长连续转写设计的。识别服务可能因为:
-
长时间没有检测到语音
-
系统服务限制
-
监听时长达到限制
-
网络状态变化
-
系统资源管理
而主动结束。
如果业务要求长时间录音并连续转换成文字,不建议简单依赖一次 listen()。
可以监听状态:
dartonStatus: (status) { if (status == 'done' && mounted) { setState(() { _isListening = false; }); } }
对于真正的长文本场景,则应该重新评估技术方案。
二十、speech_to_text并不等于录音转写
这是开发 Flutter 语音应用时非常容易混淆的概念。
speech_to_text 的主要使用方式是:
用户说话 ↓ 系统语音识别服务 ↓ 实时识别结果 ↓ Flutter获取文本
而典型的音频文件转写流程是:
用户录音 ↓ 保存音频文件 ↓ 上传服务器 ↓ 云端ASR模型 ↓ 返回完整文本
两者虽然最终都是“语音转文字”,但技术架构完全不同。
如果应用是简单的语音输入,speech_to_text 足够方便。
如果需求是:
-
上传MP3转文字
-
上传WAV转文字
-
会议录音转写
-
数小时音频批量处理
-
专业术语识别
-
多人说话区分
-
时间戳识别
-
服务端统一处理
那么更适合使用专门的语音识别服务。
二十一、项目结构建议
如果语音识别功能只是一个页面,可以直接封装在 StatefulWidget 中。
但在大型项目中,更建议将语音识别逻辑抽离。
例如:
lib/ ├── main.dart ├── pages/ │ └── speech_page.dart ├── services/ │ └── speech_service.dart └── widgets/ └── speech_button.dart
服务层可以封装:
dartclass SpeechService { final stt.SpeechToText _speech = stt.SpeechToText(); Future<bool> initialize({ stt.VoidCallback? onListening, }) async { return await _speech.initialize(); } Future<void> start({ required Function(stt.SpeechRecognitionResult) onResult, }) async { await _speech.listen( onResult: onResult, ); } Future<void> stop() async { await _speech.stop(); } bool get isListening => _speech.isListening; }
页面只负责展示状态和处理用户交互。
这样后续增加语音搜索、语音表单、语音助手等功能时,可以复用同一个服务。
二十二、生产环境中的几个注意事项
1. 不要假设所有设备都支持语音识别
初始化必须检查:
dartfinal available = await _speech.initialize();
2. 不要忽略权限
首次使用时应考虑:
用户点击麦克风 ↓ 请求权限 ↓ 用户允许 ↓ 开始识别
如果用户拒绝,需要提供明确提示。
3. 不要无限依赖单次识别
移动操作系统对语音识别持续时间存在实际限制,长时间转写应该采用其他架构。
4. 正确处理异步回调
尤其是:
dartonResult onStatus onError
中涉及 UI 更新时,应注意:
dartif (!mounted) return;
5. 不要把识别文本直接当成可信数据
语音识别结果可能存在错别字、同音字、标点错误等问题。
例如:
帮我查一下明天上海天气
可能出现:
帮我查一下明天上嗨天气
如果文本会用于支付、下单、执行命令等关键操作,应增加确认机制。
二十三、可以进一步增加自动标点和业务处理
语音识别拿到文本后,可以继续进行文本处理。
例如:
dartString normalizeText(String text) { return text .trim() .replaceAll(RegExp(r's+'), ' '); }
然后:
dartfinal content = normalizeText(_text);
如果应用是语音搜索,可以直接:
dartsearch(_text);
如果是智能助手,则可以:
dartsendMessage(_text);
如果是表单填写,则可以将文本交给业务层进一步解析。
例如:
“明天上午十点提醒我给客户打电话”
后续可以通过自然语言处理提取:
日期:明天 时间:10:00 任务:给客户打电话
这样语音识别就不再只是一个输入组件,而可以成为完整业务流程的一部分。
二十四、完整实现思路总结
一个稳定的 Flutter 语音转文字应用,可以按照下面的流程设计:
进入语音页面 ↓ 初始化 SpeechToText ↓ 检查语音识别能力 ↓ 检查麦克风权限 ↓ 获取可用语言 ↓ 用户点击麦克风 ↓ 调用 listen() ↓ onResult持续获取文本 ↓ 实时更新TextField/UI ↓ onStatus监听识别状态 ↓ onError处理异常 ↓ 用户停止或系统结束 ↓ 调用stop() ↓ 保存/提交最终文本
核心代码可以浓缩为:
dartfinal speech = stt.SpeechToText(); final available = await speech.initialize( onStatus: (status) { debugPrint(status); }, onError: (error) { debugPrint(error.errorMsg); }, ); if (available) { await speech.listen( localeId: 'zh_CN', onResult: (result) { debugPrint(result.recognizedWords); }, ); }
如果需要停止:
dartawait speech.stop();
如果需要判断当前是否正在识别:
dartspeech.isListening
这几个 API 是构建 Flutter speech_to_text 应用最核心的基础。
对于普通语音输入需求,speech_to_text 可以大幅降低开发成本。真正决定应用稳定性的,则是权限管理、状态同步、异常处理、生命周期控制以及平台差异处理。将这些部分封装好之后,就能够进一步扩展成语音搜索、语音表单、语音助手和语音备忘录等更加完整的 Flutter 应用。