Flutter多文件下载时LinearProgressIndicator状态管理失效问题
问题核心原因
- 下载进度更新时仅修改了
progress集合的内存值,未调用setState()触发框架重绘,UI无法感知进度变化,进度条自然不会更新。 FutureBuilder的future参数直接传入getUrls(),每次页面build都会重新发起网络请求拉取json配置,列表会反复重建,多个下载任务同时执行时会被重建逻辑打断。- 进度计算未做边界判断:下载刚启动时响应总大小
total可能为0,直接计算received/total会触发除零异常,导致下载中断。 - 初始状态下
progress[index]为null,LinearProgressIndicator接收到null值会进入不确定滚动模式,不会按实际进度渲染。 - 每次点击下载按钮都新建Dio实例,没有做重复点击拦截,快速重复点击会重复发起下载请求。
- ListView直接放在Column中未做自适应处理,会触发布局溢出报错。
修复步骤
- 把文件列表数据缓存到组件状态中,不要在FutureBuilder中直接调用请求方法,避免build时重复触发网络请求
- 进度更新时判断total有效值,更新值后调用
setState()刷新UI - 拿到文件列表后给每个下载项初始化0进度,避免进度条初始值为null
- 增加下载中状态拦截,防止重复点击发起重复请求
- 用Expanded包裹ListView,解决Column内列表布局溢出问题
修复后可直接运行的代码
import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import '../services/services.dart'; class ReceiveScreen extends StatefulWidget { const ReceiveScreen({Key? key}) : super(key: key); @override State<ReceiveScreen> createState() => _ReceiveScreenState(); } class _ReceiveScreenState extends State<ReceiveScreen> { // 存储每个index对应的下载进度 final Map<int, double> progress = {}; // 存储每个index对应的下载状态,防止重复点击 final Map<int, bool> downloadingMap = {}; final String _baseUrl = "http://192.168.1.100:50500"; // 缓存拉取到的文件列表数据 Files? filesData; // 统一复用Dio实例,不要每次新建 final Dio _dio = Dio(); @override void initState() { super.initState(); // 初始化时拉取一次文件列表 _loadFileList(); } @override void dispose() { _dio.close(); super.dispose(); } // 拉取文件列表的统一方法 Future<void> _loadFileList() async { try { final response = await http.get(Uri.parse("$_baseUrl/dsharing.json")); if (response.statusCode == 200) { final data = jsonDecode(response.body); final files = Files.fromJson(data, _baseUrl); setState(() { filesData = files; // 给每个文件项初始化0进度、未下载状态 for (int i = 0; i < files.names!.length; i++) { progress[i] = 0; downloadingMap[i] = false; } }); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("拉取文件列表失败: $e")) ); } } } // 单独的下载方法 Future<void> _startDownload(int index) async { // 已经在下载中就直接返回 if (downloadingMap[index] == true) return; final fileName = filesData!.names![index]; final fileUrl = filesData!.urls![index]; // 安卓公共下载目录路径,注意安卓13+需要申请对应的存储权限,否则会写入失败 final savePath = "/storage/emulated/0/Download/$fileName"; setState(() { downloadingMap[index] = true; progress[index] = 0; }); try { await _dio.download( fileUrl, savePath, onReceiveProgress: (receivedBytes, totalBytes) { // 总大小为0的时候不计算进度,避免除零错误 if (totalBytes <= 0) return; setState(() { progress[index] = receivedBytes / totalBytes; }); } ); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("$fileName 下载完成")) ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("$fileName 下载失败: $e")) ); } } finally { if (mounted) { setState(() { downloadingMap[index] = false; }); } } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Receive Files"), ), body: Column( children: [ ElevatedButton( onPressed: _loadFileList, child: const Text("刷新文件列表") ), Expanded( child: filesData == null ? const Center(child: CircularProgressIndicator()) : ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), itemCount: filesData!.names!.length, itemBuilder: (context, index) { final isDownloading = downloadingMap[index] ?? false; final currentProgress = progress[index] ?? 0; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ElevatedButton( onPressed: isDownloading ? null : () => _startDownload(index), child: Text(isDownloading ? "下载中..." : "${filesData!.names![index]} 下载") ), const SizedBox(height: 8), LinearProgressIndicator( value: currentProgress, minHeight: 20, ), const SizedBox(height: 16), ], ); } ), ) ], ), ); } } class Files { final List? names; final List? urls; Files({this.names, this.urls}); factory Files.fromJson(Map map, String url) { var fileNames = []; String name = map['name']; if (name.contains(":")) { int index = name.indexOf(":") + 1; name = name.substring(index + 1, name.length); var x = name.replaceAll(" ", '^*&'); fileNames = x.split("^*&").toList(); } else { fileNames.add(name); } var x = map['data'].toString().split("|dsharing|").toList(); var fileUrls = x.map((i) => "$url?/q=$i").toList(); return Files( names: fileNames, urls: fileUrls ); } }
额外注意
- 安卓平台运行需要在AndroidManifest.xml中配置存储权限,安卓13及以上版本需要申请
READ_MEDIA_IMAGES、READ_MEDIA_VIDEO、READ_MEDIA_AUDIO对应权限,安卓12及以下需要申请WRITE_EXTERNAL_STORAGE权限,否则文件无法写入公共下载目录。 - 当前代码已支持多文件同时下载,每个下载项的进度按index独立存储,不会互相覆盖。
- 原代码中未使用的
received、total全局变量、rsponse变量、空的receivers列表都已清理,避免冗余代码导致的异常。
内容的提问来源于stack exchange,提问作者ABDUL REHMAN
相关产品推荐
相关产品推荐

