MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Flutter dio插件的文件上传与下载:实现复杂网络操作

2024-07-291.6k 阅读

Flutter dio插件的文件上传与下载:实现复杂网络操作

一、Flutter dio插件概述

在Flutter开发中,网络请求是非常常见的需求。Dio是一个强大的HTTP请求库,它在Flutter生态系统中被广泛使用。Dio具有以下特点:

  1. 支持HTTP/HTTPS:能够处理标准的HTTP和加密的HTTPS请求,满足各种网络场景。
  2. 拦截器:通过拦截器可以方便地对请求和响应进行统一处理,例如添加通用的请求头、处理响应数据的格式等。
  3. 请求取消:可以在需要的时候取消正在进行的请求,避免资源浪费和潜在的内存泄漏。
  4. 文件上传与下载:这正是本文重点关注的功能,Dio提供了简洁而高效的API来实现文件的上传和下载操作。

要使用Dio插件,首先需要在pubspec.yaml文件中添加依赖:

dependencies:
  dio: ^4.0.0

然后运行flutter pub get命令来获取依赖。

二、文件下载

  1. 简单文件下载 以下是使用Dio进行简单文件下载的代码示例:
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'dart:io';

class FileDownloadPage extends StatefulWidget {
  @override
  _FileDownloadPageState createState() => _FileDownloadPageState();
}

class _FileDownloadPageState extends State<FileDownloadPage> {
  double _progress = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('文件下载'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text('下载进度: ${(_progress * 100).toStringAsFixed(2)}%'),
          ElevatedButton(
            onPressed: () async {
              final dio = Dio();
              final url = 'http://example.com/file.zip';
              final savePath = '${(await getApplicationDocumentsDirectory()).path}/file.zip';
              try {
                await dio.download(
                  url,
                  savePath,
                  onReceiveProgress: (received, total) {
                    if (total != -1) {
                      setState(() {
                        _progress = received / total;
                      });
                    }
                  },
                );
                print('文件下载完成');
              } catch (e) {
                print('文件下载失败: $e');
              }
            },
            child: Text('开始下载'),
          ),
        ],
      ),
    );
  }
}

在上述代码中:

  • 首先创建了一个Dio实例。
  • 定义了要下载的文件的URL和保存路径。这里使用getApplicationDocumentsDirectory获取应用的文档目录来保存文件。
  • dio.download方法执行下载操作,onReceiveProgress回调用于更新下载进度,received表示已接收的字节数,total表示文件总字节数。如果total为 -1,说明无法获取文件总大小。
  1. 断点续传 断点续传在网络不稳定或文件较大的情况下非常有用。Dio支持通过设置Range请求头来实现断点续传。以下是实现断点续传的代码示例:
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'dart:io';

class ResumeDownloadPage extends StatefulWidget {
  @override
  _ResumeDownloadPageState createState() => _ResumeDownloadPageState();
}

class _ResumeDownloadPageState extends State<ResumeDownloadPage> {
  double _progress = 0;
  bool _isDownloading = false;
  File? _downloadedFile;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('断点续传'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          if (_downloadedFile != null)
            Text('文件已下载: ${_downloadedFile!.path}'),
          Text('下载进度: ${(_progress * 100).toStringAsFixed(2)}%'),
          if (_isDownloading)
            CircularProgressIndicator(),
          ElevatedButton(
            onPressed: () async {
              if (_isDownloading) {
                return;
              }
              setState(() {
                _isDownloading = true;
              });
              final dio = Dio();
              final url = 'http://example.com/large_file.zip';
              final savePath = '${(await getApplicationDocumentsDirectory()).path}/large_file.zip';
              File file = File(savePath);
              int downloadedLength = 0;
              if (file.existsSync()) {
                downloadedLength = file.lengthSync();
              }
              try {
                await dio.download(
                  url,
                  savePath,
                  onReceiveProgress: (received, total) {
                    if (total != -1) {
                      setState(() {
                        _progress = (downloadedLength + received) / total;
                      });
                    }
                  },
                  options: Options(
                    headers: {
                      'Range': 'bytes=$downloadedLength-'
                    }
                  )
                );
                setState(() {
                  _downloadedFile = file;
                  _isDownloading = false;
                });
                print('文件下载完成');
              } catch (e) {
                setState(() {
                  _isDownloading = false;
                });
                print('文件下载失败: $e');
              }
            },
            child: Text(_isDownloading? '暂停下载' : '开始下载'),
          ),
        ],
      ),
    );
  }
}

