dio与Flutter File Picker:文件选择与上传

dio与Flutter File Picker:文件选择与上传

【免费下载链接】dio 【免费下载链接】dio 项目地址: https://gitcode.com/gh_mirrors/dio/dio

在移动应用开发中,文件选择与上传是常见的功能需求。无论是头像上传、文档提交还是媒体分享,都需要可靠的文件处理方案。本文将介绍如何结合dio(强大的HTTP客户端)和Flutter File Picker(文件选择工具)实现高效的文件选择与上传功能,解决开发者在实际项目中遇到的痛点问题。

技术准备与环境配置

核心依赖库

实现文件选择与上传功能需要以下两个核心库:

  1. dio:一个强大的Dart HTTP客户端,支持拦截器、FormData、请求取消、超时等高级功能。在项目中负责处理HTTP请求,特别是文件上传部分。
  2. Flutter File Picker:一个Flutter插件,用于从设备中选择文件,支持多种文件类型和平台。

项目配置

pubspec.yaml文件中添加依赖:

dependencies:
  dio: ^5.4.0
  file_picker: ^5.5.0

然后运行flutter pub get安装依赖。

平台特定配置

Android配置

android/app/src/main/AndroidManifest.xml中添加文件读取权限:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
iOS配置

ios/Runner/Info.plist中添加文件访问权限描述:

<key>NSPhotoLibraryUsageDescription</key>
<string>需要访问相册以选择文件</string>
<key>NSDocumentDirectoryUsageDescription</key>
<string>需要访问文档目录以选择文件</string>

文件选择:使用Flutter File Picker

基本文件选择

Flutter File Picker提供了简单易用的API来选择文件。以下是一个基本的文件选择示例:

import 'package:file_picker/file_picker.dart';

Future<FilePickerResult?> pickFile() async {
  // 选择单个文件
  FilePickerResult? result = await FilePicker.platform.pickFiles(
    type: FileType.any, // 允许选择任何类型的文件
    allowMultiple: false, // 不允许选择多个文件
  );

  if (result != null) {
    // 获取选中的文件信息
    PlatformFile file = result.files.first;
    print('选择的文件名称: ${file.name}');
    print('文件大小: ${file.size} bytes');
    print('文件路径: ${file.path}');
    return result;
  } else {
    // 用户取消了选择
    print('用户取消了文件选择');
    return null;
  }
}

高级文件选择选项

Flutter File Picker还支持更多高级选项,如筛选文件类型、限制文件大小等:

Future<FilePickerResult?> pickImageFile() async {
  FilePickerResult? result = await FilePicker.platform.pickFiles(
    type: FileType.image, // 只允许选择图片文件
    allowedExtensions: ['jpg', 'jpeg', 'png'], // 允许的文件扩展名
    allowCompression: true, // 允许压缩图片
    withData: false, // 不直接加载文件数据,节省内存
    withReadStream: true, // 获取文件读取流
  );
  return result;
}

文件上传:使用dio

了解dio的FormData

dio使用FormData类来构建multipart/form-data请求,这是文件上传的标准方式。FormData类位于dio/lib/src/form_data.dart文件中,它提供了多种构造方式来创建表单数据。

单文件上传

结合Flutter File Picker和dio实现单文件上传:

import 'package:dio/dio.dart';
import 'package:file_picker/file_picker.dart';

Future<void> uploadFile(FilePickerResult? result) async {
  if (result == null) return;

  PlatformFile file = result.files.first;
  
  // 创建dio实例
  Dio dio = Dio();
  
  // 构建FormData
  FormData formData = FormData.fromMap({
    'file': await MultipartFile.fromFile(
      file.path!, // 文件路径
      filename: file.name, // 文件名
      contentType: MediaType('image', 'jpeg'), // 文件类型
    ),
    'description': '这是一张图片', // 其他表单字段
  });
  
  try {
    // 发送POST请求上传文件
    Response response = await dio.post(
      'https://api.example.com/upload',
      data: formData,
      options: Options(
        headers: {
          'Authorization': 'Bearer your_token_here', // 添加认证头
        },
      ),
      onSendProgress: (int sent, int total) {
        // 上传进度回调
        double progress = sent / total;
        print('上传进度: ${(progress * 100).toStringAsFixed(0)}%');
      },
    );
    
    print('文件上传成功: ${response.data}');
  } catch (e) {
    print('文件上传失败: $e');
  }
}

