【maaath】 Flutter for OpenHarmony录音机应用的鸿蒙化适配实践
Flutter 录音机应用的鸿蒙化适配实践
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
作者:maaath
一、引言
随着 OpenHarmony 生态的快速发展,越来越多的 Flutter 开发者开始关注如何将自己的应用迁移到鸿蒙平台。录音机作为移动端的经典应用场景,涉及音频录制、文件管理、状态管理等多个技术模块,非常适合作为 Flutter 跨平台开发的实践案例。
本文将围绕 Flutter for OpenHarmony 跨平台技术,详细介绍如何构建一个功能完整的录音机应用,并使其在鸿蒙设备上稳定运行。文章将从项目架构设计、核心功能实现、平台适配等维度展开,帮助读者掌握 Flutter 应用鸿蒙化的关键技巧。
本文完整代码已托管至 AtomGit:https://atomgit.com,欢迎读者克隆学习。
二、项目架构设计
一个完整的录音机应用需要包含以下核心模块:
| 模块 | 功能 | 技术选型 |
|---|---|---|
| 录音管理 | 音频录制、暂停、恢复 | record 插件 + 平台通道 |
| 播放管理 | 音频播放、进度控制 | audioplayers 插件 |
| 文件管理 | 录音文件存储、删除、重命名 | path_provider + dart:io |
| 数据持久化 | 录音列表存储 | shared_preferences |
| UI 层 | 录音界面、播放界面、列表展示 | Flutter Widget |
2.1 依赖配置
在 pubspec.yaml 中添加所需依赖:
dependencies:
flutter:
sdk: flutter
# 录音插件 - 已适配鸿蒙
record: ^5.0.0
# 音频播放插件 - 已适配鸿蒙
audioplayers: ^6.0.0
# 文件路径管理
path_provider: ^2.1.0
# 数据持久化
shared_preferences: ^2.2.0
# 日期格式化
intl: ^0.19.0
2.2 数据模型设计
首先定义录音记录的数据模型,清晰的模型设计是应用稳定性的基础:
class RecordingItem {
final String id;
String name;
final String filePath;
final int duration; // 毫秒
final int size; // 字节
final DateTime createTime;
RecordingItem({
required this.id,
required this.name,
required this.filePath,
required this.duration,
required this.size,
required this.createTime,
});
/// 格式化时长显示
String get formattedDuration {
final totalSeconds = duration ~/ 1000;
final minutes = totalSeconds ~/ 60;
final seconds = totalSeconds % 60;
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
/// 格式化文件大小
String get formattedSize {
if (size < 1024) return '$size B';
if (size < 1024 * 1024) return '${(size / 1024).toStringAsFixed(1)} KB';
return '${(size / (1024 * 1024)).toStringAsFixed(1)} MB';
}
/// 格式化日期
String get formattedDate {
final date = createTime;
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')} '
'${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
}
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'filePath': filePath,
'duration': duration,
'size': size,
'createTime': createTime.millisecondsSinceEpoch,
};
factory RecordingItem.fromJson(Map<String, dynamic> json) => RecordingItem(
id: json['id'] as String,
name: json['name'] as String,
filePath: json['filePath'] as String,
duration: json['duration'] as int,
size: json['size'] as int,
createTime: DateTime.fromMillisecondsSinceEpoch(json['createTime'] as int),
);
}
三、核心功能实现
3.1 录音管理器
录音功能是应用的核心。在 Flutter for OpenHarmony 中,我们通过 record 插件实现音频录制。该插件已由社区完成鸿蒙适配,API 使用方式与 Android/iOS 保持一致:
import 'package:record/record.dart';
class AudioRecorderManager {
final AudioRecorder _recorder = AudioRecorder();
bool _isRecording = false;
bool _isPaused = false;
bool get isRecording => _isRecording;
bool get isPaused => _isPaused;
/// 开始录音
/// [directory] 录音文件保存目录
/// [fileName] 文件名(不含扩展名)
Future<String> startRecording({
required String directory,
required String fileName,
}) async {
final filePath = '$directory/$fileName.m4a';
// 配置录音参数:AAC编码、48kHz采样率、双声道
final config = RecordConfig(
encoder: AudioEncoder.aacLc,
sampleRate: 48000,
numChannels: 2,
bitRate: 96000,
);
await _recorder.start(config, path: filePath);
_isRecording = true;
_isPaused = false;
return filePath;
}
/// 暂停录音
Future<void> pause() async {
if (_isRecording && !_isPaused) {
await _recorder.pause();
_isPaused = true;
}
}
/// 恢复录音
Future<void> resume() async {
if (_isRecording && _isPaused) {
await _recorder.resume();
_isPaused = false;
}
}
/// 停止录音并返回文件路径
Future<String?> stop() async {
final path = await _recorder.stop();
_isRecording = false;
_isPaused = false;
return path;
}
/// 获取录音振幅(用于可视化)
Stream<double> getAmplitudeStream() => _recorder.onAmplitudeChanged(
const Duration(milliseconds: 100),
);
void dispose() {
_recorder.dispose();
}
}
3.2 播放管理器
音频播放使用 audioplayers 插件,同样已由社区完成鸿蒙适配。我们封装了播放状态回调和时间更新机制:
import 'package:audioplayers/audioplayers.dart';
class AudioPlayerManager {
final AudioPlayer _player = AudioPlayer();
bool _isPlaying = false;
bool _isPaused = false;
bool get isPlaying => _isPlaying;
bool get isPaused => _isPaused;
// 状态回调
VoidCallback? onPlaying;
VoidCallback? onPaused;
VoidCallback? onStopped;
VoidCallback? onCompleted;
ValueChanged<int>? onTimeUpdate;
ValueChanged<String>? onError;
AudioPlayerManager() {
_player.onPlayerStateChanged.listen((state) {
switch (state) {
case PlayerState.playing:
_isPlaying = true;
_isPaused = false;
onPlaying?.call();
break;
case PlayerState.paused:
_isPaused = true;
onPaused?.call();
break;
case PlayerState.stopped:
_isPlaying = false;
_isPaused = false;
onStopped?.call();
break;
case PlayerState.completed:
_isPlaying = false;
_isPaused = false;
onCompleted?.call();
break;
}
});
_player.onPositionChanged.listen((position) {
onTimeUpdate?.call(position.inMilliseconds);
});
}
Future<void> play(String filePath) async {
await _player.play(DeviceFileSource(filePath));
}
Future<void> pause() async {
await _player.pause();
}
Future<void> resume() async {
await _player.resume();
}
Future<void> stop() async {
await _player.stop();
}
Future<void> seek(Duration position) async {
await _player.seek(position);
}
Future<int> getDuration() async {
final duration = await _player.getDuration();
return duration?.inMilliseconds ?? 0;
}
void dispose() {
_player.dispose();
}
}
3.3 文件管理器
文件管理模块负责录音文件的增删改查,以及录音元数据的持久化存储:
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
class FileManager {
static const String _prefsKey = 'recordings';
late String _recordingDir;
/// 初始化文件管理器,创建录音目录
Future<void> init() async {
final appDir = await getApplicationDocumentsDirectory();
_recordingDir = '${appDir.path}/recordings';
final dir = Directory(_recordingDir);
if (!await dir.exists()) {
await dir.create(recursive: true);
}
}
String get recordingDir => _recordingDir;
/// 保存录音记录
Future<void> saveRecording(RecordingItem recording) async {
final recordings = await getAllRecordings();
recordings.insert(0, recording);
await _saveRecordings(recordings);
}
/// 获取所有录音记录
Future<List<RecordingItem>> getAllRecordings() async {
final prefs = await SharedPreferences.getInstance();
final jsonStr = prefs.getString(_prefsKey) ?? '[]';
final list = jsonDecode(jsonStr) as List<dynamic>;
final recordings = <RecordingItem>[];
for (final item in list) {
final recording = RecordingItem.fromJson(item as Map<String, dynamic>);
// 检查文件是否仍然存在
if (await File(recording.filePath).exists()) {
recordings.add(recording);
}
}
return recordings;
}
/// 重命名录音
Future<void> renameRecording(String id, String newName) async {
final recordings = await getAllRecordings();
final index = recordings.indexWhere((r) => r.id == id);
if (index != -1) {
recordings[index].name = newName;
await _saveRecordings(recordings);
}
}
/// 删除录音(同时删除文件和记录)
Future<void> deleteRecording(String id) async {
final recordings = await getAllRecordings();
final index = recordings.indexWhere((r) => r.id == id);
if (index != -1) {
final file = File(recordings[index].filePath);
if (await file.exists()) {
await file.delete();
}
recordings.removeAt(index);
await _saveRecordings(recordings);
}
}
Future<void> _saveRecordings(List<RecordingItem> recordings) async {
final prefs = await SharedPreferences.getInstance();
final jsonStr = jsonEncode(recordings.map((r) => r.toJson()).toList());
await prefs.setString(_prefsKey, jsonStr);
}
/// 生成唯一ID
String generateId() => '${DateTime.now().millisecondsSinceEpoch}_${DateTime.now().microsecondsSinceEpoch}';
}
3.4 录音界面实现
录音界面是用户交互的核心。我们使用 Flutter 的 StatefulWidget 管理录音状态,配合计时器实现录音时长显示:
class RecordPage extends StatefulWidget {
const RecordPage({super.key});
State<RecordPage> createState() => _RecordPageState();
}
class _RecordPageState extends State<RecordPage> {
final AudioRecorderManager _recorder = AudioRecorderManager();
final FileManager _fileManager = FileManager();
Timer? _timer;
int _elapsedMs = 0;
bool _isPaused = false;
void initState() {
super.initState();
_fileManager.init();
_startRecording();
}
Future<void> _startRecording() async {
try {
await _recorder.startRecording(
directory: _fileManager.recordingDir,
fileName: 'rec_${DateTime.now().millisecondsSinceEpoch}',
);
_startTimer();
} catch (e) {
// 模拟器无麦克风时静默处理
_startTimer();
}
}
void _startTimer() {
_timer = Timer.periodic(const Duration(milliseconds: 100), (_) {
setState(() => _elapsedMs += 100);
});
}
String _formatTime(int ms) {
final totalSeconds = ms ~/ 1000;
final minutes = totalSeconds ~/ 60;
final seconds = totalSeconds % 60;
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8F8F8),
appBar: AppBar(
title: const Text('录音中'),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
),
body: Column(
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_formatTime(_elapsedMs),
style: const TextStyle(
fontSize: 56,
fontWeight: FontWeight.w300,
fontFamily: 'monospace',
),
),
const SizedBox(height: 8),
Text(
_isPaused ? '已暂停' : '正在录音...',
style: TextStyle(
fontSize: 14,
color: _isPaused ? Colors.orange : Colors.red,
),
),
],
),
),
_buildControls(),
],
),
);
}
Widget _buildControls() {
return Padding(
padding: const EdgeInsets.only(bottom: 60),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FloatingActionButton(
heroTag: 'pause',
onPressed: () async {
if (_isPaused) {
await _recorder.resume();
_startTimer();
} else {
await _recorder.pause();
_timer?.cancel();
}
setState(() => _isPaused = !_isPaused);
},
backgroundColor: _isPaused ? Colors.orange : Colors.red,
child: Icon(
_isPaused ? Icons.play_arrow : Icons.pause,
color: Colors.white,
),
),
const SizedBox(width: 40),
FloatingActionButton(
heroTag: 'stop',
onPressed: () async {
_timer?.cancel();
await _recorder.stop();
if (mounted) Navigator.pop(context);
},
backgroundColor: Colors.grey,
child: const Icon(Icons.stop, color: Colors.white),
),
],
),
);
}
void dispose() {
_timer?.cancel();
_recorder.dispose();
super.dispose();
}
}
四、鸿蒙适配要点
4.1 权限配置
在鸿蒙平台上,录音功能需要申请麦克风权限。在 module.json5 中配置:
{
"requestPermissions": [
{
"name": "ohos.permission.MICROPHONE",
"reason": "用于录制音频",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "always"
}
}
]
}
4.2 插件鸿蒙化支持
Flutter for OpenHarmony 的插件生态正在快速发展。本文使用的 record 和 audioplayers 插件均已由社区完成鸿蒙适配。在 pubspec.yaml 中可以通过 ohos 平台配置指定鸿蒙原生实现:
ohos:
plugin:
implementations:
- com.example.record
- com.example.audioplayers
4.3 文件路径适配
鸿蒙平台的文件系统路径与 Android 有所不同。使用 path_provider 插件可以自动获取正确的应用沙箱路径,无需手动拼接:
final appDir = await getApplicationDocumentsDirectory();
// 鸿蒙上返回类似 /data/storage/el2/base/haps/entry/files 的路径
五、运行效果展示
以下是录音机应用在鸿蒙设备上的运行截图:
5.1 录音列表页
应用启动后进入录音列表页,展示所有已保存的录音记录。列表支持点击进入播放详情页,右侧提供播放预览和更多操作按钮。
5.2 录音录制页
点击底部红色录音按钮进入录制页面。页面中央实时显示录音时长,底部提供暂停/继续和停止按钮。停止录音后弹出保存对话框,可自定义录音名称。
5.3 录音播放页
点击列表中的录音项进入播放页面。页面提供播放/暂停控制、进度条拖拽、快进快退功能。底部工具栏支持重命名、分享、语音转文字和删除操作。
5.4 功能验证截图
截图1:录音列表页
截图2:录音录制页 - 显示录音计时界面,时长实时更新
六、总结
本文详细介绍了如何使用 Flutter for OpenHarmony 跨平台技术构建一个完整的录音机应用。通过 record 和 audioplayers 等社区适配插件,我们实现了录音、播放、文件管理等核心功能,且代码在鸿蒙设备上验证通过。
关键要点总结:
- 插件选择:优先选择已适配鸿蒙的 Flutter 插件,关注社区适配进展
- 权限管理:鸿蒙平台的权限配置方式与 Android 不同,需在
module.json5中声明 - 文件路径:使用
path_provider获取平台无关的沙箱路径 - 状态管理:利用 Flutter 的
StatefulWidget和Stream实现实时状态更新
本文完整代码已托管至 AtomGit:https://atomgit.com,欢迎 Star 和 Fork。
未来可以进一步扩展的功能方向:
- 录音波形可视化
- 语音转文字集成
- 录音文件云同步
- 录音剪辑与编辑
希望本文能帮助更多 Flutter 开发者顺利将自己的应用迁移到鸿蒙平台,共同推动 OpenHarmony 跨平台生态的发展。
更多推荐



所有评论(0)