Flutter中使用GetX实现连续递增的视频上传进度条
问题修复:连续平滑的双阶段上传进度条
现有代码的核心问题
- 视频上传无真实进度反馈:Service层接收了
onProgress回调但未实际调用,Provider的文件上传方法也没实现进度监听,导致第一阶段进度要么靠定时器模拟,要么逻辑错误 - 进度跳变突兀:视频上传完成后直接从40%跳到70%,破坏平滑体验
- 第二阶段进度为假模拟:用
while循环强制递增进度,和真实API执行时长无关,且可能阻塞UI线程 - 进度逻辑混乱:API进度乘以0.4的计算逻辑不合理,和预设的阶段区间不匹配
修复方案
1. 实现真实文件上传进度监听
在Provider的文件上传方法中添加进度回调,通过http.MultipartRequest的send()方法监听流进度
2. 划分清晰的进度区间
- 视频文件上传:0% → 60%
- 训练数据上传:60% → 100%
3. 平滑过渡与兜底模拟
- 优先使用真实API进度驱动
- 若真实进度长时间未更新,添加缓慢的兜底递增,避免进度条停滞
4. 移除阻塞UI的循环
用Timer.periodic替代while循环,实现第二阶段的平滑递增
修改后的代码
Controller 代码
import 'dart:async'; import 'package:get/get.dart'; import 'package:interkashi/modules/components/extension/get_extension.dart'; import 'package:interkashi/modules/feature/academy/model/training_video_upload_model.dart'; import 'package:interkashi/modules/feature/academy/service/upload_video_service.dart'; class UploadVideoController extends GetxController { final UploadVideoService _uploadVideoService; UploadVideoController(this._uploadVideoService); Rxn<String> selectedVideoPath = Rxn<String>(); RxBool isUploading = RxBool(false); RxDouble uploadProgress = RxDouble(0.0); Rx<bool?> uploadSuccess = Rx<bool?>(null); final double maxSizeMB = 100; RxBool flag = true.obs; late String lessonId; late String courseId; Timer? progressFallbackTimer; bool isFirstApiCompleted = false; @override void onInit() { super.onInit(); final arguments = Get.arguments; lessonId = arguments['lessonId']; courseId = arguments['courseId']; } @override void onClose() { progressFallbackTimer?.cancel(); super.onClose(); } Future<void> checkFileSize(String? video) async { if (video == null) return; File videoFile = File(video); int fileSize = videoFile.lengthSync(); double mxSize = fileSize / (1024 * 1024); if (mxSize > maxSizeMB) { flag.value = false; GetExt.snackBar("请上传小于100MB的文件"); } else { selectedVideoPath.value = video; flag.value = true; } } // 启动兜底进度(防止真实进度停滞时进度条不动) void startFallbackProgress() { progressFallbackTimer?.cancel(); progressFallbackTimer = Timer.periodic(const Duration(milliseconds: 200), (timer) { if (!isFirstApiCompleted && uploadProgress.value < 0.55) { // 第一阶段兜底:最多到55%,留5%给完成后的平滑过渡 uploadProgress.value += 0.005; } else if (isFirstApiCompleted && uploadProgress.value < 0.95) { // 第二阶段兜底:最多到95%,留5%给完成后的平滑过渡 uploadProgress.value += 0.005; } else { timer.cancel(); } }); } // 平滑更新进度到目标值 Future<void> smoothUpdateProgress(double target) async { const step = 0.01; const delay = Duration(milliseconds: 50); while (uploadProgress.value < target) { uploadProgress.value += step; if (uploadProgress.value > target) uploadProgress.value = target; await Future.delayed(delay); } } Future<void> uploadVideo() async { if (selectedVideoPath.value == null || !flag.value) { return; } uploadSuccess.value = null; isUploading.value = true; uploadProgress.value = 0.0; isFirstApiCompleted = false; startFallbackProgress(); // 第一API:上传视频文件(进度区间0% - 60%) final uploadResponse = await _uploadVideoService.uploadVideo( selectedVideoPath.value!, (realProgress) { // 真实进度映射到0-60%区间 final mappedProgress = realProgress * 0.6; if (mappedProgress > uploadProgress.value) { uploadProgress.value = mappedProgress; } }, ); progressFallbackTimer?.cancel(); if (uploadResponse != null) { // 平滑过渡到60% await smoothUpdateProgress(0.6); isFirstApiCompleted = true; startFallbackProgress(); // 第二API:上传训练数据(进度区间60% - 100%) final trainingResponse = await _uploadVideoService.uploadTrainingData( TrainingVideoUploadModel( lessonId: lessonId, courseId: courseId, videoUrl: uploadResponse.video.url, thumbnailUrl: uploadResponse.thumbnail.url, ), (realProgress) { // 真实进度映射到60-100%区间 final mappedProgress = 0.6 + (realProgress * 0.4); if (mappedProgress > uploadProgress.value) { uploadProgress.value = mappedProgress; } }, ); progressFallbackTimer?.cancel(); if (trainingResponse != null) { // 平滑过渡到100% await smoothUpdateProgress(1.0); await Future.delayed(const Duration(milliseconds: 500)); uploadSuccess.value = true; } else { uploadSuccess.value = false; GetExt.snackBar("训练数据上传失败"); } } else { uploadSuccess.value = false; GetExt.snackBar("视频上传失败"); } selectedVideoPath.value = null; isUploading.value = false; } }
Provider 代码
import 'dart:io'; import 'dart:typed_data'; import 'package:get/get_connect/connect.dart'; import 'package:http/http.dart' as http; import 'package:interkashi/modules/components/provider/base_provider.dart'; import 'package:interkashi/modules/feature/academy/model/training_video_upload_model.dart'; class UploadVideoProvider extends BaseProvider { Future<http.Response> uploadVideo(File file, Function(double) onProgress) async { final url = "${baseUrl}/files/videoAndThumbnailUpload"; final request = http.MultipartRequest('POST', Uri.parse(url)); // 添加文件 request.files.add(await http.MultipartFile.fromPath('file', file.path)); // 监听上传进度(真实场景下的实现) final stream = request.send(); final totalBytes = file.lengthSync(); int uploadedBytes = 0; // 实时监听上传字节数 final responseStream = stream.asBroadcastStream(); responseStream.listen((chunk) { uploadedBytes += chunk.length; final progress = uploadedBytes / totalBytes; onProgress(progress); }); final response = await responseStream.first; final responseBody = await response.stream.bytesToString(); return http.Response(responseBody, response.statusCode); } Future<Response> uploadTrainingData(TrainingVideoUploadModel trainingVideoModel, Function(double) onProgress) async { String url = "/training/upload"; final body = { 'lessonId': trainingVideoModel.lessonId, 'courseId': trainingVideoModel.courseId, 'videoUrl': trainingVideoModel.videoUrl, 'thumbnailUrl': trainingVideoModel.thumbnailUrl, }; // 模拟训练数据上传进度(若API支持真实进度监听,替换为对应逻辑) for (int i = 0; i <= 100; i += 15) { await Future.delayed(const Duration(milliseconds: 200)); onProgress(i / 100); } final response = await post(url, body); return response; } }
Service 代码
import 'dart:convert'; import 'dart:io'; import 'package:interkashi/modules/components/service/base_service.dart'; import 'package:interkashi/modules/feature/academy/model/training_video_upload_model.dart'; import 'package:interkashi/modules/feature/academy/model/upload_video_model.dart'; import 'package:interkashi/modules/feature/academy/provider/upload_video_provider.dart'; class UploadVideoService extends BaseService { final UploadVideoProvider _uploadVideoProvider; UploadVideoService(this._uploadVideoProvider); Future<UploadVideoModel?> uploadVideo(String filePath, Function(double) onProgress) async { try { final response = await _uploadVideoProvider.uploadVideo(File(filePath), onProgress); return UploadVideoModel.fromJson(jsonDecode(response.body)); } catch (e) { print("视频上传错误: $e"); return null; } } Future<TrainingVideoUploadModel?> uploadTrainingData(TrainingVideoUploadModel trainingVideoModel, Function(double) onProgress) async { try { final response = await _uploadVideoProvider.uploadTrainingData(trainingVideoModel, onProgress); if (response.statusCode == 200) { if (response.body is Map<String, dynamic>) { return TrainingVideoUploadModel.fromJson(response.body); } else { return TrainingVideoUploadModel.fromJson(jsonDecode(response.body)); } } else { return null; } } catch (e) { print('训练数据上传错误: $e'); return null; } } }
内容的提问来源于stack exchange,提问作者Ravindra Yadav
相关产品推荐
相关产品推荐