在这段代码中:

  • 首先检查本地是否已经存在部分下载的文件,如果存在,则获取已下载的长度downloadedLength
  • dio.downloadoptions中设置Range请求头,格式为bytes=已下载长度-,表示从已下载的位置继续下载。
  • 下载过程中,根据已下载长度和新接收的长度来更新进度。

三、文件上传

  1. 单文件上传 以下是使用Dio进行单文件上传的代码示例:
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'dart:io';
import 'package:path_provider/path_provider.dart';

class SingleFileUploadPage extends StatefulWidget {
  @override
  _SingleFileUploadPageState createState() => _SingleFileUploadPageState();
}

class _SingleFileUploadPageState extends State<SingleFileUploadPage> {
  String _uploadStatus = '等待上传';
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('单文件上传'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(_uploadStatus),
          ElevatedButton(
            onPressed: () async {
              setState(() {
                _uploadStatus = '正在上传';
              });
              final dio = Dio();
              final url = 'http://example.com/upload';
              final file = File('${(await getTemporaryDirectory()).path}/test.txt');
              await file.writeAsString('这是一个测试文件');
              FormData formData = FormData.fromMap({
                'file': await MultipartFile.fromFile(file.path, filename: 'test.txt')
              });
              try {
                Response response = await dio.post(url, data: formData);
                setState(() {
                  _uploadStatus = '上传成功: ${response.data}';
                });
                print('文件上传成功: ${response.data}');
              } catch (e) {
                setState(() {
                  _uploadStatus = '上传失败: $e';
                });
                print('文件上传失败: $e');
              }
            },
            child: Text('开始上传'),
          ),
        ],
      ),
    );
  }
}

在上述代码中:

  • 创建了一个临时文件test.txt并写入一些测试数据。
  • 使用FormData来包装要上传的文件,MultipartFile.fromFile方法用于将本地文件转换为MultipartFile,并可以指定文件名。
  • 通过dio.post方法将FormData发送到服务器。
  1. 多文件上传 多文件上传与单文件上传类似,只是FormData中包含多个MultipartFile。以下是多文件上传的代码示例:
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'dart:io';
import 'package:path_provider/path_provider.dart';

class MultipleFileUploadPage extends StatefulWidget {
  @override
  _MultipleFileUploadPageState createState() => _MultipleFileUploadPageState();
}

class _MultipleFileUploadPageState extends State<MultipleFileUploadPage> {
  String _uploadStatus = '等待上传';
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('多文件上传'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(_uploadStatus),
          ElevatedButton(
            onPressed: () async {
              setState(() {
                _uploadStatus = '正在上传';
              });
              final dio = Dio();
              final url = 'http://example.com/upload_multiple';
              List<MultipartFile> files = [];
              for (int i = 0; i < 3; i++) {
                final file = File('${(await getTemporaryDirectory()).path}/test$i.txt');
                await file.writeAsString('这是测试文件 $i');
                files.add(await MultipartFile.fromFile(file.path, filename: 'test$i.txt'));
              }
              FormData formData = FormData.fromMap({
                'files': files
              });
              try {
                Response response = await dio.post(url, data: formData);
                setState(() {
                  _uploadStatus = '上传成功: ${response.data}';
                });
                print('文件上传成功: ${response.data}');
              } catch (e) {
                setState(() {
                  _uploadStatus = '上传失败: $e';
                });
                print('文件上传失败: $e');
              }
            },
            child: Text('开始上传'),
          ),
        ],
      ),
    );
  }
}

在这段代码中:

  • 循环创建了三个临时文件,并将它们转换为MultipartFile添加到List中。
  • FormDatafiles字段包含了多个MultipartFile,通过dio.post发送到服务器进行多文件上传。

四、处理复杂网络操作

  1. 并发请求 在实际应用中,可能需要同时发起多个网络请求。Dio可以很方便地实现并发请求。以下是一个并发下载多个文件的示例:
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'dart:io';
import 'package:path_provider/path_provider.dart';

class ConcurrentDownloadPage extends StatefulWidget {
  @override
  _ConcurrentDownloadPageState createState() => _ConcurrentDownloadPageState();
}

