Flutter问题:使用just_audio实现多页面音频持续播放
解决just_audio页面切换时音频停止的问题
核心思路
把音频播放器实例从页面组件中抽离,放到全局状态管理容器或者单例类中,让它独立于页面生命周期存在。这样切换页面时,播放器实例不会被销毁,音频就能持续播放;同时通过全局状态来控制播放列表的切换逻辑。
具体实现步骤
1. 创建全局播放器实例(单例模式)
创建一个单例类统一管理播放器,确保整个APP中只有一个播放器实例,避免重复创建和销毁:
import 'package:just_audio/just_audio.dart'; import 'package:just_audio_background/just_audio_background.dart'; class AudioPlayerManager { // 单例实例 static final AudioPlayerManager _instance = AudioPlayerManager._internal(); factory AudioPlayerManager() => _instance; AudioPlayerManager._internal() { // 初始化播放器,复用你已有的后台播放配置 _player = AudioPlayer(); } late AudioPlayer _player; AudioPlayer get player => _player; // 标记当前播放列表ID,用于判断是否需要切换列表 String? _currentPlaylistId; // 加载播放列表:仅当新列表ID与当前不同时,才停止并切换 Future<void> loadPlaylist(String playlistId, List<AudioSource> sources) async { if (_currentPlaylistId == playlistId) { // 同一列表,直接恢复播放(如果当前未播放) if (!_player.playing) { await _player.play(); } return; } // 新列表:停止当前播放,加载新资源 await _player.stop(); await _player.setAudioSource( ConcatenatingAudioSource(children: sources), initialIndex: 0, initialPosition: Duration.zero, ); _currentPlaylistId = playlistId; await _player.play(); } // APP退出时调用,释放播放器资源 Future<void> dispose() async { await _player.dispose(); } }
2. 确认后台播放初始化
确保main函数中已正确初始化just_audio_background(你已实现,这里再做一次校验):
void main() async { await JustAudioBackground.init( androidNotificationChannelId: 'com.yourapp.channel.audio', androidNotificationChannelName: '音频播放', androidNotificationOngoing: true, ); runApp(const MyApp()); }
3. 在播放器页面使用全局实例
页面中不再创建本地播放器,直接调用全局的AudioPlayerManager:
class PlayerPage extends StatefulWidget { final String playlistId; final List<AudioSource> playlistSources; const PlayerPage({super.key, required this.playlistId, required this.playlistSources}); @override State<PlayerPage> createState() => _PlayerPageState(); } class _PlayerPageState extends State<PlayerPage> { late AudioPlayerManager _playerManager; @override void initState() { super.initState(); _playerManager = AudioPlayerManager(); // 进入页面时自动加载对应播放列表 _playerManager.loadPlaylist(widget.playlistId, widget.playlistSources); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('音频播放器')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // 播放状态控制按钮 StreamBuilder<PlayerState>( stream: _playerManager.player.playerStateStream, builder: (context, snapshot) { final playerState = snapshot.data; final processingState = playerState?.processingState; final playing = playerState?.playing; if (processingState == ProcessingState.loading || processingState == ProcessingState.buffering) { return const CircularProgressIndicator(); } else if (playing != true) { return IconButton( icon: const Icon(Icons.play_arrow), iconSize: 64.0, onPressed: _playerManager.player.play, ); } else if (processingState != ProcessingState.completed) { return IconButton( icon: const Icon(Icons.pause), iconSize: 64.0, onPressed: _playerManager.player.pause, ); } else { return IconButton( icon: const Icon(Icons.replay), iconSize: 64.0, onPressed: () => _playerManager.player.seek(Duration.zero), ); } }, ), // 可添加进度条、上/下一曲等其他控制组件 ], ), ), ); } // 页面销毁时不要释放播放器,全局实例需保留至APP退出 @override void dispose() { super.dispose(); } }
4. 切换播放列表的逻辑
当用户选择新播放列表时,调用全局管理器的loadPlaylist方法,自动完成切换判断:
// 示例:在播放列表选择页面的点击事件中 void onSelectPlaylist(String newPlaylistId, List<AudioSource> newSources) async { await AudioPlayerManager().loadPlaylist(newPlaylistId, newSources); // 可选:跳转到播放器页面 Navigator.push(context, MaterialPageRoute( builder: (context) => PlayerPage(playlistId: newPlaylistId, playlistSources: newSources) )); }
关键注意事项
- 全局播放器实例仅在APP完全退出时(可通过
WidgetsBindingObserver监听APP生命周期)调用dispose释放资源,避免内存泄漏。 - 单例模式是最简单的全局实例实现方式,也可使用Provider、Riverpod等状态管理库托管实例,效果一致。
- 已配置的
just_audio_background后台播放功能不受影响,全局实例的方式会延续其后台播放能力。
内容的提问来源于stack exchange,提问作者Rafael Schaffer Gimenes
相关产品推荐
相关产品推荐

