Flutter 框架跨平台鸿蒙开发 - 打造音乐播放器应用
音频播放和控制FilePicker:本地文件选择播放控制:播放、暂停、上下首、进度控制播放模式:顺序、随机、循环播放列表:创建、管理、加载数据持久化:SharedPreferences存储状态管理:播放状态和进度监听通过本项目,你不仅学会了如何实现音乐播放器应用,还掌握了Flutter中音频处理、文件操作、状态管理的核心技术。这些知识可以应用到更多多媒体应用的开发。享受你的音乐时光!
Flutter实战:打造音乐播放器应用
前言
音乐播放器是一款经典的多媒体应用,让用户可以播放本地音乐文件并管理播放列表。本文将带你从零开始,使用Flutter开发一个功能完整的音乐播放器应用,支持音频播放、播放列表管理、播放控制等功能。
应用特色
- 🎵 本地音乐:支持添加本地音频文件
- ▶️ 播放控制:播放、暂停、上一首、下一首
- 📊 进度条:实时显示播放进度,可拖动
- 🔀 播放模式:顺序播放、随机播放、单曲循环
- 📝 播放列表:创建和管理多个播放列表
- 💾 数据持久化:音乐和播放列表本地保存
- 🎨 Material Design 3:现代化UI设计
- 📱 双标签页:所有音乐和播放列表分离
- 🎯 快速操作:长按菜单快速操作
- ⏱️ 时长显示:格式化显示播放时间
效果展示


