Bili Music — 用 Flutter 打造一款优雅的 B 站音乐播放器
一个基于 Bilibili API 的跨平台音乐播放器。
前言
作为一名 B 站重度用户,我一直觉得 B 站上大量优质的音乐内容(翻唱、VOCALOID、原创曲)值得一个专属的播放体验。主流音乐 App 虽然强大,但 B 站的音乐生态有着独特的魅力——那些 UP 主精心制作的 MV、弹幕互动的氛围、收藏夹里沉淀的宝藏曲目,都值得被更好地呈现。
于是,Bili Music 诞生了。这是一款使用 Flutter 构建的跨平台音乐播放器,直接对接 Bilibili 的音频接口,为 B 站音乐内容提供沉浸式的播放体验。
本文将完整记录这个项目的技术选型、架构设计和核心功能实现。
技术栈一览
| 层级 | 技术方案 | 说明 |
|---|---|---|
| 框架 | Flutter 3.11+ / Dart 3.11+ | 跨平台 UI 框架 |
| 状态管理 | Riverpod + Code Generation | 响应式状态管理 |
| 路由 | GoRouter + ShellRoute | 声明式路由 |
| 音频引擎 | just_audio + audio_service | 播放内核 + 系统媒体会话 |
| 网络 | Dio + Cookie Jar | HTTP 客户端 + 会话管理 |
| 本地存储 | Hive (KV) + Drift (SQLite) | 轻量 KV + 关系型数据库 |
| UI | Material 3 + flutter_animate | 动态主题 + 声明式动画 |
架构设计
项目采用经典的四层架构,每一层职责清晰、边界明确:
lib/
├── main.dart # 应用入口
├── models/ # 数据层 — 数据模型与序列化
├── services/ # 服务层 — API 封装与业务逻辑
├── providers/ # 逻辑层 — Riverpod 状态管理
├── pages/ # 展示层 — 页面级组件
├── widgets/ # 展示层 — 可复用组件
└── utils/ # 工具类 — 通用辅助函数
核心设计原则:
- 单一职责:每个 Provider 只管理一类状态,每个 Service 只封装一个业务域
- 依赖倒置:UI 层依赖 Provider 抽象,不直接调用 Service
- 数据驱动:UI 完全由 Riverpod 的状态变化驱动渲染
核心功能实现
1. 搜索与发现
首页集成了搜索和内容发现功能,支持关键词实时搜索,搜索结果按类型分类展示。

搜索模块的技术亮点:
- 防抖搜索:通过
SearchProvider实现输入防抖,避免频繁 API 请求 - 搜索历史:使用 Hive 持久化搜索记录,支持快速回溯
- 骨架屏:搜索结果加载时展示 Shimmer 骨架屏,提升感知速度
2. Bilibili 账号登录
通过导入 Bilibili 的 SESSDATA 实现账号关联,登录后即可同步个人收藏夹数据。

登录后,App 会自动拉取你在 B 站的收藏夹列表,你可以直接浏览和管理这些收藏夹中的音乐内容。

3. 歌单管理
歌单系统支持自定义歌单创建和 Bilibili 收藏夹导入,实现了完整的内容管理闭环。

创建歌单与导入收藏夹:
支持两种歌单来源——本地新建和从 B 站收藏夹导入:


歌单管理的技术实现:
- 双数据源:自定义歌单存储在 Drift 数据库,Bilibili 收藏夹通过 API 实时拉取
- JSON 导入:支持导入 Bilibili 收藏夹的 JSON 数据,自动解析并合并为本地歌单
- 拖拽排序:歌单内曲目支持拖拽重排,交互体验流畅
- 离线缓存:歌单元数据持久化,无网络时仍可浏览已缓存歌单
4. 播放列表与队列
正在播放的队列管理,支持实时查看和操作当前播放列表。

5. 沉浸式播放体验
播放器是整个 App 的灵魂,分为 Mini 播放条 和 全屏播放器 两种形态。
Mini 播放条
底部常驻的迷你播放条,展示当前曲目信息,上滑即可展开全屏播放器。

全屏播放器
全屏播放器提供了沉浸式的音乐体验:

播放器的技术实现:
- 毛玻璃背景:使用
BackdropFilter实现封面图色调的磨砂玻璃效果 - 旋转唱片动画:播放时封面旋转,暂停时停止,过渡自然
- 同步滚动歌词:逐行高亮当前歌词,自动滚动跟随播放进度
- 圆形频谱可视化:基于音频数据的实时圆形频谱动画
- 预加载机制:当前歌曲播放时后台预加载下一首,实现无缝切歌
6. 播放模式与定时器
支持多种播放模式(顺序、随机、单曲循环)和睡眠定时功能。

7. 下载管理
支持将喜欢的歌曲下载到本地,实现真正的离线播放。

下载功能的技术实现:
- 队列管理:多任务并行下载,支持队列排序和优先级调整
- 进度追踪:实时显示每首歌曲的下载进度和状态
- 断点续传:下载中断后可恢复,避免重复下载
- 存储优化:已下载歌曲可直接离线播放,减少流量消耗
8. 视频评论与弹幕
除了纯音频播放,App 还支持查看原视频的评论和弹幕内容,保留了 B 站社区互动的核心体验。

9. 主题系统 — Material 3 动态配色
完整的主题定制系统,支持亮色/暗色模式切换和自定义主题色。