多文件上传

dio同样支持多文件上传,只需在FormData中添加多个文件字段即可:

Future<void> uploadMultipleFiles(List<PlatformFile> files) async {
  Dio dio = Dio();
  
  FormData formData = FormData();
  
  // 添加多个文件
  for (int i = 0; i < files.length; i++) {
    formData.files.add(MapEntry(
      'files[$i]', // 字段名,后端可以通过files数组获取
      await MultipartFile.fromFile(
        files[i].path!,
        filename: files[i].name,
      ),
    ));
  }
  
  // 添加其他表单字段
  formData.fields.add(const MapEntry('user_id', '123456'));
  
  try {
    Response response = await dio.post(
      'https://api.example.com/upload/multiple',
      data: formData,
      onSendProgress: (int sent, int total) {
        print('总上传进度: ${(sent / total * 100).toStringAsFixed(0)}%');
      },
    );
    
    print('多文件上传成功: ${response.data}');
  } catch (e) {
    print('多文件上传失败: $e');
  }
}

完整示例:文件选择与上传综合应用

文件选择与上传流程

下面是一个完整的文件选择与上传流程示例,包括错误处理和用户反馈:

import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:file_picker/file_picker.dart';

class FileUploader extends StatefulWidget {
  @override
  _FileUploaderState createState() => _FileUploaderState();
}

class _FileUploaderState extends State<FileUploader> {
  FilePickerResult? _selectedFile;
  double _uploadProgress = 0.0;
  String _statusMessage = '请选择文件';

  Future<void> _pickAndUploadFile() async {
    setState(() {
      _statusMessage = '正在选择文件...';
    });

    // 选择文件
    FilePickerResult? result = await FilePicker.platform.pickFiles(
      type: FileType.image,
      allowMultiple: false,
    );

    if (result == null) {
      setState(() {
        _statusMessage = '文件选择已取消';
      });
      return;
    }

    setState(() {
      _selectedFile = result;
      _statusMessage = '正在上传文件: ${result.files.first.name}';
      _uploadProgress = 0.0;
    });

    // 上传文件
    await _uploadSelectedFile(result);
  }

  Future<void> _uploadSelectedFile(FilePickerResult result) async {
    PlatformFile file = result.files.first;
    Dio dio = Dio();

    try {
      FormData formData = FormData.fromMap({
        'file': await MultipartFile.fromFile(
          file.path!,
          filename: file.name,
        ),
      });

      Response response = await dio.post(
        'https://api.example.com/upload',
        data: formData,
        onSendProgress: (int sent, int total) {
          setState(() {
            _uploadProgress = sent / total;
          });
        },
      );

      setState(() {
        _statusMessage = '文件上传成功! 服务器响应: ${response.statusCode}';
      });
    } catch (e) {
      setState(() {
        _statusMessage = '文件上传失败: $e';
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ElevatedButton(
          onPressed: _pickAndUploadFile,
          child: const Text('选择并上传文件'),
        ),
        const SizedBox(height: 20),
        Text(_statusMessage),
        const SizedBox(height: 10),
        LinearProgressIndicator(
          value: _uploadProgress,
          minHeight: 8,
        ),
        if (_selectedFile != null)
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              children: [
                Text('选中的文件: ${_selectedFile!.files.first.name}'),
                Text('文件大小: ${_selectedFile!.files.first.size} bytes'),
              ],
            ),
          ),
      ],
    );
  }
}

示例代码解析

上述示例实现了一个完整的文件选择与上传功能,主要包含以下几个部分:

  1. UI组件:包含一个按钮用于触发文件选择,一个文本组件显示状态信息,一个进度条显示上传进度。
  2. 文件选择:调用FilePicker.platform.pickFiles方法选择图片文件。
  3. 文件上传:使用dio的FormDataMultipartFile构建上传请求,并通过onSendProgress回调获取上传进度。
  4. 状态管理:使用setState更新UI状态,包括上传进度、状态消息和选中的文件信息。

高级功能与最佳实践

大文件分片上传

