Flutter中如何缓存自定义语音消息Widget?
问题描述
我用Dart开发聊天App,使用audio_waveforms: ^1.0.5编写自定义语音消息Widget时遇到两个问题:
- 无法有效缓存Widget,加载耗时极长;
- 发送新语音消息时组件异常,显示上一条消息内容,需重新渲染页面才恢复正常。
以下是我的代码:
import 'dart:async'; import 'dart:io'; import 'package:http/http.dart' as http; import 'package:audio_waveforms/audio_waveforms.dart'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; class AudioMessageBeta extends StatefulWidget { final String id; final String audioUrl; final String message; const AudioMessageBeta( {super.key, required this.audioUrl, required this.message, required this.id}); @override State<AudioMessageBeta> createState() => _AudioMessageBetaState(); } class _AudioMessageBetaState extends State<AudioMessageBeta> { bool isLoading = false; final PlayerController playerController = new PlayerController(); StreamSubscription<PlayerState>? playerStateSubsription; Future<dynamic> downloadFile(String url) async { String dir = (await getApplicationDocumentsDirectory()).path; if (await File("$dir/${widget.id}.mp3").exists()) { return "$dir/${widget.id}.mp3"; } else { File file = File("$dir/${widget.id}.mp3"); var request = await http.get( Uri.parse(url), ); var bytes = request.bodyBytes; await file.writeAsBytes(bytes); return file.path; } } void prepareVoice() async { await playerController.preparePlayer( path: await downloadFile(widget.audioUrl), shouldExtractWaveform: true, noOfSamples: (MediaQuery.of(context).size.width * 0.5) ~/ 5); } @override void initState() { isLoading = true; Future.delayed(const Duration(milliseconds: 200), () { prepareVoice(); playerStateSubsription = playerController.onPlayerStateChanged.listen((PlayerState) { setState(() {}); }); }); super.initState(); isLoading = false; } @override void dispose() { if (mounted) { playerController.dispose(); playerStateSubsription!.cancel(); } super.dispose(); } void playVoice() async { switch (playerController.playerState) { case PlayerState.playing: await playerController.pausePlayer(); break; default: await playerController.startPlayer(finishMode: FinishMode.pause); } } @override Widget build(BuildContext context) { return Container( constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.75, ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: ListTile( contentPadding: const EdgeInsets.all(0), horizontalTitleGap: 8, leading: GestureDetector( onTap: () => playVoice(), child: SizedBox( height: 45, width: 45, child: CircleAvatar( backgroundColor: Theme.of(context).colorScheme.background, child: isLoading ? const CircularProgressIndicator() : Icon(playerController.playerState.isPlaying ? Icons.pause_rounded : Icons.play_arrow), ), ), ), title: Padding( padding: const EdgeInsets.symmetric(vertical: 5), child: AudioFileWaveforms( size: Size(MediaQuery.of(context).size.width * 0.5, 25), playerController: playerController, enableSeekGesture: true, continuousWaveform: false, waveformType: WaveformType.fitWidth, playerWaveStyle: PlayerWaveStyle( fixedWaveColor: Theme.of(context).colorScheme.background, liveWaveColor: Colors.white, spacing: 5, waveThickness: 2.5, scaleFactor: 200, ), ), ), ), ), ); } }
解决方案
一、解决缓存与加载耗时问题
修正加载状态管理逻辑
原代码中initState里直接设置isLoading = false会导致加载状态提前结束,应该在音频准备完成后再更新状态:void prepareVoice() async { setState(() => isLoading = true); try { final filePath = await downloadFile(widget.audioUrl); await playerController.preparePlayer( path: filePath, shouldExtractWaveform: true, noOfSamples: (MediaQuery.of(context).size.width * 0.5) ~/ 5, ); } catch (e) { print('音频加载失败: $e'); } finally { setState(() => isLoading = false); } }同时删除
initState里的延迟和手动状态设置,直接调用prepareVoice:@override void initState() { super.initState(); prepareVoice(); playerStateSubsription = playerController.onPlayerStateChanged.listen((_) { setState(() {}); }); }优化文件下载的异常处理
给下载逻辑增加异常捕获,避免因网络问题或请求失败卡住流程:Future<String> downloadFile(String url) async { final dir = (await getApplicationDocumentsDirectory()).path; final file = File("$dir/${widget.id}.mp3"); if (await file.exists()) { return file.path; } try { final response = await http.get(Uri.parse(url)); if (response.statusCode == 200) { await file.writeAsBytes(response.bodyBytes); return file.path; } else { throw Exception('请求失败,状态码: ${response.statusCode}'); } } catch (e) { throw Exception('下载失败: $e'); } }
二、解决新消息显示旧内容的问题
该问题是ListView复用Widget导致的状态混淆,需从控制器绑定和列表key两方面修复:
根据消息ID重置播放器控制器
当Widget的消息ID变化时,销毁旧控制器并创建新的:// 将控制器改为可重新赋值的类型 late PlayerController playerController; @override void initState() { super.initState(); playerController = PlayerController(); prepareVoice(); playerStateSubsription = playerController.onPlayerStateChanged.listen((_) { setState(() {}); }); } @override void didUpdateWidget(covariant AudioMessageBeta oldWidget) { super.didUpdateWidget(oldWidget); if (widget.id != oldWidget.id) { playerController.dispose(); playerStateSubsription?.cancel(); playerController = PlayerController(); prepareVoice(); playerStateSubsription = playerController.onPlayerStateChanged.listen((_) { setState(() {}); }); } }给列表项设置唯一Key
在聊天列表中使用AudioMessageBeta时,必须传入以消息ID为值的Key,避免复用错误:AudioMessageBeta( key: ValueKey(message.id), audioUrl: message.audioUrl, message: message.content, id: message.id, )
三、额外优化建议
- 可以在聊天页面初始化时预加载最近的几条语音,缩短用户等待时间;
- 定期清理应用目录中过期的音频缓存,避免占用过多存储空间;
- 用
FutureBuilder替代手动状态管理,更优雅地处理异步加载流程。
内容的提问来源于stack exchange,提问作者Giovanni Fittizio
相关产品推荐
相关产品推荐

