Flutter Music Player - 设计文档

1. 项目概述

1.1 产品定位

跨平台音乐播放器 MVP,支持 Windows / Linux / macOS / iOS / Android 五大平台。用户可以播放本地音乐文件和在线音乐流,管理播放列表和收藏,并获得原生级别的系统媒体控制集成。

1.2 技术栈

层级 技术选型 版本
框架 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

1.3 代码规模

  • 25 个 Dart 源文件,共计 2,920 行代码
  • flutter analyze 零错误零警告
  • flutter build linux 构建通过

2. 系统架构

2.1 整体架构图

┌─────────────────────────────────────────────────────────────┐
│                    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              │   │
│  └──────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
 

2.2 目录结构

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                # 桌面键盘快捷键
 

2.3 分层原则

采用 Feature-First 分层架构,每个功能模块独立包含 data/providers/ui/ 三层:

职责 依赖方向
UI Widget 渲染、用户交互 依赖 Providers
Providers 状态管理、业务逻辑编排 依赖 Data
Data 数据获取(文件系统/网络/数据库) 不依赖上层

shared/ 目录存放跨模块共享的数据模型和通用组件。


3. 核心模块设计

3.1 播放引擎 (Player Engine)

3.1.1 MusicPlayerHandler

文件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  │
                    └───────────────┘
 

关键实现细节

  1. 状态同步:通过 Rx.combineLatest3 组合 playingStreamprocessingStateStreamplaybackEventStream,实时生成 PlaybackState 广播给系统。

  2. URI 路由_createAudioSource(MediaItem) 根据 URI 前缀自动选择:

    • / 或 file:// → AudioSource.file() (本地文件)
    • http:// / https:// → AudioSource.uri() (网络流)
  3. 队列管理:使用 AudioPlayer 的原生队列 API(addAudioSource/removeAudioSourceAt/insertAudioSource),避免已废弃的 ConcatenatingAudioSource 手动管理。

  4. 时长修正_listenToDuration() 监听实际解码时长,自动更新 MediaItem.duration 并同步回 queue。

3.1.2 Player Providers

文件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
 

3.2 本地音乐库 (Library)

3.2.1 LocalMusicService

文件lib/features/library/data/local_music_service.dart (75 行)

  • 递归扫描 ~/Music 目录
  • 支持格式:.mp3.flac.wav.ogg.m4a
  • 文件名解析策略:
    • 匹配 Artist - Title.ext 模式 → 自动提取艺术家和标题
    • 不匹配 → 文件名作为标题,艺术家设为 "Unknown Artist"
  • 生成唯一 ID:文件路径 hashCode 的十六进制表示

3.2.2 排序系统

通过 LibrarySortNotifier + sortedLocalSongsProvider 实现三种排序:

  • 按标题 (title)
  • 按艺术家 (artist)
  • 按专辑 (album)

3.3 在线音乐 (Online)

3.3.1 OnlineMusicService

文件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)仅需修改此文件。

3.3.2 搜索防抖

searchResultsProvider 监听 searchQueryProvider,仅当查询长度 >= 2 时触发搜索,避免过于频繁的请求。

3.4 播放列表与收藏 (Playlist & Favorites)

3.4.1 数据持久化

文件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 的依赖冲突。

3.4.2 播放列表 CRUD

createPlaylist(name)         → 生成 UUID v4 → 存入 Hive
deletePlaylist(id)           → 从 Hive 删除
renamePlaylist(id, newName)  → 读取 → 修改 → 写回
addSongToPlaylist(id, song)  → 去重检查 → 追加 → 写回
removeSongFromPlaylist(id, songId) → 过滤 → 写回
 

3.4.3 收藏系统

收藏本质上是一个特殊的 "歌曲集合",以 song ID 为 key 独立存储在 favorites box 中:

  • toggleFavorite(song): 存在则删除,不存在则添加
  • isFavorite(songId): O(1) 查询,利用 Hive box 的 containsKey

4. UI 设计

4.1 导航结构

┌─────────────────────────────────────────────┐
│                    App                       │
│  ┌─────────────────────────────────────────┐│
│  │  StatefulShellRoute (IndexedStack)      ││
│  │  ┌─────┬─────────┬────────┬──────────┐ ││
│  │  │Home │ Library │ Search │ Playlists│ ││
│  │  │  /  │/library │/search │/playlists│ ││
│  │  └─────┴─────────┴────────┴──┬───────┘ ││
│  │                               │         ││
│  │                         /playlists/:id  ││
│  └─────────────────────────────────────────┘│
│                                              │
│  /player  ← 全屏播放页(滑入动画,独立导航栈)  │
└─────────────────────────────────────────────┘
 

使用 StatefulShellRoute.indexedStack 确保各 Tab 页面状态独立保持(切换 Tab 不会销毁/重建)。

全屏播放页 /player 使用 parentNavigatorKey 指向根导航器,配合 CustomTransitionPage 实现从底部滑入的动画效果。

4.2 响应式布局

