跨平台音乐播放器 MVP,支持 Windows / Linux / macOS / iOS / Android 五大平台。用户可以播放本地音乐文件和在线音乐流,管理播放列表和收藏,并获得原生级别的系统媒体控制集成。
| 层级 | 技术选型 | 版本 |
|---|---|---|
| 框架 | Flutter | 3.41.5 |
| 语言 | Dart | 3.11.3 |
| 状态管理 | Riverpod 3.x | ^3.3.1 (NotifierProvider API) |
| 路由 | GoRouter | ^17.1.0 |
| 音频引擎 | just_audio + audio_service | ^0.10.5 / ^0.18.18 |
| 本地存储 | Hive | ^2.2.3 |
| UI 规范 | Material Design 3 | — |
- 25 个 Dart 源文件,共计 2,920 行代码
flutter analyze零错误零警告flutter build linux构建通过
┌─────────────────────────────────────────────────────────────┐
│ Flutter UI Layer │
│ Material 3 · 响应式布局 · 键盘快捷键 │
│ ┌──────────┬──────────┬───────────┬────────────┬─────────┐ │
│ │ HomePage │ Library │ SearchPage│ Playlist │ Player │ │
│ │ │ Page │ │ Page │ Page │ │
│ └──────────┴──────────┴───────────┴────────────┴─────────┘ │
│ │ MiniPlayer (全局常驻) │ │
├─────────────────────────────────────────────────────────────┤
│ State Layer (Riverpod) │
│ ┌───────────────┬────────────────┬──────────────────────┐ │
│ │ Player │ Library │ Playlist │ │
│ │ Providers │ Providers │ Providers │ │
│ │ (20+ 个) │ (3 个) │ (3 个) │ │
│ └───────┬───────┴────────┬───────┴──────────┬───────────┘ │
├──────────┼────────────────┼──────────────────┼──────────────┤
│ │ Data / Service Layer │ │
│ ┌───────▼───────┐ ┌──────▼──────┐ ┌─────────▼─────────┐ │
│ │ AudioHandler │ │ LocalMusic │ │ PlaylistService │ │
│ │ (just_audio │ │ Service │ │ (Hive CRUD) │ │
│ │ + audio_svc) │ │ (文件扫描) │ │ │ │
│ └───────┬───────┘ └─────────────┘ └───────────────────┘ │
│ │ ┌─────────────┐ │
│ │ │ OnlineMusic │ │
│ │ │ Service │ │
│ │ │ (API/Mock) │ │
│ │ └─────────────┘ │
├──────────┼──────────────────────────────────────────────────┤
│ │ Platform Layer │
│ ┌───────▼──────────────────────────────────────────────┐ │
│ │ Linux: GStreamer │ Android: ExoPlayer │ iOS: AVFound │ │
│ │ Windows: WinRT │ macOS: AVFoundation │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
lib/
├── main.dart # 应用入口
├── core/
│ ├── router/app_router.dart # GoRouter 路由配置
│ ├── theme/app_theme.dart # Material 3 主题
│ └── utils/format_duration.dart # 工具函数
├── features/
│ ├── home/ui/
│ │ ├── home_page.dart # 首页(快捷入口)
│ │ └── shell_page.dart # 响应式 Shell(导航栏/侧栏)
│ ├── library/
│ │ ├── data/local_music_service.dart # 本地音乐扫描
│ │ ├── providers/library_providers.dart # 本地歌曲 Provider
│ │ └── ui/library_page.dart # 本地音乐列表
│ ├── online/
│ │ ├── data/online_music_service.dart # 在线音乐 API
│ │ ├── providers/online_providers.dart # 搜索 Provider
│ │ └── ui/search_page.dart # 搜索页面
│ ├── player/
│ │ ├── data/audio_handler.dart # just_audio ↔ audio_service 桥接
│ │ ├── providers/player_providers.dart # 20+ 播放状态 Provider
│ │ └── ui/
│ │ ├── mini_player.dart # 底部迷你播放栏
│ │ ├── player_page.dart # 全屏播放页
│ │ └── queue_page.dart # 播放队列(可拖拽排序)
│ └── playlist/
│ ├── data/playlist_service.dart # Hive CRUD 服务
│ ├── providers/playlist_providers.dart # 播放列表 + 收藏 Provider
│ └── ui/
│ ├── playlist_page.dart # 播放列表管理
│ └── playlist_detail_page.dart # 播放列表详情
└── shared/
├── models/
│ ├── song.dart # Song 数据模型
│ └── playlist.dart # Playlist 数据模型
└── widgets/
├── song_list_tile.dart # 通用歌曲列表项
└── keyboard_shortcuts.dart # 桌面键盘快捷键
采用 Feature-First 分层架构,每个功能模块独立包含 data/、providers/、ui/ 三层:
| 层 | 职责 | 依赖方向 |
|---|---|---|
| UI | Widget 渲染、用户交互 | 依赖 Providers |
| Providers | 状态管理、业务逻辑编排 | 依赖 Data |
| Data | 数据获取(文件系统/网络/数据库) | 不依赖上层 |
shared/ 目录存放跨模块共享的数据模型和通用组件。
文件: lib/features/player/data/audio_handler.dart (339 行)
核心类 MusicPlayerHandler 继承 BaseAudioHandler,混入 SeekHandler 和 QueueHandler,实现 just_audio 与 audio_service 的双向桥接。
audio_service
┌───────────────────┐
│ PlaybackState │ ──→ 系统通知/锁屏控件
│ MediaItem │ ──→ 蓝牙/耳机按钮
│ Queue │
└───────┬───────────┘
│ 双向同步
┌───────────▼───────────┐
│ MusicPlayerHandler │
│ │
│ • play / pause │
│ • skipToNext/Prev │
│ • seek │
│ • loadPlaylist() │
│ • setRepeatMode() │
│ • setShuffleMode() │
└───────────┬───────────┘
│
┌───────▼───────┐
│ just_audio │
│ AudioPlayer │
└───────────────┘
关键实现细节:
-
状态同步:通过
Rx.combineLatest3组合playingStream、processingStateStream、playbackEventStream,实时生成PlaybackState广播给系统。 -
URI 路由:
_createAudioSource(MediaItem)根据 URI 前缀自动选择:/或file://→AudioSource.file()(本地文件)http:///https://→AudioSource.uri()(网络流)
-
队列管理:使用
AudioPlayer的原生队列 API(addAudioSource/removeAudioSourceAt/insertAudioSource),避免已废弃的ConcatenatingAudioSource手动管理。 -
时长修正:
_listenToDuration()监听实际解码时长,自动更新MediaItem.duration并同步回 queue。
文件: lib/features/player/providers/player_providers.dart (250 行)
提供 20+ 个 Riverpod Provider,分为三类:
| 类别 | Provider | 类型 | 用途 |
|---|---|---|---|
| 核心 | audioHandlerProvider |
Provider<MusicPlayerHandler> |
Handler 单例(main.dart 中 override) |
| 流式 | currentMediaItemProvider |
StreamProvider<MediaItem?> |
当前曲目 |
playbackStateProvider |
StreamProvider<PlaybackState> |
播放状态 | |
positionProvider |
StreamProvider<Duration> |
当前位置 | |
bufferedPositionProvider |
StreamProvider<Duration> |
缓冲位置 | |
durationProvider |
StreamProvider<Duration?> |
总时长 | |
queueProvider |
StreamProvider<List<MediaItem>> |
播放队列 | |
| 派生 | isPlayingProvider |
Provider<bool> |
是否正在播放 |
currentSongProvider |
Provider<Song?> |
当前 Song 模型 | |
progressProvider |
Provider<double> |
播放进度 0.0~1.0 | |
hasNextProvider |
Provider<bool> |
是否有下一曲 | |
hasPreviousProvider |
Provider<bool> |
是否有上一曲 | |
| 状态 | playModeProvider |
NotifierProvider<PlayModeNotifier, PlayMode> |
播放模式管理 |
播放模式状态机:
sequential ──→ repeatAll ──→ repeatOne ──→ shuffle ──→ sequential
│ │ │ │
└── LoopMode.off LoopMode.all LoopMode.one shuffle=true
文件: lib/features/library/data/local_music_service.dart (75 行)
- 递归扫描
~/Music目录 - 支持格式:
.mp3,.flac,.wav,.ogg,.m4a - 文件名解析策略:
- 匹配
Artist - Title.ext模式 → 自动提取艺术家和标题 - 不匹配 → 文件名作为标题,艺术家设为 "Unknown Artist"
- 匹配
- 生成唯一 ID:文件路径 hashCode 的十六进制表示
通过 LibrarySortNotifier + sortedLocalSongsProvider 实现三种排序:
- 按标题 (title)
- 按艺术家 (artist)
- 按专辑 (album)
文件: lib/features/online/data/online_music_service.dart (92 行)
MVP 阶段使用 Mock 实现,内置 8 首 SoundHelix 公共域示例音乐:
| ID | 曲名 | 艺术家 | 专辑 |
|---|---|---|---|
| demo-1 | Ambient Soundscape | SoundHelix | Demo Collection |
| demo-2 | Electronic Dreams | SoundHelix | Demo Collection |
| demo-3 | Classical Fusion | SoundHelix | Instrumental Vibes |
| demo-4 | Jazz Exploration | Melody Makers | Instrumental Vibes |
| demo-5 | Rock Anthem | Melody Makers | Power Tracks |
| demo-6 | Chill Lofi Beat | LoFi Studio | Relaxation |
| demo-7 | Synthwave Runner | Retro Synth | Neon Nights |
| demo-8 | Acoustic Morning | Nature Sounds | Relaxation |
搜索支持标题/艺术家/专辑的大小写不敏感匹配,模拟 300ms 网络延迟。
后续扩展:替换为真实 API(如 NeteaseCloudMusicApi、Spotify Web API)仅需修改此文件。
searchResultsProvider 监听 searchQueryProvider,仅当查询长度 >= 2 时触发搜索,避免过于频繁的请求。
文件: lib/features/playlist/data/playlist_service.dart (120 行)
使用 Hive 作为轻量 NoSQL 存储:
| Hive Box | Key | Value | 用途 |
|---|---|---|---|
playlists |
playlist UUID | JSON string (Playlist) | 用户播放列表 |
favorites |
song ID | JSON string (Song) | 收藏歌曲 |
序列化方案:使用 dart:convert 的 jsonEncode/jsonDecode,配合 Song.toJson()/Song.fromJson() 和 Playlist.toJson()/Playlist.fromJson()。不使用 Hive TypeAdapter,避免了 hive_generator 的依赖冲突。
createPlaylist(name) → 生成 UUID v4 → 存入 Hive
deletePlaylist(id) → 从 Hive 删除
renamePlaylist(id, newName) → 读取 → 修改 → 写回
addSongToPlaylist(id, song) → 去重检查 → 追加 → 写回
removeSongFromPlaylist(id, songId) → 过滤 → 写回
收藏本质上是一个特殊的 "歌曲集合",以 song ID 为 key 独立存储在 favorites box 中:
toggleFavorite(song): 存在则删除,不存在则添加isFavorite(songId): O(1) 查询,利用 Hive box 的containsKey
┌─────────────────────────────────────────────┐
│ App │
│ ┌─────────────────────────────────────────┐│
│ │ StatefulShellRoute (IndexedStack) ││
│ │ ┌─────┬─────────┬────────┬──────────┐ ││
│ │ │Home │ Library │ Search │ Playlists│ ││
│ │ │ / │/library │/search │/playlists│ ││
│ │ └─────┴─────────┴────────┴──┬───────┘ ││
│ │ │ ││
│ │ /playlists/:id ││
│ └─────────────────────────────────────────┘│
│ │
│ /player ← 全屏播放页(滑入动画,独立导航栈) │
└─────────────────────────────────────────────┘
使用 StatefulShellRoute.indexedStack 确保各 Tab 页面状态独立保持(切换 Tab 不会销毁/重建)。
全屏播放页 /player 使用 parentNavigatorKey 指向根导航器,配合 CustomTransitionPage 实现从底部滑入的动画效果。
文件: lib/features/home/ui/shell_page.dart (104 行)
| 屏幕宽度 | 导航形式 | MiniPlayer 位置 |
|---|---|---|
| < 800px (手机/小平板) | 底部 NavigationBar |
NavigationBar 上方 |
| >= 800px (桌面/大平板) | 左侧 NavigationRail |
底部全宽 |
移动端布局: 桌面端布局:
┌──────────────────┐ ┌──┬────────────────┐
│ │ │ │ │
│ Content │ │N │ Content │
│ │ │a │ │
│ │ │v │ │
├──────────────────┤ │ │ │
│ MiniPlayer │ │R │ │
├──────────────────┤ │a │ │
│ NavigationBar │ │i │ │
│ 🏠 📁 🔍 📋 │ │l │ │
└──────────────────┘ ├──┴────────────────┤
│ MiniPlayer │
└───────────────────┘
┌──────────────────────────────────┐
│ ▼ 收起 🎵 队列 │ ← AppBar
│ │
│ ┌──────────────────┐ │
│ │ │ │
│ │ Album Cover │ │
│ │ 300 x 300 │ │
│ │ │ │
│ └──────────────────┘ │
│ │
│ 歌曲标题 (headlineSmall) │
│ 艺术家 (titleMedium) │
│ │
│ 0:45 ───●────────────── 3:21 │ ← ProgressBar (可拖拽)
│ │
│ ⏮ ▶ (FAB) ⏭ │ ← 主控制
│ │
│ 🔀 🔁 │ ← 模式切换
│ │
│ 🔉 ─────────────────── 🔊 │ ← 音量 (仅桌面端)
└──────────────────────────────────┘
桌面端独有功能:
- 音量滑块(
StreamBuilder实时监听player.volumeStream) - 仅在
Platform.isLinux || Platform.isMacOS || Platform.isWindows时显示
┌─────────────────────────────────┐
│ ═══════●═══════════════════════ │ ← 2px 进度条
│ 🎵 歌曲名 - 艺术家 ▶/⏸ │ ← 点击进入全屏
└─────────────────────────────────┘
- 仅当有曲目加载时显示(否则返回
SizedBox.shrink()) - 点击主区域通过
context.push('/player')打开全屏播放页
ReorderableListView.builder实现拖拽排序- 当前播放项高亮(
primaryContainer背景色) - AppBar "Clear Queue" 按钮清空队列
基于 ColorScheme.fromSeed(seedColor: Colors.deepPurple),自动派生完整的 Material 3 色彩系统:
| 属性 | 亮色模式 | 暗色模式 |
|---|---|---|
| 主色调 | Deep Purple 派生 | Deep Purple 派生 (darkMode) |
| Material 3 | 启用 | 启用 |
| AppBar | Surface 背景 | Surface 背景 |
| 导航栏 | 系统默认 M3 | 系统默认 M3 |
| 卡片 | 圆角 12px | 圆角 12px |
支持跟随系统自动切换亮色/暗色(ThemeMode.system)。
class Song {
final String id; // 唯一标识(本地:路径 hash;在线:API ID)
final String title; // 歌曲标题
final String? artist; // 艺术家
final String? album; // 专辑名
final Duration? duration; // 时长
final String? uri; // 播放地址(本地路径或 URL)
final String? artworkUrl; // 封面图地址
final bool isLocal; // 是否本地文件
}
提供与 audio_service.MediaItem 的双向转换:
Song.fromMediaItem(MediaItem)— 从系统通知/播放控制反向构建song.toMediaItem()— 转为 MediaItem 用于播放引擎
class Playlist {
final String id; // UUID v4
final String name; // 播放列表名称
final List<Song> songs; // 歌曲列表
final DateTime createdAt; // 创建时间
}
文件: lib/shared/widgets/keyboard_shortcuts.dart (85 行)
| 按键 | 功能 |
|---|---|
Space |
播放/暂停 |
← |
后退 5 秒 |
→ |
前进 5 秒 |
↑ |
音量 +0.1 |
↓ |
音量 -0.1 |
N |
下一曲 |
P |
上一曲 |
通过 Focus widget 的 onKeyEvent 实现,仅处理 KeyDownEvent 避免重复触发。
audio_service 在 Linux 上自动通过 D-Bus 实现 MPRIS 协议,无需额外代码即可支持:
- 系统媒体通知
- 桌面环境媒体控件(如 GNOME 顶栏)
- 蓝牙/耳机媒体按钮
main() async {
WidgetsFlutterBinding.ensureInitialized();
// 1. 初始化 Hive 文件存储
await Hive.initFlutter();
// 2. 打开播放列表和收藏的 Hive Box
await PlaylistService.init();
// 3. 初始化 audio_service,注册 MusicPlayerHandler
final audioHandler = await AudioService.init(
builder: () => MusicPlayerHandler(),
config: AudioServiceConfig(...),
);
// 4. 启动应用,注入 audioHandler 到 Riverpod
runApp(
ProviderScope(
overrides: [audioHandlerProvider.overrideWithValue(audioHandler)],
child: const MusicPlayerApp(),
),
);
}
依赖注入关键点:audioHandlerProvider 在定义时抛出 UnimplementedError,必须在 ProviderScope.overrides 中注入实际实例。这确保了 Handler 的单例生命周期由 AudioService.init 管理,而非 Riverpod。
# 核心
flutter_riverpod: ^3.3.1 # 状态管理
go_router: ^17.1.0 # 声明式路由
just_audio: ^0.10.5 # 跨平台音频播放
audio_service: ^0.18.18 # 后台播放 + 系统媒体控制
just_audio_background: ^0.0.1 # just_audio ↔ audio_service 桥接
# 数据
hive: ^2.2.3 # 轻量 NoSQL 存储
hive_flutter: ^1.1.0 # Hive Flutter 适配
uuid: ^4.5.3 # 播放列表 ID 生成
# UI
audio_video_progress_bar: ^2.0.3 # 音频进度条
cached_network_image: ^3.4.1 # 网络图片缓存
# 工具
rxdart: ^0.28.0 # 响应式流组合
on_audio_query: ^2.9.0 # 音频元数据查询(预留)
path_provider: ^2.1.5 # 平台路径
| 优先级 | 功能 | 说明 |
|---|---|---|
| P0 | 真实在线 API 对接 | 替换 Mock 为 NeteaseCloudMusicApi 或 Spotify |
| P0 | 音频元数据读取 | 使用 on_audio_query 或 id3 包读取 MP3 标签和封面 |
| P1 | 歌词显示 | LRC 格式逐行高亮同步 |
| P1 | 搜索历史 | Hive 持久化搜索记录 |
| 优先级 | 功能 | 说明 |
|---|---|---|
| P1 | 均衡器 | just_audio AndroidEqualizer / iOS 原生均衡器 |
| P1 | 睡眠定时器 | 定时暂停播放 |
| P2 | 跨设备同步 | 通过 Firebase/Supabase 同步播放列表 |
| P2 | 自定义主题色 | 用户可选 seed color,或从封面提取主色调 |
| 优先级 | 功能 | 说明 |
|---|---|---|
| P2 | 播客支持 | RSS 订阅、章节标记、播放速度 |
| P2 | CarPlay / Android Auto | 车载适配 |
| P3 | 社交功能 | 分享歌单、协作播放列表 |
| P3 | 本地网络播放 | DLNA/AirPlay 推送 |
- Flutter SDK >= 3.41.0
- Dart SDK >= 3.11.0
- Linux: GStreamer 开发库 (
libgstreamer1.0-dev) - macOS: Xcode >= 15
- Windows: Visual Studio 2022 with Desktop C++ workload
# 获取依赖
flutter pub get
# Linux 桌面运行
flutter run -d linux
# Android 运行
flutter run -d <device_id>
# 构建 Release
flutter build linux
flutter build apk
flutter build ios
flutter build macos
flutter build windows
将音乐文件放入 ~/Music 目录,支持格式:.mp3, .flac, .wav, .ogg, .m4a。
文件名建议使用 艺术家 - 标题.mp3 格式以获得最佳解析效果。
在 Search 页面输入以下关键词试听示例音乐:
sound— 匹配 SoundHelix 系列jazz— Jazz Explorationchill— Chill Lofi Beatrelaxation— 匹配 Relaxation 专辑下的歌曲