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

Flutter中Workmanager后台上传进度无法更新UI的问题解决

问题根源

Workmanager的callbackDispatcher运行在独立的隔离Isolate中,和主UI线程的内存完全隔离。你在主线程定义的顶级uploadPercent(ValueNotifier)和videoUploadingProvider,在后台任务里是完全独立的实例——更新后台实例的值,根本不会同步到UI线程的实例上,这就是直接调用正常、用Workmanager失效的核心原因。

解决方案

下面提供两种可行的实现方式,根据你的需求选择:


方案一:用SharedPreferences存储进度(简单易实现)

通过SharedPreferences在后台任务和UI线程之间共享进度数据,UI侧定时读取并更新。

1. 后台任务修改进度存储逻辑

在uploadAsset的onChanged回调中,将计算后的进度写入SharedPreferences,用任务的唯一ID作为键,避免多任务冲突:

await uploadAsset(
  rid: uploadVideoData?.rId ?? 0,
  grpId: uploadVideoData?.groupId ?? 0,
  pId: uploadVideoData?.pId ?? 0,
  asset: File(compressedVideoPath),
  onChanged: (percent) async {
    double calculatedPercent = uploadVideoData?.fromGallery == true
        ? ((percent * 100) / 2) + 50
        : percent * 100;
    
    // 写入SharedPreferences
    final prefs = await SharedPreferences.getInstance();
    await prefs.setDouble('upload_progress_$uniqueId', calculatedPercent);
    
    debugPrint('Uploading percent is: $calculatedPercent');
  },
);

注意:要把任务的uniqueId传递到uploadVideos和uploadAsset函数中。

2. UI侧监听进度更新

在UI页面中,通过定时读取SharedPreferences来更新进度UI:

class UploadProgressWidget extends StatefulWidget {
  final String taskUniqueId;
  const UploadProgressWidget({super.key, required this.taskUniqueId});

  @override
  State<UploadProgressWidget> createState() => _UploadProgressWidgetState();
}

class _UploadProgressWidgetState extends State<UploadProgressWidget> {
  late ValueNotifier<double> _progressNotifier;
  late SharedPreferences _prefs;
  Timer? _progressTimer;

  @override
  void initState() {
    super.initState();
    _progressNotifier = ValueNotifier(0);
    _initPrefs();
    // 每500ms读取一次进度,平衡实时性和性能
    _progressTimer = Timer.periodic(const Duration(milliseconds: 500), (_) => _updateProgress());
  }

  Future<void> _initPrefs() async {
    _prefs = await SharedPreferences.getInstance();
    await _updateProgress();
  }

  Future<void> _updateProgress() async {
    final progress = _prefs.getDouble('upload_progress_${widget.taskUniqueId}') ?? 0;
    _progressNotifier.value = progress;

    // 进度完成后清理缓存并停止定时器
    if (progress >= 100) {
      _progressTimer?.cancel();
      await _prefs.remove('upload_progress_${widget.taskUniqueId}');
    }
  }

  @override
  void dispose() {
    _progressTimer?.cancel();
    _progressNotifier.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<double>(
      valueListenable: _progressNotifier,
      builder: (context, progress, _) {
        return LinearProgressIndicator(value: progress / 100);
      },
    );
  }
}

方案二:Isolate通信(实时性更高)

利用Isolate的SendPort/ReceivePort机制,直接在后台任务和主UI线程之间传递进度消息,适合对实时性要求高的场景。

1. 主线程初始化通信端口

在main函数中创建ReceivePort,监听后台任务的进度消息:

// 主Isolate的通信端口
final ReceivePort _progressReceivePort = ReceivePort();
// 用Map管理多个任务的进度
final ValueNotifier<Map<String, double>> uploadProgressMap = ValueNotifier({});

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // 监听后台传来的进度消息
  _progressReceivePort.listen((message) {
    if (message is Map<String, dynamic>) {
      final taskId = message['taskId'] as String;
      final progress = message['progress'] as double;
      // 更新进度Map
      uploadProgressMap.value = {...uploadProgressMap.value, taskId: progress};
    }
  });

  // 初始化Workmanager
  await Workmanager().initialize(
    callbackDispatcher,
    isInDebugMode: true,
  );
  
  runApp(const YourApp());
}

2. 注册任务时传递通信端口

注册Workmanager任务时,把SendPort的序列化数据传入inputData:

String uniqueId = '${uploadVideoData.rId}_${uploadVideoData.pId}_${uploadVideoData.groupId}';
await Workmanager().registerOneOffTask(
  uniqueId,
  uniqueId,
  inputData: {
    ...uploadVideoData.toJson(),
    // 序列化SendPort
    'sendPort': _progressReceivePort.sendPort.toRawSendPort().toJson(),
    'taskId': uniqueId,
  },
);

3. 后台任务发送进度消息

在callbackDispatcher中还原SendPort,然后在进度更新时发送消息到主线程:

@pragma('vm:entry-point')
void callbackDispatcher() {
  Workmanager().executeTask((task, inputData) async {
    if (inputData != null) {
      // 还原SendPort
      final rawSendPort = RawSendPort.fromJson(inputData['sendPort']);
      final sendPort = SendPort.fromRawSendPort(rawSendPort);
      final taskId = inputData['taskId'] as String;

      await uploadVideos(
        uploadVideoData: UploadVideoData.fromJson(inputData),
        sendPort: sendPort,
        taskId: taskId,
      );
    }
    return Future.value(true);
  });
}

// 修改uploadVideos函数,接收通信参数
Future<void> uploadVideos({
  required UploadVideoData uploadVideoData,
  required SendPort sendPort,
  required String taskId,
}) async {
  // ...其他逻辑
  await uploadAsset(
    rid: uploadVideoData?.rId ?? 0,
    grpId: uploadVideoData?.groupId ?? 0,
    pId: uploadVideoData?.pId ?? 0,
    asset: File(compressedVideoPath),
    onChanged: (percent) {
      double calculatedPercent = uploadVideoData?.fromGallery == true
          ? ((percent * 100) / 2) + 50
          : percent * 100;
      
      // 发送进度到主UI线程
      sendPort.send({
        'taskId': taskId,
        'progress': calculatedPercent,
      });

      debugPrint('Uploading percent is: $calculatedPercent');
    },
  );
  // ...其他逻辑
}

4. UI侧监听进度Map

在UI中通过ValueListenableBuilder监听进度Map的变化:

ValueListenableBuilder<Map<String, double>>(
  valueListenable: uploadProgressMap,
  builder: (context, progressMap, _) {
    final progress = progressMap[yourTaskUniqueId] ?? 0;
    return LinearProgressIndicator(value: progress / 100);
  },
);
注意事项
  • 后台任务完成后,记得清理SharedPreferences中的进度缓存(方案一),或者发送完成消息通知主线程移除进度记录(方案二)。
  • Android平台下,Workmanager任务可能在应用被杀后继续运行,UI重启时需要读取SharedPreferences恢复进度状态。

内容的提问来源于stack exchange,提问作者Shreyansh Sharma

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 03:13:09