解决Flutter中just_audio与flutter_riverpod的'Cannot add new events after calling close'错误
错误成因分析
这个错误的核心原因是音频播放器(_audioPlayer)已经被释放(dispose),但代码仍尝试调用它的方法,主要存在两个触发场景:
- 竞态条件:
playSound中先检查!_isDisposed,但检查完成后、执行setAsset前,TimerNotifier的dispose方法被调用,导致音频播放器被关闭,后续操作触发错误。 - Riverpod Provider生命周期问题:如果
timerNotifierProvider是AutoDispose类型,当页面没有监听该Provider时,它会被自动销毁,但页面仍通过ref.read获取到已销毁的实例并调用playSound,此时实例内的_audioPlayer已经被释放。
分步解决方法
1. 修复竞态条件,增加错误防护
在playSound中添加异常捕获,并使用互斥锁避免并发操作冲突:
// 在TimerNotifier类中添加互斥锁(需导入package:synchronized/synchronized.dart) final _playLock = Lock(); Future<void> playSound() async { await _playLock.synchronized(() async { if (_isDisposed) return; final selectedSound = ref.watch(selectedSoundProvider.notifier).state; try { await _audioPlayer.setAsset(selectedSound.path); _audioPlayer.play(); } catch (e) { // 针对性处理已释放的错误 if (e.toString().contains('Cannot add new events after calling close')) { print("音频播放器已释放,无法播放音效"); // 可选:若业务需要,可重新初始化播放器 // _audioPlayer = AudioPlayer(); // await _audioPlayer.setAsset(selectedSound.path); // _audioPlayer.play(); } else { rethrow; // 抛出其他未知错误 } } }); } // 修改dispose方法,确保锁同步 @override void dispose() { _playLock.synchronized(() { print('dispose is being called'); _mounted = false; _timer?.cancel(); _isDisposed = true; _audioPlayer.dispose(); }); super.dispose(); }
2. 修正Riverpod Provider生命周期
如果timerNotifierProvider是AutoDispose类型,需要在SettingsPage中保持对Provider的监听,防止其被意外销毁:
class SettingsPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { // 添加这行代码,保持TimerNotifierProvider活跃 ref.watch(timerNotifierProvider); return Scaffold( // ... 原有页面代码 ); } }
或者,将timerNotifierProvider改为非AutoDispose类型(全局Provider),使其生命周期与应用一致:
// 原AutoDispose写法: // final timerNotifierProvider = StateNotifierProvider.autoDispose<TimerNotifier, int>((ref) => ...); // 修改为非AutoDispose: final timerNotifierProvider = StateNotifierProvider<TimerNotifier, int>((ref) => ...);
3. 分离音频播放器的生命周期管理(推荐方案)
将音频播放器从TimerNotifier中剥离,单独用Riverpod Provider管理,避免与Timer的生命周期绑定:
// 单独定义音频播放器Provider final audioPlayerProvider = Provider<AudioPlayer>((ref) { final player = AudioPlayer(); // 当Provider被销毁时释放播放器 ref.onDispose(() { player.dispose(); }); return player; }); // 修改TimerNotifier的初始化,注入音频播放器 class TimerNotifier extends StateNotifier<int> { final AudioPlayer _audioPlayer; final Ref _ref; bool _isDisposed = false; Timer? _timer; TimerNotifier(this._ref, this._audioPlayer) : super(0); Future<void> playSound() async { if (_isDisposed) return; final selectedSound = _ref.watch(selectedSoundProvider.notifier).state; try { await _audioPlayer.setAsset(selectedSound.path); _audioPlayer.play(); } catch (e) { if (e.toString().contains('Cannot add new events after calling close')) { print("音频播放器已释放,无法播放音效"); } else { rethrow; } } } @override void dispose() { _isDisposed = true; _timer?.cancel(); // 不再在这里释放音频播放器,交给audioPlayerProvider处理 super.dispose(); } } // 修改TimerNotifierProvider的定义 final timerNotifierProvider = StateNotifierProvider<TimerNotifier, int>((ref) { final audioPlayer = ref.watch(audioPlayerProvider); return TimerNotifier(ref, audioPlayer); });
验证方案
- 点击音效按钮时,若播放器已释放,会捕获错误并打印日志,不会崩溃。
- 确保
TimerNotifier在活跃状态时,音频播放器可用;被销毁时,不会影响音频播放器的生命周期(如果用分离方案)。
内容的提问来源于stack exchange,提问作者pomoworko.com
相关产品推荐
相关产品推荐

