You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter多文件下载时LinearProgressIndicator状态管理失效问题

问题核心原因
  • 下载进度更新时仅修改了progress集合的内存值,未调用setState()触发框架重绘,UI无法感知进度变化,进度条自然不会更新。
  • FutureBuilder的future参数直接传入getUrls(),每次页面build都会重新发起网络请求拉取json配置,列表会反复重建,多个下载任务同时执行时会被重建逻辑打断。
  • 进度计算未做边界判断:下载刚启动时响应总大小total可能为0,直接计算received/total会触发除零异常,导致下载中断。
  • 初始状态下progress[index]为null,LinearProgressIndicator接收到null值会进入不确定滚动模式,不会按实际进度渲染。
  • 每次点击下载按钮都新建Dio实例,没有做重复点击拦截,快速重复点击会重复发起下载请求。
  • ListView直接放在Column中未做自适应处理,会触发布局溢出报错。
修复步骤
  1. 把文件列表数据缓存到组件状态中,不要在FutureBuilder中直接调用请求方法,避免build时重复触发网络请求
  2. 进度更新时判断total有效值,更新值后调用setState()刷新UI
  3. 拿到文件列表后给每个下载项初始化0进度,避免进度条初始值为null
  4. 增加下载中状态拦截,防止重复点击发起重复请求
  5. 用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 19:24:25