依赖配置
首先在 pubspec.yaml 中添加依赖:
dependencies:
flutter:
sdk: flutter
audioplayers: ^6.0.0 # 音频播放
file_picker: ^8.0.0+1 # 文件选择
shared_preferences: ^2.2.2 # 数据存储
permission_handler: ^11.3.0 # 权限管理
运行命令安装依赖:
flutter pub get
数据模型设计
1. 音乐信息模型
class Music {
String id;
String title;
String artist;
String path;
Duration duration;
DateTime addedAt;
Music({
required this.id,
required this.title,
required this.artist,
required this.path,
required this.duration,
required this.addedAt,
});
}
字段说明:
id:唯一标识符title:歌曲标题artist:艺术家名称path:文件路径duration:歌曲时长addedAt:添加时间
2. 播放列表模型
class Playlist {
String id;
String name;
List<String> musicIds;
DateTime createdAt;
Playlist({
required this.id,
required this.name,
required this.musicIds,
required this.createdAt,
});
}
字段说明:
id:播放列表IDname:播放列表名称musicIds:包含的音乐ID列表createdAt:创建时间
核心功能实现
1. 音频播放器初始化
final AudioPlayer _audioPlayer = AudioPlayer();
void _initAudioPlayer() {
// 监听播放状态
_audioPlayer.onPlayerStateChanged.listen((state) {
setState(() {
_playerState = state;
_isPlaying = state == PlayerState.playing;
});
});
// 监听总时长
_audioPlayer.onDurationChanged.listen((duration) {
setState(() {
_totalDuration = duration;
});
});
// 监听播放进度
_audioPlayer.onPositionChanged.listen((position) {
setState(() {
_currentPosition = position;
});
});
// 监听播放完成
_audioPlayer.onPlayerComplete.listen((event) {
_onSongComplete();
});
}
AudioPlayer事件:
onPlayerStateChanged:播放状态变化onDurationChanged:总时长变化onPositionChanged:播放位置变化onPlayerComplete:播放完成
2. 播放音乐
Future<void> _playMusic(Music music, int index) async {
try {
await _audioPlayer.stop();
await _audioPlayer.play(DeviceFileSource(music.path));
setState(() {
_currentMusic = music;
_currentIndex = index;
_isPlaying = true;
});
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('播放失败: $e')),
);
}
}
}
播放流程:
- 停止当前播放
- 使用
DeviceFileSource加载本地文件 - 更新当前音乐和索引
- 捕获异常并提示用户
3. 播放控制
// 播放/暂停
Future<void> _togglePlayPause() async {
if (_isPlaying) {
await _audioPlayer.pause();
} else {
await _audioPlayer.resume();
}
}
// 下一首
Future<void> _playNext() async {
if (_currentPlaylist.isEmpty) return;
int nextIndex;
if (_isShuffle) {
nextIndex = DateTime.now().millisecondsSinceEpoch % _currentPlaylist.length;
} else {
nextIndex = (_currentIndex + 1) % _currentPlaylist.length;
}
await _playMusic(_currentPlaylist[nextIndex], nextIndex);
}
// 上一首
Future<void> _playPrevious() async {
if (_currentPlaylist.isEmpty) return;
int prevIndex;
if (_isShuffle) {
prevIndex = DateTime.now().millisecondsSinceEpoch % _currentPlaylist.length;
} else {
prevIndex = (_currentIndex - 1 + _currentPlaylist.length) % _currentPlaylist.length;
}
await _playMusic(_currentPlaylist[prevIndex], prevIndex);
}
// 进度跳转
Future<void> _seekTo(Duration position) async {
await _audioPlayer.seek(position);
}
播放模式:
- 顺序播放:按索引顺序播放
- 随机播放:随机选择下一首
- 单曲循环:播放完成后重新播放
4. 歌曲完成处理
void _onSongComplete() {
if (_isRepeat) {
_audioPlayer.seek(Duration.zero);
_audioPlayer.resume();
} else {
_playNext();
}
}
5. 添加音乐文件
Future<void> _pickMusicFiles() async {
try {
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.audio,
allowMultiple: true,
);
if (result != null) {
for (var file in result.files) {
if (file.path != null) {
final music = Music(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: file.name.replaceAll(RegExp(r'\.[^.]+$'), ''),
artist: '未知艺术家',
path: file.path!,
duration: Duration.zero,
addedAt: DateTime.now(),
);
setState(() {
_allMusic.add(music);
_currentPlaylist.add(music);
});
}
}
await _saveData();
}
} catch (e) {
// 错误处理
}
}
FilePicker参数:
type: FileType.audio:只选择音频文件allowMultiple: true:允许多选
6. 数据持久化
Future<void> _loadData() async {
final prefs = await SharedPreferences.getInstance();
// 加载音乐列表
final musicJson = prefs.getString('music_list');
if (musicJson != null) {
final List<dynamic> decoded = json.decode(musicJson);
setState(() {
_allMusic = decoded.map((item) => Music.fromJson(item)).toList();
_currentPlaylist = List.from(_allMusic);
});
}
// 加载播放列表
final playlistsJson = prefs.getString('playlists');
if (playlistsJson != null) {
final List<dynamic> decoded = json.decode(playlistsJson);
setState(() {
_playlists = decoded.map((item) => Playlist.fromJson(item)).toList();
});
}
}
Future<void> _saveData() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(
'music_list',
json.encode(_allMusic.map((m) => m.toJson()).toList()),
);
await prefs.setString(
'playlists',
json.encode(_playlists.map((p) => p.toJson()).toList()),
);
}
7. 播放列表管理
// 创建播放列表
void _createPlaylist() {
final controller = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('创建播放列表'),
content: TextField(
controller: controller,
decoration: const InputDecoration(
labelText: '播放列表名称',
hintText: '例如:我的最爱',
border: OutlineInputBorder(),
),
autofocus: true,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
FilledButton(
onPressed: () {
if (controller.text.isNotEmpty) {
final playlist = Playlist(
id: DateTime.now().millisecondsSinceEpoch.toString(),
name: controller.text,
musicIds: [],
createdAt: DateTime.now(),
);
setState(() {
_playlists.add(playlist);
});
_saveData();
Navigator.pop(context);
}
},
child: const Text('创建'),
),
],
),
);
}
// 添加到播放列表
void _addToPlaylist(Music music) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('添加到播放列表'),
content: ListView.builder(
itemCount: _playlists.length,
itemBuilder: (context, index) {
final playlist = _playlists[index];
final isAdded = playlist.musicIds.contains(music.id);
return ListTile(
title: Text(playlist.name),
subtitle: Text('${playlist.musicIds.length} 首歌曲'),
trailing: isAdded
? const Icon(Icons.check, color: Colors.green)
: null,
onTap: () {
setState(() {
if (isAdded) {
playlist.musicIds.remove(music.id);
} else {
playlist.musicIds.add(music.id);
}
});
_saveData();
Navigator.pop(context);
},
);
},
),
),
);
}
// 加载播放列表
void _loadPlaylist(Playlist playlist) {
final playlistMusic = _allMusic
.where((music) => playlist.musicIds.contains(music.id))
.toList();
setState(() {
_currentPlaylist = playlistMusic;
});
}
UI组件设计
1. 音乐列表
Widget _buildMusicList() {
if (_allMusic.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.music_off,
size: 100,
color: Colors.grey[400],
),
const SizedBox(height: 24),
Text(
'还没有音乐',
style: TextStyle(
fontSize: 18,
color: Colors.grey[600],
),
),
const SizedBox(height: 24),
FilledButton.icon(
onPressed: _pickMusicFiles,
icon: const Icon(Icons.add),
label: const Text('添加音乐'),
),
],
),
);
}
return ListView.builder(
itemCount: _allMusic.length,
itemBuilder: (context, index) {
final music = _allMusic[index];
final isPlaying = _currentMusic?.id == music.id && _isPlaying;
return ListTile(
leading: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: isPlaying
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
isPlaying ? Icons.equalizer : Icons.music_note,
color: isPlaying
? Theme.of(context).colorScheme.primary
: null,
),
),
title: Text(
music.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(music.artist),
trailing: PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
onSelected: (value) {
switch (value) {
case 'add_to_playlist':
_addToPlaylist(music);
break;
case 'delete':
_deleteMusic(music);
break;
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'add_to_playlist',
child: Row(
children: [
Icon(Icons.playlist_add),
SizedBox(width: 8),
Text('添加到播放列表'),
],
),
),
const PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete, color: Colors.red),
SizedBox(width: 8),
Text('删除'),
],
),
),
],
),
onTap: () => _playMusic(music, index),
);
},
);
}
设计要点:
- 正在播放的歌曲高亮显示
- 使用equalizer图标表示播放中
- PopupMenuButton提供快捷操作
2. 播放控制器
Widget _buildPlayerControls() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 歌曲信息
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_currentMusic!.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
_currentMusic!.artist,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
const SizedBox(height: 8),
// 进度条
Row(
children: [
Text(_formatDuration(_currentPosition)),
Expanded(
child: Slider(
value: _currentPosition.inMilliseconds.toDouble(),
max: _totalDuration.inMilliseconds.toDouble(),
onChanged: (value) {
_seekTo(Duration(milliseconds: value.toInt()));
},
),
),
Text(_formatDuration(_totalDuration)),
],
),
// 控制按钮
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
icon: Icon(
_isShuffle ? Icons.shuffle_on_outlined : Icons.shuffle,
color: _isShuffle ? Theme.of(context).colorScheme.primary : null,
),
onPressed: () {
setState(() {
_isShuffle = !_isShuffle;
});
},
tooltip: '随机播放',
),
IconButton(
icon: const Icon(Icons.skip_previous),
iconSize: 36,
onPressed: _playPrevious,
tooltip: '上一首',
),
IconButton(
icon: Icon(
_isPlaying ? Icons.pause_circle_filled : Icons.play_circle_filled,
),
iconSize: 56,
onPressed: _togglePlayPause,
tooltip: _isPlaying ? '暂停' : '播放',
),
IconButton(
icon: const Icon(Icons.skip_next),
iconSize: 36,
onPressed: _playNext,
tooltip: '下一首',
),
IconButton(
icon: Icon(
_isRepeat ? Icons.repeat_one : Icons.repeat,
color: _isRepeat ? Theme.of(context).colorScheme.primary : null,
),
onPressed: () {
setState(() {
_isRepeat = !_isRepeat;
});
},
tooltip: '单曲循环',
),
],
),
],
),
);
}
控制按钮:
- 随机播放:shuffle图标
- 上一首:skip_previous
- 播放/暂停:play_circle_filled / pause_circle_filled
- 下一首:skip_next
- 单曲循环:repeat / repeat_one
3. 时长格式化
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final minutes = twoDigits(duration.inMinutes.remainder(60));
final seconds = twoDigits(duration.inSeconds.remainder(60));
return '$minutes:$seconds';
}
格式示例:
- 65秒 → “01:05”
- 125秒 → “02:05”
- 3665秒 → “61:05”
4. 标签页布局
DefaultTabController(
length: 2,
child: Column(
children: [
const TabBar(
tabs: [
Tab(text: '所有音乐', icon: Icon(Icons.music_note)),
Tab(text: '播放列表', icon: Icon(Icons.playlist_play)),
],
),
Expanded(
child: TabBarView(
children: [
_buildMusicList(),
_buildPlaylistsView(),
],
),
),
],
),
)
技术要点详解
1. AudioPlayer生命周期
void dispose() {
_audioPlayer.dispose();
super.dispose();
}
重要性:
- 释放音频资源
- 停止后台播放
- 避免内存泄漏
2. 播放状态管理
enum PlayerState {
stopped,
playing,
paused,
completed,
}
状态转换:
stopped → playing → paused → playing → completed → stopped
3. 文件路径处理
// 本地文件
DeviceFileSource(music.path)
// 网络文件
UrlSource('https://example.com/music.mp3')
// 资源文件
AssetSource('assets/music.mp3')
4. 权限处理
在 AndroidManifest.xml 中添加:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
在 Info.plist 中添加:
<key>NSAppleMusicUsageDescription</key>
<string>需要访问您的音乐库</string>
5. 后台播放
配置后台播放(Android):
<service android:name="xyz.luan.audioplayers.AudioplayersPlugin$AudioplayersService"
android:exported="false">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
应用场景
1. 离线音乐播放器
class OfflineMusicPlayer {
List<Music> downloadedMusic = [];
Future<void> downloadMusic(String url, String title) async {
// 下载音乐到本地
}
List<Music> getOfflineMusic() {
return downloadedMusic;
}
}
2. 播客播放器
class PodcastPlayer {
double playbackSpeed = 1.0;
Future<void> setSpeed(double speed) async {
await _audioPlayer.setPlaybackRate(speed);
}
Future<void> skipForward(Duration duration) async {
final newPosition = _currentPosition + duration;
await _audioPlayer.seek(newPosition);
}
}
3. 有声书播放器
class AudiobookPlayer {
Map<String, Duration> bookmarks = {};
void addBookmark(String name, Duration position) {
bookmarks[name] = position;
}
Future<void> jumpToBookmark(String name) async {
if (bookmarks.containsKey(name)) {
await _audioPlayer.seek(bookmarks[name]!);
}
}
}
功能扩展建议
1. 歌词显示
class Lyrics {
List<LyricLine> lines;
Lyrics(this.lines);
String getCurrentLine(Duration position) {
for (var line in lines) {
if (position >= line.startTime && position < line.endTime) {
return line.text;
}
}
return '';
}
}
class LyricLine {
Duration startTime;
Duration endTime;
String text;
LyricLine(this.startTime, this.endTime, this.text);
}
2. 均衡器
class Equalizer {
Map<String, double> bands = {
'60Hz': 0.0,
'230Hz': 0.0,
'910Hz': 0.0,
'3.6kHz': 0.0,
'14kHz': 0.0,
};
void setBand(String frequency, double gain) {
bands[frequency] = gain;
// 应用均衡器设置
}
void applyPreset(String preset) {
switch (preset) {
case 'Rock':
bands = {'60Hz': 5.0, '230Hz': 3.0, '910Hz': -2.0, '3.6kHz': 4.0, '14kHz': 5.0};
break;
case 'Pop':
bands = {'60Hz': -1.0, '230Hz': 2.0, '910Hz': 4.0, '3.6kHz': 4.0, '14kHz': -1.0};
break;
}
}
}
3. 睡眠定时器
class SleepTimer {
Timer? _timer;
void setTimer(Duration duration) {
_timer?.cancel();
_timer = Timer(duration, () {
_audioPlayer.stop();
});
}
void cancelTimer() {
_timer?.cancel();
}
}
4. 音乐可视化
import 'package:flutter_audio_visualizer/flutter_audio_visualizer.dart';
class MusicVisualizer extends StatelessWidget {
final AudioPlayer player;
const MusicVisualizer({required this.player});
Widget build(BuildContext context) {
return AudioVisualizer(
audioPlayer: player,
barCount: 50,
barColor: Colors.blue,
barWidth: 3,
barSpace: 2,
);
}
}
5. 在线音乐搜索
class MusicSearch {
Future<List<Music>> searchOnline(String query) async {
final response = await http.get(
Uri.parse('https://api.example.com/search?q=$query'),
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
return (data['results'] as List)
.map((item) => Music.fromJson(item))
.toList();
}
return [];
}
}
6. 音乐标签编辑
import 'package:audiotagger/audiotagger.dart';
class MusicTagger {
final Audiotagger tagger = Audiotagger();
Future<void> updateTags(String path, {
String? title,
String? artist,
String? album,
}) async {
await tagger.writeTags(
path: path,
tagModel: TagModel(
title: title,
artist: artist,
album: album,
),
);
}
Future<TagModel?> readTags(String path) async {
return await tagger.readTags(path: path);
}
}
性能优化
1. 预加载下一首
void _preloadNext() {
if (_currentIndex < _currentPlaylist.length - 1) {
final nextMusic = _currentPlaylist[_currentIndex + 1];
// 预加载但不播放
_audioPlayer.setSourceDeviceFile(nextMusic.path);
}
}
2. 缓存专辑封面
class AlbumArtCache {
static final Map<String, Uint8List> _cache = {};
static Future<Uint8List?> getAlbumArt(String musicPath) async {
if (_cache.containsKey(musicPath)) {
return _cache[musicPath];
}
// 从文件提取封面
final art = await extractAlbumArt(musicPath);
if (art != null) {
_cache[musicPath] = art;
}
return art;
}
}
3. 懒加载音乐列表
ListView.builder(
itemCount: _allMusic.length,
itemBuilder: (context, index) {
if (index >= _allMusic.length) {
return const CircularProgressIndicator();
}
return _buildMusicItem(_allMusic[index]);
},
)
常见问题解答
Q1: 如何支持更多音频格式?
A: audioplayers支持大多数常见格式(MP3、WAV、AAC、OGG等),由底层平台决定。
Q2: 如何实现后台播放?
A: 需要配置平台特定的后台服务,并使用audio_service包。
Q3: 如何获取音频元数据?
A: 使用flutter_audio_metadata或audiotagger包读取ID3标签。
Q4: 播放网络音乐需要注意什么?
A: 需要处理网络延迟、缓冲、断网重连等情况。
项目结构
lib/
├── main.dart # 主程序入口
├── models/
│ ├── music.dart # 音乐模型
│ └── playlist.dart # 播放列表模型
├── screens/
│ ├── music_player_page.dart # 播放器页面
│ ├── playlist_detail_page.dart # 播放列表详情
│ └── settings_page.dart # 设置页面
├── widgets/
│ ├── music_list_item.dart # 音乐列表项
│ ├── player_controls.dart # 播放控制器
│ ├── progress_bar.dart # 进度条
│ └── playlist_card.dart # 播放列表卡片
└── utils/
├── audio_manager.dart # 音频管理
├── storage_helper.dart # 存储辅助
└── format_helper.dart # 格式化工具
总结
本文实现了一个功能完整的音乐播放器应用,涵盖了以下核心技术:
- AudioPlayer:音频播放和控制
- FilePicker:本地文件选择
- 播放控制:播放、暂停、上下首、进度控制
- 播放模式:顺序、随机、循环
- 播放列表:创建、管理、加载
- 数据持久化:SharedPreferences存储
- 状态管理:播放状态和进度监听
通过本项目,你不仅学会了如何实现音乐播放器应用,还掌握了Flutter中音频处理、文件操作、状态管理的核心技术。这些知识可以应用到更多多媒体应用的开发。
享受你的音乐时光!
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐





所有评论(0)