主题系统的架构设计:
- Material 3 规范:严格遵循 Google Material Design 3 设计语言
- 动态取色:基于用户选择的主题色,自动生成完整的 ColorScheme
- 系统跟随:支持跟随系统深色模式自动切换
- 持久化:主题偏好存储在 Hive,重启后自动恢复
亮色与暗色模式
App 提供了精心设计的亮色和暗色两套主题,每一套都经过细致的视觉调优。
亮色模式

暗色模式

两套主题并非简单的颜色反转,而是在对比度、层次感、视觉舒适度上都做了针对性优化。暗色模式下特别注意了:
- 背景层次通过不同深度的灰度区分,而非纯黑
- 文字对比度满足 WCAG AA 标准
- 强调色在深色背景上适当提亮,保证视觉突出
Bilibili API 对接 — 核心挑战
对接 Bilibili API 是本项目最具挑战性的部分,需要解决几个关键技术问题。
WBI 签名机制
Bilibili 的部分接口使用 WBI 签名 进行请求校验,这是一种基于密钥对的参数签名方案:
class WbiSigner {// 从 Bilibili 获取密钥对Future<WbiKeys> fetchKeys() async { ... }// 对请求参数进行签名String sign(Map<String, dynamic> params, WbiKeys keys) {final mixinKey = _getMixinKey(keys);params['wts'] = DateTime.now().millisecondsSinceEpoch ~/ 1000;final sorted = params.entries.toList()..sort((a, b) => a.key.compareTo(b.key));final query = sorted.map((e) => '${e.key}=${e.value}').join('&');return md5.convert(utf8.encode(query + mixinKey)).toString();}
}
Cookie 与设备指纹
Bilibili API 需要正确的 Cookie 和设备指纹才能正常工作:
- 自动设备指纹生成:首次启动时自动生成
buvid3、buvid4等设备标识 - SESSDATA 导入:支持用户手动导入登录凭证,解锁个人收藏夹等需要登录的接口
- Cookie 持久化:使用
cookie_jar管理会话,避免重复认证
音频流防盗链
Bilibili 的音频流有 Referer 校验,必须正确设置请求头:
final response = await dio.get(audioUrl, options: Options(headers: {'Referer': 'https://www.bilibili.com/'},
));
音频系统架构
音频播放是整个 App 的基础设施,采用分层设计:
┌─────────────────────────────────┐
│ PlayerProvider (769行) │ ← 状态管理层
│ 播放状态 / 队列 / 模式 / 进度 │
├─────────────────────────────────┤
│ AudioService + just_audio │ ← 播放引擎层
│ 播放控制 / 后台服务 / 媒体会话 │
├─────────────────────────────────┤
│ BilibiliAudioService │ ← 数据获取层
│ 音频URL / 缓存 / 预加载 │
└─────────────────────────────────┘
后台播放
通过 audio_service 实现完整的系统媒体会话集成:
- 通知栏控制:播放/暂停、上一首/下一首
- 锁屏控制:系统锁屏界面的媒体控件
- 音频焦点:来电、闹钟等场景自动暂停,结束后恢复
- 蓝牙控制:车载/耳机蓝牙按键完整支持
离线播放
- 歌单级别的离线缓存:将音频文件下载到本地
- 缓存命中时直接播放本地文件,无网络依赖
- 断点续传:下载中断后可恢复
智能预加载
// 当前歌曲播放到 80% 时,开始预加载下一首
if (position.inSeconds >= duration.inSeconds * 0.8) {_preloadNext();
}
数据持久化策略
项目采用 Hive + Drift 双数据库策略,各取所长:
| 场景 | 存储方案 | 原因 |
|---|---|---|
| 用户设置 | Hive | 简单 KV,读写频繁 |
| 播放状态 | Hive | 需要快速恢复 |
| 搜索历史 | Hive | 简单列表结构 |
| 歌单/曲目 | Drift (SQLite) | 关系型查询 |
| 下载记录 | Drift (SQLite) | 事务与索引需求 |
| 音频缓存 | 文件系统 | 大文件存储 |
状态恢复机制:App 启动时从 Hive 读取上次的播放状态(当前曲目、进度、播放模式),实现无缝恢复。
开发中的经验与踩坑
1. 状态管理的粒度把控
PlayerProvider 最终膨胀到了 769 行,这是一个需要反思的设计。后续计划拆分为:
PlaybackStateProvider— 播放/暂停/进度QueueProvider— 播放队列管理PlayerModeProvider— 播放模式/定时器
2. Bilibili API 的不稳定性
Bilibili 的非公开 API 随时可能变更,需要做好:
- 接口版本化封装
- 优雅的错误降级
- 关键数据的本地缓存兜底
3. Flutter 动画的性能优化
全屏播放器包含多层动画(旋转唱片、歌词滚动、频谱可视化),需要:
- 合理使用
AnimatedBuilder限制重建范围 - 频谱数据做降采样处理
- 毛玻璃效果使用
RepaintBoundary隔离重绘
写在最后
Bili Music 是一个纯粹出于个人需求驱动的 Side Project,但在这个过程中,我深入实践了 Flutter 的状态管理、音频处理、动画系统、本地存储等核心技术。从架构设计到细节打磨,每一步都是真实的工程挑战。
如果你也是 B 站音乐爱好者,或者对 Flutter 音频开发感兴趣,欢迎交流探讨。
本文写于 2026 年 5 月,基于项目当时的开发状态。
