Flutter关闭modalBottomSheet触发setState() called after dispose()异常
问题根因
点击外部区域关闭modalBottomSheet时,弹窗内的AudioPlayerWidget会被销毁并执行dispose()生命周期,但音频播放器的事件流订阅没有被取消,播放过程中持续推送的进度更新、时长变化等事件仍会触发回调,回调内的setState()作用在已销毁的组件上,就会抛出setState() called after dispose()异常。
现有代码仅在dispose中释放了_audioPlayer实例,既没有提前取消事件流订阅,也没有在调用setState前校验组件挂载状态,是异常出现的直接原因。
修复方案
1. 持有流订阅引用,组件销毁时主动取消
在_AudioPlayerWidgetState类中新增成员变量存储所有事件流的订阅对象:
// 存储音频事件流订阅 StreamSubscription<Duration> _durationSub; StreamSubscription<Duration> _positionSub; StreamSubscription<void> _completionSub; StreamSubscription<String> _errorSub;
修改initState中的事件监听逻辑,将订阅返回值赋值给上述变量,同时在所有setState调用前增加组件挂载状态校验:
@override void initState() { _audioPlayer = AudioPlayer(mode: PlayerMode.MEDIA_PLAYER); _audioCache = AudioCache(fixedPlayer: _audioPlayer); _durationSub = _audioPlayer.onDurationChanged.listen((d) { if (!mounted) return; setState(() { _duration = d; }); }); _positionSub = _audioPlayer.onAudioPositionChanged.listen((p) { if (!mounted) return; setState((){ _position = p; }); }); _completionSub = _audioPlayer.onPlayerCompletion.listen((event) { if (!mounted) return; setState(() { _position = Duration(seconds: 0); _playerState = PlayerState.STOPPED; }); }); _errorSub = _audioPlayer.onPlayerError.listen((msg) { print('audioPlayer error : $msg'); if (!mounted) return; setState(() { _playerState = PlayerState.STOPPED; }); }); super.initState(); }
重写dispose方法,按顺序先取消所有流订阅,再停止音频播放、释放播放器资源:
@override void dispose() { // 取消所有事件订阅,阻断后续回调触发 _durationSub?.cancel(); _positionSub?.cancel(); _completionSub?.cancel(); _errorSub?.cancel(); // 组件销毁时停止播放,避免后台无感知占用音频资源 _audioPlayer?.stop(); _audioPlayer?.dispose(); super.dispose(); }
2. 全量setState调用增加挂载校验
所有存在异步等待逻辑的位置,在await返回后调用setState前,都必须先判断mounted属性为true,避免异步任务返回时组件已经销毁。_playPause方法修改后如下:
_playPause() async { if (_playerState == PlayerState.PLAYING) { final playerResult = await _audioPlayer.pause(); if (playerResult == 1 && mounted) { setState(() { _playerState = PlayerState.PAUSED; }); } } else if (_playerState == PlayerState.PAUSED) { final playerResult = await _audioPlayer.resume(); if (playerResult == 1 && mounted) { setState(() { _playerState = PlayerState.PLAYING; }); } } else { if (widget.isAsset) { _audioPlayer = await _audioCache.play(widget.url); if(mounted){ setState(() { _playerState = PlayerState.PLAYING; }); } } else { final playerResult = await _audioPlayer.play(widget.url, isLocal: _isLocal); if (playerResult == 1 && mounted) { setState(() { _playerState = PlayerState.PLAYING; }); } } } }
_stop方法修改后如下:
_stop() async { final playerResult = await _audioPlayer.stop(); if (playerResult == 1 && mounted) { setState(() { _playerState = PlayerState.STOPPED; }); } }
可选场景适配
如果业务需要弹窗关闭后音频继续后台播放,不需要在dispose中调用_audioPlayer.stop(),只需要将AudioPlayer实例上移到modalBottomSheet的父页面层级管理,播放器生命周期不随弹窗绑定即可,不会再触发该异常。
内容的提问来源于stack exchange,提问作者Laurent Thomas
相关产品推荐
相关产品推荐

