如何在ListView.builder指定卡片单独显示视频下载进度条?
解决ListView.builder中单个卡片显示下载进度的问题
看起来你遇到的核心问题是全局状态变量导致所有卡片共享下载状态,另外checkid函数没有返回值也会引发异常。下面是一步步的解决方案:
1. 替换全局状态为每个item单独的状态跟踪
原来的downloadMessage和_isDownloading是全局变量,所以所有卡片都会同步更新。我们改用两个Map来分别存储每个item的下载状态和进度信息:
// 替换原来的全局变量 Map<int, bool> _isDownloadingMap = {}; Map<int, String> _downloadMessageMap = {};
2. 修复checkid函数的返回值问题
你的checkid函数没有else分支,当条件不满足时会返回null,这会导致ListView.builder抛出异常。我们添加一个空占位返回:
Widget checkid(int index){ if(widget.data[index]["unit_id"].toString() == widget.unitid){ return articles(index); } else { // 条件不满足时返回空容器,避免null错误 return SizedBox.shrink(); } }
3. 修改下载逻辑,更新对应item的状态
在IconButton的点击事件中,我们更新对应index的状态,而不是全局状态,并且添加错误处理和文件名避免重复:
IconButton( icon: _isDownloadingMap[index] == true ? CircularProgressIndicator(size: 20,) // 下载中显示加载图标 : Icon(Icons.file_download), onPressed: _isDownloadingMap[index] == true ? null : () async { // 下载中禁用按钮 bool exist = await db.checkdataid(widget.data[index]["data_video_id"]); print(exist); if(exist == false){ VideoDetails videoDetail = VideoDetails(data_id: widget.data[index]["data_video_id"]); await networkUtils.videodetail(url, body:videoDetail.toMap()); } String videopath = await db.getvideopath(); String videourl = baseurl + videopath; print(videourl); // 初始化当前item的下载状态 setState(() { _isDownloadingMap[index] = true; _downloadMessageMap[index] = 'Starting download...'; }); var dir = await getApplicationDocumentsDirectory(); Dio dio = Dio(); try { await dio.download( videourl, '${dir.path}/video_${index}.ehb', // 文件名添加index,避免多个下载覆盖 onReceiveProgress: (actualbytes, totalbytes){ if(totalbytes != -1){ var percentage = (actualbytes/totalbytes * 100).floor(); setState(() { _downloadMessageMap[index] = 'Downloading ${percentage} %'; }); } } ); // 下载完成更新状态 setState(() { _downloadMessageMap[index] = 'Download completed!'; _isDownloadingMap[index] = false; }); } catch (e) { // 处理下载失败的情况 setState(() { _downloadMessageMap[index] = 'Download failed'; _isDownloadingMap[index] = false; }); print('Download error: $e'); } }, ), // 显示当前item的下载进度 Text( _downloadMessageMap[index] ?? '', style: TextStyle(fontSize: 12), )
4. 初始化状态(可选)
为了避免null值,你可以在initState中初始化每个item的状态:
@override void initState() { super.initState(); // 给每个item初始化下载状态 for(int i = 0; i < widget.data.length; i++){ _isDownloadingMap[i] = false; _downloadMessageMap[i] = ''; } }
完整修改后的代码
// 导入必要的包 import 'package:flutter/material.dart'; import 'package:dio/dio.dart'; import 'package:path_provider/path_provider.dart'; // 假设你的StatefulWidget类是这样的 class YourVideoListScreen extends StatefulWidget { final List<Map<String, dynamic>> data; final String unitid; final String unitno; // 其他需要的参数... const YourVideoListScreen({Key? key, required this.data, required this.unitid, required this.unitno}) : super(key: key); @override State<YourVideoListScreen> createState() => _YourVideoListScreenState(); } class _YourVideoListScreenState extends State<YourVideoListScreen> { // 替换全局变量为每个item的状态跟踪 Map<int, bool> _isDownloadingMap = {}; Map<int, String> _downloadMessageMap = {}; // 其他变量... String baseurl = ''; // 你的base url // 假设db和networkUtils是已定义的实例 var db; var networkUtils; String url = ''; // 你的videodetail接口url @override void initState() { super.initState(); // 初始化每个item的下载状态 for(int i = 0; i < widget.data.length; i++){ _isDownloadingMap[i] = false; _downloadMessageMap[i] = ''; } } Widget checkid(int index){ if(widget.data[index]["unit_id"].toString() == widget.unitid){ return articles(index); } else { return SizedBox.shrink(); } } Container articles(int index){ return Container( child: Center( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[ GestureDetector( child: Card( child: Padding( padding: const EdgeInsets.only(top: 20.0, bottom: 20, left: 13.0, right: 22.0), child: Row( children: <Widget>[ Column( children: <Widget>[ Text(widget.data[index]["video_size"]+" MB"), IconButton( icon: _isDownloadingMap[index] == true ? CircularProgressIndicator(size: 20,) : Icon(Icons.file_download), onPressed: _isDownloadingMap[index] == true ? null : () async { bool exist = await db.checkdataid(widget.data[index]["data_video_id"]); print(exist); if(exist == false){ VideoDetails videoDetail = VideoDetails(data_id: widget.data[index]["data_video_id"]); await networkUtils.videodetail(url, body:videoDetail.toMap()); } String videopath = await db.getvideopath(); String videourl = baseurl + videopath; print(videourl); setState(() { _isDownloadingMap[index] = true; _downloadMessageMap[index] = 'Starting download...'; }); var dir = await getApplicationDocumentsDirectory(); Dio dio = Dio(); try { await dio.download( videourl, '${dir.path}/video_${index}.ehb', onReceiveProgress: (actualbytes , totalbytes){ if(totalbytes != -1){ var percentage = (actualbytes/totalbytes * 100).floor(); setState(() { _downloadMessageMap[index] = 'Downloading ${percentage} %'; }); } } ); setState(() { _downloadMessageMap[index] = 'Download completed!'; _isDownloadingMap[index] = false; }); } catch (e) { setState(() { _downloadMessageMap[index] = 'Download failed'; _isDownloadingMap[index] = false; }); print('Download error: $e'); } }, ), Text( _downloadMessageMap[index] ?? '', style: TextStyle(fontSize: 12), ) ], ), Container( width: 300, child: Text(widget.data[index]["topic_name"], style: TextStyle(fontSize: 15), textAlign: TextAlign.start,), ), ], ), ), ), onTap: () {}, ) ], ), ), ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.indigo[700], body: ListView( children: <Widget>[ Column( children: <Widget>[ Padding( padding: const EdgeInsets.fromLTRB(0, 10, 110, 0), child: Container( padding: EdgeInsets.fromLTRB(0, 30, 200, 0), child: IconButton( icon: Icon(Icons.arrow_back), color: Colors.black, onPressed: (){ Navigator.pop(context); }, ), ), ), SizedBox(height: 10,), Text(" ", style: TextStyle(color: Colors.white, fontSize: 32, fontWeight: FontWeight.bold), ), Text("Unit " + widget.unitno, style: TextStyle(color: Colors.white, fontSize: 32, fontWeight: FontWeight.bold), ), SizedBox(height: 10,), Text('', style: TextStyle(color: Colors.white, fontSize: 17), ), ], ), SizedBox(height: 40,), Container( decoration: BoxDecoration( color: Colors.white70, borderRadius: BorderRadius.only(topLeft: Radius.circular(75.0),), ), child: Padding( padding: EdgeInsets.fromLTRB(0, 100, 0, 70), child: ListView.builder( physics: ScrollPhysics(), shrinkWrap: true, itemCount: widget.data.length, itemBuilder: (BuildContext context,int index){ return Container( child: checkid(index), ); }), ), ) ], ), ); } } // 假设VideoDetails类是这样的 class VideoDetails { String data_id; VideoDetails({required this.data_id}); Map<String, dynamic> toMap() { return {'data_id': data_id}; } }
关键修改点总结
- 用
Map<int, bool>和Map<int, String>代替全局变量,实现每个item独立的状态管理 - 修复
checkid函数的返回值,避免ListView抛出null异常 - 下载时给文件名添加index,防止多个下载文件互相覆盖
- 下载中禁用按钮并显示加载图标,优化用户体验
- 添加下载失败的错误处理,更新对应item的状态
内容的提问来源于stack exchange,提问作者Mrunal Joshi
相关产品推荐
相关产品推荐

