Flutter 跨平台文件上传 - GetX + ImagePicker + Dio 实现
Flutter 跨平台文件上传 - GetX + ImagePicker + Dio 实现
以下是使用 GetX、ImagePicker 和 Dio 实现的跨平台文件上传方案,支持 Android 和 Web:
1. 依赖配置 (pubspec.yaml)
dependencies:flutter:sdk: flutterget: ^4.6.6dio: ^5.0.0image_picker: ^1.0.4universal_html: ^2.2.0path_provider: ^2.1.1path: ^1.8.3mime: ^1.0.4file_picker: ^5.6.1dev_dependencies:flutter_test:sdk: flutterflutter_lints: ^2.0.0
2. 跨平台上传控制器 (CrossPlatformUploadController)
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:dio/dio.dart' as dio;
import 'package:path/path.dart' as path;
import 'package:mime/mime.dart';
import 'package:universal_html/html.dart' as html;
import 'package:flutter/foundation.dart';enum UploadStatus { pending, uploading, completed, failed }
enum PlatformType { web, android, ios }class UploadItem {final String fileName;final int fileSize;final PlatformType platform;UploadStatus status;double progress;String? error;String? filePath;Uint8List? fileBytes;html.File? webFile;UploadItem({required this.fileName,required this.fileSize,required this.platform,required this.status,required this.progress,this.error,this.filePath,this.fileBytes,this.webFile,});UploadItem copyWith({String? fileName,int? fileSize,PlatformType? platform,UploadStatus? status,double? progress,String? error,String? filePath,Uint8List? fileBytes,html.File? webFile,}) {return UploadItem(fileName: fileName ?? this.fileName,fileSize: fileSize ?? this.fileSize,platform: platform ?? this.platform,status: status ?? this.status,progress: progress ?? this.progress,error: error ?? this.error,filePath: filePath ?? this.filePath,fileBytes: fileBytes ?? this.fileBytes,webFile: webFile ?? this.webFile,);}
}class CrossPlatformUploadController extends GetxController {static CrossPlatformUploadController get to => Get.find();// 可观察的上传列表var uploadItems = <UploadItem>[].obs;var isUploading = false.obs;var totalProgress = 0.0.obs;// 上传配置final String uploadUrl;final Map<String, String> headers;final String fileFieldName;final Map<String, String>? additionalFormData;final int maxFiles;// ImagePicker 实例final ImagePicker _imagePicker = ImagePicker();// Dio 实例final dio.Dio _dio = dio.Dio();CrossPlatformUploadController({required this.uploadUrl,required this.headers,this.fileFieldName = 'file',Map<String, String>? formData,this.maxFiles = 5,}) : additionalFormData = formData {// 配置 Dio_dio.options.headers.addAll(headers);_dio.options.connectTimeout = const Duration(seconds: 30);_dio.options.receiveTimeout = const Duration(seconds: 30);}// 获取当前平台类型PlatformType get currentPlatform {if (kIsWeb) return PlatformType.web;if (Platform.isAndroid) return PlatformType.android;if (Platform.isIOS) return PlatformType.ios;return PlatformType.android; // 默认}// 选择图片文件Future<void> pickImageFiles() async {try {if (kIsWeb) {await _pickImageFilesWeb();} else {await _pickImageFilesMobile();}} catch (e) {Get.snackbar('选择文件失败','错误: $e',snackPosition: SnackPosition.BOTTOM,backgroundColor: Colors.red,colorText: Colors.white,);}}// Web 平台选择图片文件Future<void> _pickImageFilesWeb() async {final input = html.FileUploadInputElement();input.accept = 'image/*';input.multiple = maxFiles > 1;input.onChange.listen((event) {final files = input.files;if (files != null && files.isNotEmpty) {final selectedFiles = files.take(maxFiles).toList();_addWebFilesToList(selectedFiles);}});input.click();}// 移动端选择图片文件Future<void> _pickImageFilesMobile() async {final List<XFile> selectedFiles = await _imagePicker.pickMultiImage(imageQuality: 85,maxWidth: 1920,);if (selectedFiles.isNotEmpty) {_addMobileFilesToList(selectedFiles);}}// 选择任意类型文件Future<void> pickAnyFiles() async {try {if (kIsWeb) {await _pickAnyFilesWeb();} else {await _pickAnyFilesMobile();}} catch (e) {Get.snackbar('选择文件失败','错误: $e',snackPosition: SnackPosition.BOTTOM,backgroundColor: Colors.red,colorText: Colors.white,);}}
// 移动端选择任意文件(使用 file_picker)Future<void> _pickAnyFilesMobile() async {// 注意:这里需要使用 file_picker 包来选择任意文件// 由于依赖中已经包含 file_picker,这里假设使用它// 实际使用时需要导入 file_picker 包// final FilePickerResult? result = await FilePicker.platform.pickFiles(// allowMultiple: maxFiles > 1,// type: FileType.any,// );// if (result != null && result.files.isNotEmpty) {// _addMobileFilePickerFilesToList(result.files);// }// 临时使用图片选择器作为示例await pickImageFiles();}// Web 平台选择任意文件Future<void> _pickAnyFilesWeb() async {final input = html.FileUploadInputElement();input.multiple = maxFiles > 1;input.onChange.listen((event) {final files = input.files;if (files != null && files.isNotEmpty) {final selectedFiles = files.take(maxFiles).toList();_addWebFilesToList(selectedFiles);}});input.click();}// 添加 Web 文件到列表void _addWebFilesToList(List<html.File> files) {for (final file in files) {final uploadItem = UploadItem(fileName: file.name,fileSize: file.size,platform: PlatformType.web,status: UploadStatus.pending,progress: 0.0,webFile: file,);uploadItems.add(uploadItem);}Get.snackbar('文件已添加','已添加 ${files.length} 个文件到上传队列',snackPosition: SnackPosition.BOTTOM,);}// 添加移动端文件到列表void _addMobileFilesToList(List<XFile> files) {for (final file in files) {final uploadItem = UploadItem(fileName: path.basename(file.path),fileSize: 0, // 需要异步获取platform: currentPlatform,status: UploadStatus.pending,progress: 0.0,filePath: file.path,);uploadItems.add(uploadItem);// 异步获取文件大小_getFileSize(file).then((size) {final index = uploadItems.indexOf(uploadItem);if (index != -1) {uploadItems[index] = uploadItems[index].copyWith(fileSize: size);}});}Get.snackbar('文件已添加','已添加 ${files.length} 个文件到上传队列',snackPosition: SnackPosition.BOTTOM,);}// 获取移动端文件大小Future<int> _getFileSize(XFile file) async {try {final fileData = File(file.path);final stat = await fileData.stat();return stat.size;} catch (e) {return 0;}}// 开始上传所有文件Future<void> uploadAllFiles() async {if (uploadItems.isEmpty || isUploading.value) return;isUploading.value = true;totalProgress.value = 0.0;final pendingItems = uploadItems.where((item) => item.status != UploadStatus.completed).toList();if (pendingItems.isEmpty) {isUploading.value = false;return;}int successCount = 0;int failCount = 0;for (int i = 0; i < pendingItems.length; i++) {final item = pendingItems[i];final index = uploadItems.indexOf(item);if (index != -1) {uploadItems[index] = item.copyWith(status: UploadStatus.uploading,progress: 0.0,error: null,);try {await _uploadSingleFile(uploadItems[index], index);successCount++;} catch (e) {failCount++;}// 更新总进度totalProgress.value = ((i + 1) / pendingItems.length) * 100;}}isUploading.value = false;// 显示上传结果if (failCount == 0) {Get.snackbar('上传完成','所有文件上传成功',snackPosition: SnackPosition.BOTTOM,backgroundColor: Colors.green,colorText: Colors.white,);} else {Get.snackbar('上传完成','成功: $successCount, 失败: $failCount',snackPosition: SnackPosition.BOTTOM,backgroundColor: failCount > 0 ? Colors.orange : Colors.green,colorText: Colors.white,);}}// 上传单个文件Future<void> _uploadSingleFile(UploadItem item, int index) async {try {if (kIsWeb) {await _uploadWebFile(item, index);} else {await _uploadMobileFile(item, index);}} catch (e) {uploadItems[index] = uploadItems[index].copyWith(status: UploadStatus.failed,error: '上传异常: $e',);rethrow;}}// 上传 Web 文件Future<void> _uploadWebFile(UploadItem item, int index) async {if (item.webFile == null) return;final file = item.webFile!;// 获取 MIME 类型final mimeType = lookupMimeType(file.name) ?? 'application/octet-stream';// 创建 FormDatafinal formDataObj = dio.FormData.fromMap({fileFieldName: dio.MultipartFile.fromBytes(await _readWebFileBytes(file),filename: file.name,// 使用 DioMediaType.parse 创建正确的类型contentType: dio.DioMediaType.parse(mimeType),),...?additionalFormData,});// 使用 Dio 上传await _dio.post(uploadUrl,data: formDataObj,onSendProgress: (sent, total) {final progress = (sent / total) * 100;uploadItems[index] = uploadItems[index].copyWith(progress: progress.clamp(0.0, 100.0),);},);uploadItems[index] = uploadItems[index].copyWith(status: UploadStatus.completed,progress: 100.0,);}// 读取 Web 文件字节Future<Uint8List> _readWebFileBytes(html.File file) async {final reader = html.FileReader();reader.readAsArrayBuffer(file);await reader.onLoad.first;return reader.result as Uint8List;}// 上传移动端文件Future<void> _uploadMobileFile(UploadItem item, int index) async {if (item.filePath == null) return;final file = File(item.filePath!);if (!await file.exists()) {throw Exception('文件不存在: ${item.filePath}');}// 获取 MIME 类型final mimeType = lookupMimeType(item.fileName) ?? 'application/octet-stream';// 创建 FormDatafinal formDataObj = dio.FormData.fromMap({fileFieldName: await dio.MultipartFile.fromFile(item.filePath!,filename: item.fileName,// 使用 DioMediaType.parse 创建正确的类型contentType: dio.DioMediaType.parse(mimeType),),...?additionalFormData,});// 使用 Dio 上传await _dio.post(uploadUrl,data: formDataObj,onSendProgress: (sent, total) {final progress = (sent / total) * 100;uploadItems[index] = uploadItems[index].copyWith(progress: progress.clamp(0.0, 100.0),);},);uploadItems[index] = uploadItems[index].copyWith(status: UploadStatus.completed,progress: 100.0,);}// 重试上传void retryUpload(int index) {if (index < uploadItems.length) {uploadItems[index] = uploadItems[index].copyWith(status: UploadStatus.pending,progress: 0.0,error: null,);}}// 移除单个文件void removeFile(int index) {if (index < uploadItems.length) {uploadItems.removeAt(index);}}// 清空所有文件void clearAll() {uploadItems.clear();totalProgress.value = 0.0;}// 获取上传统计Map<String, int> getUploadStats() {final total = uploadItems.length;final completed = uploadItems.where((item) => item.status == UploadStatus.completed).length;final failed = uploadItems.where((item) => item.status == UploadStatus.failed).length;final pending = uploadItems.where((item) => item.status == UploadStatus.pending).length;final uploading = uploadItems.where((item) => item.status == UploadStatus.uploading).length;return {'total': total,'completed': completed,'failed': failed,'pending': pending,'uploading': uploading,};}// 计算总进度double calculateTotalProgress() {if (uploadItems.isEmpty) return 0.0;final totalProgress = uploadItems.fold<double>(0.0, (sum, item) => sum + item.progress);return totalProgress / uploadItems.length;}// 获取平台图标IconData getPlatformIcon(PlatformType platform) {return switch (platform) {PlatformType.web => Icons.web,PlatformType.android => Icons.android,PlatformType.ios => Icons.phone_iphone,};}// 获取平台名称String getPlatformName(PlatformType platform) {return switch (platform) {PlatformType.web => 'Web',PlatformType.android => 'Android',PlatformType.ios => 'iOS',};}void onClose() {uploadItems.clear();_dio.close();super.onClose();}
}
3. 上传列表项组件 (UploadListItem)
import 'package:flutter/material.dart';
import 'package:get/get.dart';class UploadListItem extends StatelessWidget {final int index;final UploadItem item;const UploadListItem({Key? key,required this.index,required this.item,}) : super(key: key); Widget build(BuildContext context) {final controller = Get.find<CrossPlatformUploadController>();return Card(margin: EdgeInsets.symmetric(vertical: 4),child: ListTile(leading: _buildFileIcon(item),title: Text(item.fileName,overflow: TextOverflow.ellipsis,),subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start,children: [if (item.status == UploadStatus.uploading) ...[SizedBox(height: 4),LinearProgressIndicator(value: item.progress / 100,backgroundColor: Colors.grey[200],valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),),SizedBox(height: 4),],Row(children: [Icon(controller.getPlatformIcon(item.platform),size: 12,color: Colors.grey,),SizedBox(width: 4),Text(controller.getPlatformName(item.platform),style: TextStyle(fontSize: 10, color: Colors.grey),),SizedBox(width: 8),Expanded(child: Text(_buildStatusText(item),style: TextStyle(color: _getStatusColor(item.status),fontSize: 12,),),),],),],),trailing: _buildTrailingWidget(index, item),),);}Widget _buildFileIcon(UploadItem item) {final ext = item.fileName.split('.').last.toLowerCase();final isImage = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].contains(ext);if (isImage) {return Stack(children: [Container(width: 40,height: 40,decoration: BoxDecoration(color: Colors.grey[200],borderRadius: BorderRadius.circular(4),),child: item.filePath != null && !kIsWeb? Image.file(File(item.filePath!),fit: BoxFit.cover,errorBuilder: (context, error, stackTrace) {return Icon(Icons.image, color: Colors.blue);},): Icon(Icons.image, color: Colors.blue),),if (item.status == UploadStatus.uploading)Container(width: 40,height: 40,color: Colors.black54,child: Center(child: SizedBox(width: 20,height: 20,child: CircularProgressIndicator(strokeWidth: 2,valueColor: AlwaysStoppedAnimation<Color>(Colors.white),),),),),],);} else {final icon = switch (ext) {'pdf' => Icons.picture_as_pdf,'doc' || 'docx' => Icons.description,'zip' || 'rar' => Icons.archive,_ => Icons.insert_drive_file,};return Icon(icon, color: Colors.blue, size: 40);}}Widget _buildTrailingWidget(int index, UploadItem item) {final controller = Get.find<CrossPlatformUploadController>();return switch (item.status) {UploadStatus.pending => IconButton(icon: Icon(Icons.cancel, color: Colors.red),onPressed: () => controller.removeFile(index),),UploadStatus.uploading => SizedBox(width: 24,height: 24,child: CircularProgressIndicator(strokeWidth: 2,value: item.progress / 100,),),UploadStatus.completed => Icon(Icons.check_circle, color: Colors.green),UploadStatus.failed => Row(mainAxisSize: MainAxisSize.min,children: [Icon(Icons.error, color: Colors.red),SizedBox(width: 8),IconButton(icon: Icon(Icons.refresh, color: Colors.blue),onPressed: () => controller.retryUpload(index),),],),};}String _buildStatusText(UploadItem item) {final fileSize = _formatFileSize(item.fileSize);return switch (item.status) {UploadStatus.pending => '等待上传 - $fileSize',UploadStatus.uploading => '上传中: ${item.progress.toStringAsFixed(1)}% - $fileSize',UploadStatus.completed => '上传成功 - $fileSize',UploadStatus.failed => '上传失败: ${item.error} - $fileSize',};}Color _getStatusColor(UploadStatus status) {return switch (status) {UploadStatus.pending => Colors.orange,UploadStatus.uploading => Colors.blue,UploadStatus.completed => Colors.green,UploadStatus.failed => Colors.red,};}String _formatFileSize(int bytes) {if (bytes <= 0) return '未知大小';if (bytes < 1024) return '$bytes B';if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';}
}
4. 拖拽上传组件 (DragDropUploadArea)
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:universal_html/html.dart' as html;class DragDropUploadArea extends StatefulWidget {final Function(List<html.File> files) onFilesDropped;final String title;final String subtitle;final Widget? icon;final List<String>? allowedFileTypes;final int maxFiles;const DragDropUploadArea({Key? key,required this.onFilesDropped,this.title = '拖拽文件到此处上传',this.subtitle = '支持单个或多个文件',this.icon,this.allowedFileTypes,this.maxFiles = 5,}) : super(key: key); _DragDropUploadAreaState createState() => _DragDropUploadAreaState();
}class _DragDropUploadAreaState extends State<DragDropUploadArea> {var isDragging = false.obs;void initState() {super.initState();if (kIsWeb) {_setupDropZone();}}void _setupDropZone() {final dropZone = html.DivElement();dropZone.style..position = 'absolute'..top = '0'..left = '0'..width = '100%'..height = '100%'..pointerEvents = 'none';dropZone.onDragOver.listen((event) {event.preventDefault();event.stopPropagation();isDragging.value = true;});dropZone.onDragLeave.listen((event) {event.preventDefault();event.stopPropagation();isDragging.value = false;});dropZone.onDrop.listen((event) {event.preventDefault();event.stopPropagation();isDragging.value = false;final files = event.dataTransfer?.files;if (files != null && files.isNotEmpty) {final selectedFiles = files.take(widget.maxFiles).toList();widget.onFilesDropped(selectedFiles);}});html.document.body?.append(dropZone);} Widget build(BuildContext context) {return Obx(() => Container(width: double.infinity,height: 200,decoration: BoxDecoration(border: Border.all(color: isDragging.value ? Colors.blue : Colors.grey,width: isDragging.value ? 3 : 2,),borderRadius: BorderRadius.circular(12),color: isDragging.value ? Colors.blue.withOpacity(0.1) : Colors.grey.withOpacity(0.1),),child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [widget.icon ?? Icon(isDragging.value ? Icons.cloud_done : Icons.cloud_upload,size: 48,color: isDragging.value ? Colors.blue : Colors.grey,),const SizedBox(height: 16),Text(widget.title,style: TextStyle(fontSize: 18,fontWeight: FontWeight.bold,color: isDragging.value ? Colors.blue : Colors.grey[700],),),const SizedBox(height: 8),Text(widget.subtitle,style: TextStyle(color: Colors.grey[600],),),if (!kIsWeb) ...[const SizedBox(height: 16),Text('移动端请使用下方按钮选择文件',style: TextStyle(fontSize: 12,color: Colors.grey[500],),),],],),));}
}
5. 主上传页面 (CrossPlatformUploadPage)
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:universal_html/html.dart' as html;class CrossPlatformUploadPage extends StatelessWidget {final CrossPlatformUploadController controller;const CrossPlatformUploadPage({Key? key, required this.controller}) : super(key: key); Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Obx(() {final stats = controller.getUploadStats();return Text('文件上传 (${stats['completed']}/${stats['total']})');}),backgroundColor: Colors.blue,actions: [// 平台指示器Padding(padding: EdgeInsets.symmetric(horizontal: 16),child: Row(children: [Icon(controller.getPlatformIcon(controller.currentPlatform),color: Colors.white,),SizedBox(width: 4),Text(controller.getPlatformName(controller.currentPlatform),style: TextStyle(color: Colors.white),),],),),],),body: Padding(padding: const EdgeInsets.all(16.0),child: Column(crossAxisAlignment: CrossAxisAlignment.start,children: [// Web 拖拽上传区域if (kIsWeb) ...[DragDropUploadArea(onFilesDropped: (files) => controller._addWebFilesToList(files),title: '拖拽文件到此处上传',subtitle: '支持图片、文档、压缩文件等',allowedFileTypes: ['.jpg', '.png', '.pdf', '.doc', '.docx', '.zip'],maxFiles: 5,),const SizedBox(height: 20),],// 操作按钮区域_buildActionButtons(),const SizedBox(height: 20),// 总进度条Obx(() => controller.isUploading.value ? Column(children: [LinearProgressIndicator(value: controller.totalProgress.value / 100,backgroundColor: Colors.grey[200],valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),),SizedBox(height: 8),Text('总进度: ${controller.totalProgress.value.toStringAsFixed(1)}%',style: TextStyle(fontSize: 12, color: Colors.grey),),],): SizedBox.shrink()),const SizedBox(height: 20),// 上传列表标题_buildListHeader(),const SizedBox(height: 10),// 上传列表_buildUploadList(),],),),);}Widget _buildActionButtons() {return Wrap(spacing: 10,runSpacing: 10,children: [// 选择图片按钮ElevatedButton.icon(onPressed: controller.pickImageFiles,icon: Icon(Icons.photo_library),label: Text('选择图片'),),// 选择文件按钮ElevatedButton.icon(onPressed: controller.pickAnyFiles,icon: Icon(Icons.attach_file),label: Text('选择文件'),),// 开始上传按钮Obx(() => ElevatedButton(onPressed: controller.uploadItems.isEmpty || controller.isUploading.value? null: controller.uploadAllFiles,style: ElevatedButton.styleFrom(backgroundColor: Colors.green,foregroundColor: Colors.white,),child: controller.isUploading.value? Row(mainAxisSize: MainAxisSize.min,children: [SizedBox(width: 16,height: 16,child: CircularProgressIndicator(strokeWidth: 2,valueColor: AlwaysStoppedAnimation(Colors.white),),),SizedBox(width: 8),Text('上传中...'),],): Text('开始上传'),)),// 清空列表按钮Obx(() => TextButton(onPressed: controller.uploadItems.isEmpty ? null : controller.clearAll,child: Text('清空列表'),)),// 上传统计信息Obx(() {final stats = controller.getUploadStats();return Container(padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),decoration: BoxDecoration(color: Colors.grey[100],borderRadius: BorderRadius.circular(16),),child: Text('总计: ${stats['total']} | ''完成: ${stats['completed']} | ''失败: ${stats['failed']}',style: TextStyle(fontSize: 12,color: Colors.grey[600],),),);}),],);}Widget _buildListHeader() {return Row(children: [Text('上传队列',style: TextStyle(fontSize: 18,fontWeight: FontWeight.bold,),),SizedBox(width: 10),Obx(() => Text('(${controller.uploadItems.length} 个文件)',style: TextStyle(fontSize: 14,color: Colors.grey[600],),)),],);}Widget _buildUploadList() {return Expanded(child: Obx(() {if (controller.uploadItems.isEmpty) {return _buildEmptyState();}return ListView.builder(itemCount: controller.uploadItems.length,itemBuilder: (context, index) {final item = controller.uploadItems[index];return UploadListItem(index: index,item: item,);},);}),);}Widget _buildEmptyState() {return Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [Icon(kIsWeb ? Icons.cloud_upload : Icons.phone_android,size: 64,color: Colors.grey,),SizedBox(height: 16),Text('暂无待上传文件',style: TextStyle(fontSize: 16, color: Colors.grey),),SizedBox(height: 8),Text(kIsWeb ? '点击上方按钮或拖拽文件到上传区域': '点击上方按钮选择文件',style: TextStyle(fontSize: 14, color: Colors.grey),),],),);}
}
6. 应用入口和绑定
import 'package:flutter/material.dart';
import 'package:get/get.dart';void main() {runApp(MyApp());
}class MyApp extends StatelessWidget { Widget build(BuildContext context) {return GetMaterialApp(title: '跨平台文件上传示例',theme: ThemeData(primarySwatch: Colors.blue,useMaterial3: true,),home: UploadBinding(),);}
}class UploadBinding extends StatelessWidget { Widget build(BuildContext context) {return GetBuilder<CrossPlatformUploadController>(init: CrossPlatformUploadController(uploadUrl: 'https://your-upload-endpoint.com/upload',headers: {'Authorization': 'Bearer your-token','Content-Type': 'multipart/form-data',},maxFiles: 5,),builder: (controller) {return CrossPlatformUploadPage(controller: controller);},);}
}
主要特性
-
跨平台支持:
- Web:使用 HTML5 File API 和拖拽上传
- Android/iOS:使用 ImagePicker 选择文件
- 统一的界面和用户体验
-
文件类型支持:
- 图片文件(JPEG、PNG、GIF 等)
- 任意文件类型
- 文件大小和类型验证
-
上传功能:
- Web:使用 Dio 进行文件上传
- Android:使用 Dio 进行文件上传
- 实时进度显示
- 错误处理和重试机制
-
GetX 状态管理:
- 响应式状态更新
- 依赖注入和管理
- 简洁的代码组织
-
用户友好的界面:
- 平台自适应的界面
- 拖拽上传(Web)
- 清晰的进度和状态显示
- 平台标识和统计信息
这个实现提供了完整的跨平台文件上传解决方案,支持 Web 和 Android 平台,使用统一的代码结构和用户界面。你可以根据实际需求进一步定制和扩展功能。