class _ConcurrentDownloadPageState extends State<ConcurrentDownloadPage> {
  List<double> _progresses = [];
  @override
  void initState() {
    super.initState();
    _progresses = List.filled(3, 0);
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('并发下载'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          for (int i = 0; i < _progresses.length; i++)
            Text('文件 $i 下载进度: ${(_progresses[i] * 100).toStringAsFixed(2)}%'),
          ElevatedButton(
            onPressed: () async {
              final dio = Dio();
              List<Future> downloadFutures = [];
              List<String> urls = [
                'http://example.com/file1.zip',
                'http://example.com/file2.zip',
                'http://example.com/file3.zip'
              ];
              for (int i = 0; i < urls.length; i++) {
                final savePath = '${(await getApplicationDocumentsDirectory()).path}/file$i.zip';
                downloadFutures.add(dio.download(
                  urls[i],
                  savePath,
                  onReceiveProgress: (received, total) {
                    if (total != -1) {
                      setState(() {
                        _progresses[i] = received / total;
                      });
                    }
                  },
                ));
              }
              await Future.wait(downloadFutures);
              print('所有文件下载完成');
            },
            child: Text('开始并发下载'),
          ),
        ],
      ),
    );
  }
}

在上述代码中:

  • 创建了一个List来存储每个文件的下载进度。
  • 遍历URL列表,为每个文件的下载创建一个Future并添加到downloadFutures列表中。
  • 使用Future.wait等待所有下载任务完成。
  1. 请求重试机制 网络请求可能会因为各种原因失败,例如网络波动、服务器繁忙等。添加请求重试机制可以提高应用的稳定性。以下是一个带有重试机制的文件下载示例:
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'dart:io';

class RetryDownloadPage extends StatefulWidget {
  @override
  _RetryDownloadPageState createState() => _RetryDownloadPageState();
}

class _RetryDownloadPageState extends State<RetryDownloadPage> {
  double _progress = 0;
  int _retryCount = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('重试下载'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text('下载进度: ${(_progress * 100).toStringAsFixed(2)}%'),
          Text('重试次数: $_retryCount'),
          ElevatedButton(
            onPressed: () async {
              final dio = Dio();
              final url = 'http://example.com/file.zip';
              final savePath = '${(await getApplicationDocumentsDirectory()).path}/file.zip';
              int maxRetries = 3;
              while (_retryCount < maxRetries) {
                try {
                  await dio.download(
                    url,
                    savePath,
                    onReceiveProgress: (received, total) {
                      if (total != -1) {
                        setState(() {
                          _progress = received / total;
                        });
                      }
                    },
                  );
                  print('文件下载完成');
                  break;
                } catch (e) {
                  setState(() {
                    _retryCount++;
                  });
                  print('下载失败,重试 ($_retryCount/$maxRetries): $e');
                  await Future.delayed(Duration(seconds: 2));
                }
              }
              if (_retryCount >= maxRetries) {
                print('达到最大重试次数,下载失败');
              }
            },
            child: Text('开始下载'),
          ),
        ],
      ),
    );
  }
}

在这段代码中:

  • 设置了最大重试次数maxRetries为3。
  • 使用while循环进行重试,每次下载失败时,增加重试次数并等待2秒后再次尝试,直到下载成功或达到最大重试次数。

五、错误处理与优化

  1. 错误处理 在文件上传和下载过程中,可能会遇到各种错误。常见的错误包括网络连接失败、服务器响应错误、文件读写错误等。
    • 网络连接失败:这通常是由于网络不可用或服务器无法访问导致的。在Dio中,这类错误通常会抛出DioError,并且errorTypeDioErrorType.connectTimeoutDioErrorType.receiveTimeout等。可以通过捕获DioError并根据errorType进行相应处理。
    • 服务器响应错误:例如服务器返回404(未找到)、500(内部服务器错误)等状态码。可以在dio请求的options中设置validateStatus回调来自定义对服务器响应状态码的处理。如果状态码不符合预期,可以抛出异常或进行其他处理。
    • 文件读写错误:在文件下载保存或上传读取文件时可能发生。例如目标目录不存在、文件权限不足等。在进行文件操作前,应该先检查目录是否存在,并且确保应用具有相应的文件读写权限。
  2. 优化措施
    • 缓存:对于频繁下载的文件,可以考虑使用缓存机制。在下载文件前,先检查本地缓存中是否存在该文件,如果存在且未过期,则直接使用缓存文件,避免重复下载。
    • 优化网络请求:尽量合并多个小的网络请求为一个大请求,减少网络开销。同时,合理设置请求超时时间,避免长时间等待无响应的请求。
    • 资源管理:在文件上传和下载完成后,及时释放相关资源,例如关闭文件流、取消未完成的请求等,防止内存泄漏。

通过合理处理错误和采取优化措施,可以提高基于Dio的文件上传与下载功能的稳定性和性能,为用户提供更好的体验。