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

Flutter中使用GetX实现连续递增的视频上传进度条

问题修复:连续平滑的双阶段上传进度条

现有代码的核心问题

  1. 视频上传无真实进度反馈:Service层接收了onProgress回调但未实际调用,Provider的文件上传方法也没实现进度监听,导致第一阶段进度要么靠定时器模拟,要么逻辑错误
  2. 进度跳变突兀:视频上传完成后直接从40%跳到70%,破坏平滑体验
  3. 第二阶段进度为假模拟:用while循环强制递增进度,和真实API执行时长无关,且可能阻塞UI线程
  4. 进度逻辑混乱: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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 06:12:05