文件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     │
                                  └───────────────────┘
 

4.3 全屏播放页

┌──────────────────────────────────┐
│  ▼ 收起              🎵 队列    │  ← AppBar
│                                  │
│       ┌──────────────────┐       │
│       │                  │       │
│       │   Album Cover    │       │
│       │    300 x 300     │       │
│       │                  │       │
│       └──────────────────┘       │
│                                  │
│         歌曲标题 (headlineSmall)  │
│         艺术家 (titleMedium)     │
│                                  │
│  0:45 ───●────────────── 3:21   │  ← ProgressBar (可拖拽)
│                                  │
│      ⏮       ▶ (FAB)       ⏭   │  ← 主控制
│                                  │
│         🔀          🔁          │  ← 模式切换
│                                  │
│    🔉 ─────────────────── 🔊    │  ← 音量 (仅桌面端)
└──────────────────────────────────┘
 

桌面端独有功能:

  • 音量滑块(StreamBuilder 实时监听 player.volumeStream
  • 仅在 Platform.isLinux || Platform.isMacOS || Platform.isWindows 时显示

4.4 MiniPlayer

┌─────────────────────────────────┐
│ ═══════●═══════════════════════ │  ← 2px 进度条
│ 🎵  歌曲名 - 艺术家       ▶/⏸ │  ← 点击进入全屏
└─────────────────────────────────┘
 
  • 仅当有曲目加载时显示(否则返回 SizedBox.shrink()
  • 点击主区域通过 context.push('/player') 打开全屏播放页

4.5 播放队列

  • ReorderableListView.builder 实现拖拽排序
  • 当前播放项高亮(primaryContainer 背景色)
  • AppBar "Clear Queue" 按钮清空队列

4.6 主题

基于 ColorScheme.fromSeed(seedColor: Colors.deepPurple),自动派生完整的 Material 3 色彩系统:

属性 亮色模式 暗色模式
主色调 Deep Purple 派生 Deep Purple 派生 (darkMode)
Material 3 启用 启用
AppBar Surface 背景 Surface 背景
导航栏 系统默认 M3 系统默认 M3
卡片 圆角 12px 圆角 12px

支持跟随系统自动切换亮色/暗色(ThemeMode.system)。


5. 数据模型

5.1 Song

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 用于播放引擎

5.2 Playlist

class Playlist {
  final String id;             // UUID v4
  final String name;           // 播放列表名称
  final List<Song> songs;      // 歌曲列表
  final DateTime createdAt;    // 创建时间
}
 

6. 桌面端适配

6.1 键盘快捷键

文件lib/shared/widgets/keyboard_shortcuts.dart (85 行)

按键 功能
Space 播放/暂停
后退 5 秒
前进 5 秒
音量 +0.1
音量 -0.1
N 下一曲
P 上一曲

通过 Focus widget 的 onKeyEvent 实现,仅处理 KeyDownEvent 避免重复触发。

6.2 MPRIS 集成 (Linux)

audio_service 在 Linux 上自动通过 D-Bus 实现 MPRIS 协议,无需额外代码即可支持:

  • 系统媒体通知
  • 桌面环境媒体控件(如 GNOME 顶栏)
  • 蓝牙/耳机媒体按钮

7. 应用启动流程

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。


8. 依赖关系总览

# 核心
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          # 平台路径
 

9. 后续演进路线

9.1 短期 (v1.1)

优先级 功能 说明
P0 真实在线 API 对接 替换 Mock 为 NeteaseCloudMusicApi 或 Spotify
P0 音频元数据读取 使用 on_audio_query 或 id3 包读取 MP3 标签和封面
P1 歌词显示 LRC 格式逐行高亮同步
P1 搜索历史 Hive 持久化搜索记录

9.2 中期 (v1.5)

优先级 功能 说明
P1 均衡器 just_audio AndroidEqualizer / iOS 原生均衡器
P1 睡眠定时器 定时暂停播放
P2 跨设备同步 通过 Firebase/Supabase 同步播放列表
P2 自定义主题色 用户可选 seed color,或从封面提取主色调

9.3 长期 (v2.0)

优先级 功能 说明
P2 播客支持 RSS 订阅、章节标记、播放速度
P2 CarPlay / Android Auto 车载适配
P3 社交功能 分享歌单、协作播放列表
P3 本地网络播放 DLNA/AirPlay 推送

10. 构建与运行

10.1 开发环境要求

  • 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

10.2 快速启动

# 获取依赖
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
 

10.3 测试本地音乐

将音乐文件放入 ~/Music 目录,支持格式:.mp3.flac.wav.ogg.m4a

文件名建议使用 艺术家 - 标题.mp3 格式以获得最佳解析效果。

10.4 测试在线搜索

在 Search 页面输入以下关键词试听示例音乐:

  • sound — 匹配 SoundHelix 系列
  • jazz — Jazz Exploration
  • chill — Chill Lofi Beat
  • relaxation — 匹配 Relaxation 专辑下的歌曲