Flutter中如何避免build重复调用导致AudioPlayer音频重复播放
解决Flutter中build多次调用导致音频重复播放的问题
问题描述
我正在开发一款按顺序播放图片与音频列表的App,音频播放完成后切换至下一组图片和音频,通过setState()更新图片。每次调用setState()后,build()方法会被调用两次,导致音频重复播放。请问如何让播放逻辑在build时仅执行一次?
原代码:
class PlayingScreen extends StatefulWidget { final PostModel postModel; PlayingScreen({ Key? key, required this.postModel, }) : super(key: key); @override State<PlayingScreen> createState() => _PlayingScreenState(); } class _PlayingScreenState extends State<PlayingScreen> { final _audioPlayer = AudioPlayer(); List<String> _audios = []; // audio file path list int _playIndex = 0; @override void initState() { _getAudios(); super.initState(); } @override void dispose() { _audioPlayer.dispose(); super.dispose(); } @override Widget build(BuildContext context) { _play(_playIndex); return Scaffold( appBar: AppBar( title: Text(widget.postModel.title), ), body: InkWell( onTap: () { Get.back(); }, // image widget ), ), ); } Future _play(int index) async { await _audioPlayer.play(DeviceFileSource(_audios[index])); logger.d("current index: $_playIndex / ${_audios.length - 1}"); //complete event _audioPlayer.onPlayerComplete.listen((event) { logger.d("complet, next"); setState(() { if (_playIndex >= _imageSets.length - 1) { _playIndex = 0; logger.d("return first index"); } else { _playIndex++; } }); }); } }
核心问题分析
build()是Flutter用于构建UI的回调,会在setState()、屏幕旋转、父组件重建等多种场景下触发,绝对不能在build中执行播放、网络请求这类有副作用的逻辑,否则必然导致重复执行甚至异常。
修正方案
- 移除
build()中的_play()调用,将播放逻辑移到生命周期回调或音频完成事件中 - 在
initState()中启动首次播放,并仅监听一次音频完成事件,避免重复订阅 - 音频完成后直接触发下一次播放,无需依赖
build()执行播放逻辑
修正后的代码:
class PlayingScreen extends StatefulWidget { final PostModel postModel; PlayingScreen({ Key? key, required this.postModel, }) : super(key: key); @override State<PlayingScreen> createState() => _PlayingScreenState(); } class _PlayingScreenState extends State<PlayingScreen> { final _audioPlayer = AudioPlayer(); List<String> _audios = []; // 音频文件路径列表 int _playIndex = 0; StreamSubscription? _completeSubscription; // 保存订阅对象,用于取消监听 @override void initState() { super.initState(); // 音频列表加载完成后启动首次播放 _getAudios().then((_) { _playCurrent(); // 仅订阅一次音频完成事件,避免重复触发 _completeSubscription = _audioPlayer.onPlayerComplete.listen((_) { _playNext(); }); }); } @override void dispose() { _completeSubscription?.cancel(); // 取消订阅,防止内存泄漏 _audioPlayer.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.postModel.title), ), body: InkWell( onTap: () { Get.back(); }, // 展示当前索引对应的图片 child: // 替换为你的图片Widget,例如 Image.asset(_imageSets[_playIndex]) ), ); } // 异步加载音频列表 Future<void> _getAudios() async { // 替换为你的实际加载逻辑,比如从postModel中获取音频路径 _audios = await // 音频列表加载逻辑 } // 播放当前索引的音频 Future<void> _playCurrent() async { if (_audios.isEmpty) return; await _audioPlayer.play(DeviceFileSource(_audios[_playIndex])); logger.d("current index: $_playIndex / ${_audios.length - 1}"); } // 切换到下一个音频并播放 void _playNext() { setState(() { if (_playIndex >= _audios.length - 1) { _playIndex = 0; logger.d("return first index"); } else { _playIndex++; } }); // 状态更新后播放新音频 _playCurrent(); } }
关键说明
- 彻底移除
build()中的播放逻辑,从根源避免build多次调用带来的重复播放 - 使用
StreamSubscription管理音频完成事件的订阅,在dispose()中取消订阅,防止内存泄漏 - 首次播放放在
_getAudios()的回调中,确保音频列表加载完成后再执行播放 - 音频完成后直接调用
_playNext(),通过setState()更新索引后播放新音频,逻辑更清晰可控
内容的提问来源于stack exchange,提问作者Kevin Yang
相关产品推荐
相关产品推荐