对于大文件,建议使用分片上传的方式,将文件分成多个小块依次上传,以提高上传成功率和断点续传能力。以下是一个简单的分片上传实现:

Future<void> uploadLargeFile(PlatformFile file, {int chunkSize = 2 * 1024 * 1024}) async {
  Dio dio = Dio();
  int fileSize = file.size;
  int chunks = (fileSize / chunkSize).ceil();
  
  for (int i = 0; i < chunks; i++) {
    int start = i * chunkSize;
    int end = (i + 1) * chunkSize;
    if (end > fileSize) end = fileSize;
    
    // 读取文件分片
    File fileData = File(file.path!);
    Uint8List chunkData = await fileData.readAsBytes().then((bytes) => bytes.sublist(start, end));
    
    // 创建分片上传的FormData
    FormData formData = FormData.fromMap({
      'file_id': 'unique_file_id', // 唯一文件ID,用于后端合并分片
      'chunk_index': i, // 当前分片索引
      'total_chunks': chunks, // 总分片数
      'chunk_data': MultipartFile.fromBytes(
        chunkData,
        filename: 'chunk_$i',
      ),
    });
    
    // 上传分片
    await dio.post(
      'https://api.example.com/upload_chunk',
      data: formData,
    );
    
    // 更新整体进度
    setState(() {
      _uploadProgress = (i + 1) / chunks;
    });
  }
  
  // 所有分片上传完成后,通知后端合并文件
  await dio.post('https://api.example.com/merge_chunks', data: {
    'file_id': 'unique_file_id',
    'total_chunks': chunks,
    'filename': file.name,
  });
}

错误处理与重试机制

在实际应用中,网络请求可能会失败,因此需要添加错误处理和重试机制:

Future<Response> uploadWithRetry({
  required FormData data,
  required String url,
  int maxRetries = 3,
}) async {
  Dio dio = Dio();
  int retries = 0;
  
  while (retries < maxRetries) {
    try {
      Response response = await dio.post(
        url,
        data: data,
        onSendProgress: (sent, total) {
          // 处理上传进度
        },
      );
      return response;
    } catch (e) {
      retries++;
      if (retries >= maxRetries) {
        throw Exception('上传失败,已重试$maxRetries次: $e');
      }
      // 指数退避重试
      int delay = (retries * retries) * 1000; // 1s, 4s, 9s...
      print('上传失败,$delay毫秒后重试($retries/$maxRetries)...');
      await Future.delayed(Duration(milliseconds: delay));
    }
  }
  
  throw Exception('上传失败,达到最大重试次数');
}

取消上传请求

dio支持取消正在进行的请求,可以用于实现取消上传功能:

// 创建取消令牌
CancelToken cancelToken = CancelToken();

// 发起上传请求,传入取消令牌
try {
  Response response = await dio.post(
    'https://api.example.com/upload',
    data: formData,
    cancelToken: cancelToken,
  );
} on DioException catch (e) {
  if (CancelToken.isCancel(e)) {
    print('上传已取消');
  } else {
    print('上传失败: $e');
  }
}

// 取消上传
void cancelUpload() {
  if (!cancelToken.isCancelled) {
    cancelToken.cancel('用户取消上传');
  }
}

总结与展望

本文详细介绍了如何结合dio和Flutter File Picker实现文件选择与上传功能,从基本用法到高级特性,涵盖了实际项目开发中可能遇到的各种场景。通过本文的学习,开发者可以掌握:

  1. 使用Flutter File Picker选择不同类型的文件
  2. 使用dio的FormData构建文件上传请求
  3. 实现单文件和多文件上传
  4. 显示上传进度和处理上传状态
  5. 大文件分片上传、错误重试和请求取消等高级功能

文件上传是移动应用开发中的常见需求,掌握这些技能可以帮助开发者构建更健壮、更用户友好的应用。未来,随着Web技术的发展,我们可以期待更多新的特性和优化,如更高效的文件压缩算法、更智能的分片策略等,进一步提升文件上传体验。

希望本文对你的项目开发有所帮助,如果你有任何问题或建议,欢迎在评论区留言讨论!

【免费下载链接】dio 【免费下载链接】dio 项目地址: https://gitcode.com/gh_mirrors/dio/dio

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值