如何用Flutter+Riverpod+JustAudio实现同步的音乐播放界面?
基于Riverpod + JustAudio 实现跨页面同步的音乐播放器方案
核心思路:用Riverpod管理全局播放状态
把AudioPlayer实例和播放相关状态(当前歌曲、播放进度、播放状态等)全部交给Riverpod的Provider统一管理,让所有UI组件(主播放页、迷你播放器)都从Provider读取状态、调用操作方法,彻底解耦UI与播放器实例。
具体实现步骤
1. 创建全局播放状态Provider
先定义包含播放器和核心状态的类,用StateNotifierProvider提供全局访问:
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:just_audio/just_audio.dart'; // 自定义歌曲模型,按需扩展字段 class Song { final String title; final String artist; final String audioUrl; Song({required this.title, required this.artist, required this.audioUrl}); } // 封装播放核心状态 class PlayerState { final AudioPlayer player; final Song? currentSong; final Duration? position; final Duration? duration; final PlayerState playerStatus; PlayerState({ required this.player, this.currentSong, this.position, this.duration, required this.playerStatus, }); // 状态复制方法,用于局部更新 PlayerState copyWith({ AudioPlayer? player, Song? currentSong, Duration? position, Duration? duration, PlayerState? playerStatus, }) { return PlayerState( player: player ?? this.player, currentSong: currentSong ?? this.currentSong, position: position ?? this.position, duration: duration ?? this.duration, playerStatus: playerStatus ?? this.playerStatus, ); } } // 状态管理类,处理播放器逻辑与状态同步 class PlayerStateNotifier extends StateNotifier<PlayerState> { PlayerStateNotifier() : super(PlayerState(player: AudioPlayer(), playerStatus: PlayerState.stopped)) { _initPlayerListeners(); } // 监听播放器的各类状态流,同步到StateNotifier void _initPlayerListeners() { state.player.playerStateStream.listen((status) { state = state.copyWith(playerStatus: status); }); state.player.positionStream.listen((pos) { state = state.copyWith(position: pos); }); state.player.durationStream.listen((dur) { state = state.copyWith(duration: dur); }); } // 加载并播放指定歌曲 Future<void> playSong(Song song) async { await state.player.setUrl(song.audioUrl); state = state.copyWith(currentSong: song); await state.player.play(); } // 暂停播放 void pause() => state.player.pause(); // 恢复播放 void resume() => state.player.play(); // 切换播放/暂停状态 void togglePlayPause() { state.playerStatus.playing ? pause() : resume(); } @override void dispose() { state.player.dispose(); super.dispose(); } } // 全局Provider,所有UI组件通过它获取播放状态与操作能力 final playerStateProvider = StateNotifierProvider<PlayerStateNotifier, PlayerState>((ref) { return PlayerStateNotifier(); });
2. 实现迷你播放器组件
迷你播放器仅从Provider读取状态,无需持有播放器实例:
class MiniPlayer extends ConsumerWidget { const MiniPlayer({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final playerState = ref.watch(playerStateProvider); final currentSong = playerState.currentSong; // 无播放歌曲时隐藏迷你播放器 if (currentSong == null) return const SizedBox.shrink(); return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), color: Colors.grey[900], child: Row( children: [ const SizedBox(width: 48, height: 48, child: ColoredBox(color: Colors.blue)), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(currentSong.title, style: const TextStyle(color: Colors.white, fontSize: 14)), Text(currentSong.artist, style: TextStyle(color: Colors.grey[400], fontSize: 12)), ], ), ), IconButton( icon: Icon( playerState.playerStatus.playing ? Icons.pause : Icons.play_arrow, color: Colors.white, ), onPressed: () => ref.read(playerStateProvider.notifier).togglePlayPause(), ), ], ), ); } }
3. 实现主播放页面
主播放页同样通过Provider获取状态,所有操作调用Notifier方法:
class PlayerScreen extends ConsumerWidget { const PlayerScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final playerState = ref.watch(playerStateProvider); final currentSong = playerState.currentSong; if (currentSong == null) { return const Center(child: Text('暂无播放歌曲')); } return Scaffold( appBar: AppBar(title: const Text('正在播放')), body: Column( children: [ Expanded( child: Center( child: ColoredBox(color: Colors.blue, width: 200, height: 200), ), ), Padding( padding: const EdgeInsets.all(16), child: Column( children: [ Text(currentSong.title, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 8), Text(currentSong.artist, style: TextStyle(color: Colors.grey[600])), ], ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Slider( value: playerState.position?.inMilliseconds.toDouble() ?? 0, max: playerState.duration?.inMilliseconds.toDouble() ?? 100, onChanged: (value) { ref.read(playerStateProvider.notifier).state.player.seek(Duration(milliseconds: value.toInt())); }, ), ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( icon: const Icon(Icons.skip_previous, size: 40), onPressed: () { /* 实现上一曲逻辑 */ }, ), IconButton( icon: Icon( playerState.playerStatus.playing ? Icons.pause_circle : Icons.play_circle, size: 60, ), onPressed: () => ref.read(playerStateProvider.notifier).togglePlayPause(), ), IconButton( icon: const Icon(Icons.skip_next, size: 40), onPressed: () { /* 实现下一曲逻辑 */ }, ), ], ), const SizedBox(height: 32), ], ), ); } }
4. 全局嵌入迷你播放器
将迷你播放器放在全局布局的底部(比如Scaffold的bottomNavigationBar),确保所有页面都能显示并同步状态:
class MainApp extends StatelessWidget { const MainApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: const Navigator(/* 你的页面路由逻辑 */), bottomNavigationBar: const MiniPlayer(), ), ); } }
关键注意事项
- 状态同步:确保播放器的所有流(状态、进度、时长)都正确监听并同步到
PlayerState,保证UI实时响应变化。 - 资源释放:在
PlayerStateNotifier的dispose方法中必须调用AudioPlayer.dispose(),避免内存泄漏。 - 单一实例:通过
StateNotifierProvider确保整个应用只有一个AudioPlayer实例,避免多实例冲突。 - 错误处理:在
playSong等方法中加入try-catch,处理音频加载失败的情况,可新增状态字段提示用户。
内容的提问来源于stack exchange,提问作者Proud boy
相关产品推荐
相关产品推荐

