You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

解决Flutter中just_audio与flutter_riverpod的'Cannot add new events after calling close'错误

错误成因分析

这个错误的核心原因是音频播放器(_audioPlayer)已经被释放(dispose),但代码仍尝试调用它的方法,主要存在两个触发场景:

  1. 竞态条件:playSound中先检查!_isDisposed,但检查完成后、执行setAsset前,TimerNotifier的dispose方法被调用,导致音频播放器被关闭,后续操作触发错误。
  2. 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);
});
验证方案
  1. 点击音效按钮时,若播放器已释放,会捕获错误并打印日志,不会崩溃。
  2. 确保TimerNotifier在活跃状态时,音频播放器可用;被销毁时,不会影响音频播放器的生命周期(如果用分离方案)。

内容的提问来源于stack exchange,提问作者pomoworko.com

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 09:05:46