Flutter中errors_patch.dart的_throw方法异常排查求助
异常原因
数组索引越界
章节号从1开始计数,但widget.audioBookData["chapters"]是0-based数组。你直接用章节号+1作为数组索引,会跳过正确位置甚至超出数组长度触发异常。比如当前章节号为1(对应数组索引0),取下一章时你用索引2,直接访问了第3章的位置;如果当前是最后一章,章节号+1会超出数组最大索引,直接抛出越界错误。未清理旧的Stream订阅
每次调用setupPlayer都会重新监听positionStream,但旧订阅未取消,导致多个订阅同时运行。切换章节时旧订阅仍会触发逻辑,造成状态混乱甚至未处理异常。Player资源未重置
切换章节时直接调用setUrl,未先重置Player释放旧音频资源,容易引发资源冲突导致内部异常。空指针未处理
直接强制解包player.duration!,若音频加载失败duration为null,会触发空指针异常。
解决步骤
修正数组索引逻辑
章节号转数组索引时用章节号-1,取下一章时计算当前索引再加1,匹配数组的0-based规则。管理Stream订阅
定义StreamSubscription?变量保存订阅,每次调用setupPlayer前取消旧订阅,避免多订阅冲突。切换章节时重置Player
在setupPlayer开头调用player.reset(),释放旧资源后再加载新音频。添加空安全与异常捕获
对player.duration做空判断,在异步操作(Firebase下载、Player设置)中加入try-catch,避免未处理异常。
修复后的完整代码
import 'dart:convert'; import 'dart:io'; import 'package:firebase_storage/firebase_storage.dart'; import 'package:flutter/material.dart'; import 'package:just_audio/just_audio.dart'; import 'package:path_provider/path_provider.dart'; class PlayAudioBookScreen extends StatefulWidget { final Map<String, dynamic> initialChapterData; final Map<String, dynamic> audioBookData; const PlayAudioBookScreen(this.initialChapterData, this.audioBookData, {super.key}); @override State<PlayAudioBookScreen> createState() => PlayAudioBookScreenState(); } class PlayAudioBookScreenState extends State<PlayAudioBookScreen> { AudioPlayer player = AudioPlayer(); bool playing = true; double sliderPos = 0; String bookTitle = ""; String chapterTitle = ""; String author = ""; String audioFileLocalPath = ""; String audioFileFirebasePath = ""; int audioFileDuration = 0; // 单位:毫秒 Map<String, dynamic> currentChapterData = jsonDecode("{}"); StreamSubscription? positionSubscription; // 保存播放位置监听订阅 @override void initState() { bookTitle = widget.audioBookData["title"] ?? ""; author = widget.audioBookData["author"] ?? ""; currentChapterData = widget.initialChapterData; chapterTitle = currentChapterData["title"] ?? ""; audioFileLocalPath = currentChapterData["localPath"] ?? ""; audioFileFirebasePath = currentChapterData["firebasePath"] ?? ""; setupPlayer(); super.initState(); } void setupPlayer() async { // 取消旧的播放位置监听 await positionSubscription?.cancel(); try { // 重置播放器释放旧资源 await player.reset(); Directory appDocDir = await getApplicationDocumentsDirectory(); File audioFile = File("${appDocDir.path}/$audioFileLocalPath"); if (!await audioFile.exists()) { await audioFile.create(recursive: true); print("开始下载音频文件"); Reference audioFileFirebaseRef = FirebaseStorage.instance.ref(audioFileFirebasePath); await audioFileFirebaseRef.writeToFile(audioFile); } await player.setUrl(audioFile.path); // 等待音频时长加载完成 await player.durationFuture; // 空判断避免空指针 if (player.duration != null) { audioFileDuration = player.duration!.inMilliseconds; } else { print("音频文件加载失败,无法获取时长"); return; } // 监听播放位置 positionSubscription = player.positionStream.listen((position) { if (position.inMilliseconds >= audioFileDuration) { int currentChapterNum = int.tryParse(currentChapterData["chapterNumber"] ?? "0") ?? 0; int totalChapters = widget.audioBookData["chapters"]?.length ?? 0; // 修正索引逻辑:章节号从1开始,数组索引从0开始 if (currentChapterNum < totalChapters) { print("准备切换到下一章"); int currentIndex = currentChapterNum - 1; int nextIndex = currentIndex + 1; if (nextIndex < totalChapters) { setupNewChapter(widget.audioBookData["chapters"][nextIndex]); } } else { print("已播放到最后一章"); player.pause(); setState(() { sliderPos = audioFileDuration.toDouble(); playing = false; }); } } else { setState(() { sliderPos = position.inMilliseconds.toDouble(); }); } }); await player.play(); setState(() { playing = true; }); } catch (e, stackTrace) { print("播放设置出错: $e"); print(stackTrace); setState(() { playing = false; }); } } void setupNewChapter(Map<String, dynamic> chapterData) { print("开始切换章节"); setState(() { currentChapterData = chapterData; chapterTitle = currentChapterData["title"] ?? ""; audioFileLocalPath = currentChapterData["localPath"] ?? ""; audioFileFirebasePath = currentChapterData["firebasePath"] ?? ""; }); setupPlayer(); } @override void dispose() { // 页面销毁时清理资源 positionSubscription?.cancel(); player.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: Column(children: [ const SizedBox(height: 80), Text(bookTitle, style: const TextStyle(fontWeight: FontWeight.bold)), Text(chapterTitle, style: const TextStyle(fontWeight: FontWeight.bold)), Text("by $author"), const SizedBox(height: 20), playing ? IconButton( icon: const Icon(Icons.pause), onPressed: () async { await player.pause(); setState(() { playing = false; }); }, ) : IconButton( icon: const Icon(Icons.play_arrow), onPressed: () async { await player.play(); setState(() { playing = true; }); }, ), Slider( min: 0, max: audioFileDuration.toDouble(), value: sliderPos.clamp(0, audioFileDuration.toDouble()), // 防止滑块超出范围 onChanged: (value) async { await player.seek(Duration(milliseconds: value.toInt())); }), ]), ); } }
内容的提问来源于stack exchange,提问作者Ameen Izhac

