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

如何基于PocketBase实现Flutter多平台文件下载功能?

全平台兼容的Flutter文件下载方案(PocketBase + Cubit)

一、多平台适配核心要点

1. 存储路径适配

用path_provider获取各平台合规存储目录,避免权限问题:

  • Android:应用专属目录(getApplicationDocumentsDirectory()),无需额外权限;若需存公共目录,Android 13+需申请READ_MEDIA_IMAGES等细分权限
  • iOS:getApplicationDocumentsDirectory(),沙盒内目录无需权限
  • Windows/macOS/Linux:getApplicationSupportDirectory()
  • Web:复用已实现的浏览器下载逻辑

2. 下载工具与PocketBase集成

用dio替代原生http,支持断点续传、进度监听且全平台兼容。通过PocketBase API获取文件下载URL:

// 从PocketBase记录中获取文件下载地址
String fileUrl = pb.files.getUrl(pocketBaseRecord, "file_field_name");

二、动态批量下载逻辑实现

1. 封装下载任务模型

统一管理每个任务的状态与参数:

enum DownloadStatus { waiting, downloading, completed, failed }

class DownloadTask {
  final String id; // 用PocketBase记录ID作为唯一标识
  final String url;
  final String savePath;
  double progress;
  DownloadStatus status;
  String? errorMessage;

  DownloadTask({
    required this.id,
    required this.url,
    required this.savePath,
    this.progress = 0.0,
    this.status = DownloadStatus.waiting,
    this.errorMessage,
  });

  // 状态更新时复制对象,保证不可变性
  DownloadTask copyWith({
    double? progress,
    DownloadStatus? status,
    String? errorMessage,
  }) {
    return DownloadTask(
      id: id,
      url: url,
      savePath: savePath,
      progress: progress ?? this.progress,
      status: status ?? this.status,
      errorMessage: errorMessage ?? this.errorMessage,
    );
  }
}

2. Cubit状态与任务管理

创建DownloadCubit控制下载任务的并发、状态更新:

// 状态类
class DownloadState extends Equatable {
  final List<DownloadTask> tasks;
  final int maxConcurrentTasks; // 限制最大并发数,比如3

  const DownloadState({
    this.tasks = const [],
    this.maxConcurrentTasks = 3,
  });

  DownloadState copyWith({
    List<DownloadTask>? tasks,
    int? maxConcurrentTasks,
  }) {
    return DownloadState(
      tasks: tasks ?? this.tasks,
      maxConcurrentTasks: maxConcurrentTasks ?? this.maxConcurrentTasks,
    );
  }

  @override
  List<Object> get props => [tasks, maxConcurrentTasks];
}

// Cubit核心逻辑
class DownloadCubit extends Cubit<DownloadState> {
  final Dio _dio = Dio();
  final Map<String, CancelToken> _cancelTokens = {}; // 存储取消令牌
  int _activeTasks = 0;

  DownloadCubit() : super(const DownloadState());

  // 添加单个任务
  void addDownloadTask(DownloadTask task) {
    final updatedTasks = List<DownloadTask>.from(state.tasks)..add(task);
    emit(state.copyWith(tasks: updatedTasks));
    _startPendingTasks();
  }

  // 批量添加任务
  void addBatchTasks(List<DownloadTask> tasks) {
    final updatedTasks = List<DownloadTask>.from(state.tasks)..addAll(tasks);
    emit(state.copyWith(tasks: updatedTasks));
    _startPendingTasks();
  }

  // 启动等待队列中的任务,控制并发数
  void _startPendingTasks() {
    final pendingTasks = state.tasks.where((t) => t.status == DownloadStatus.waiting).toList();
    while (_activeTasks < state.maxConcurrentTasks && pendingTasks.isNotEmpty) {
      final task = pendingTasks.first;
      _startDownload(task);
      pendingTasks.removeAt(0);
    }
  }

  // 执行单个下载任务
  void _startDownload(DownloadTask task) async {
    _activeTasks++;
    final cancelToken = CancelToken();
    _cancelTokens[task.id] = cancelToken;

    // 更新任务状态为下载中
    final updatedTasks = state.tasks.map((t) => t.id == task.id ? t.copyWith(status: DownloadStatus.downloading) : t).toList();
    emit(state.copyWith(tasks: updatedTasks));

    try {
      await _dio.download(
        task.url,
        task.savePath,
        cancelToken: cancelToken,
        onReceiveProgress: (count, total) {
          final progress = total > 0 ? count / total : 0.0;
          // 实时更新进度
          final progressUpdatedTasks = state.tasks.map((t) => t.id == task.id ? t.copyWith(progress: progress) : t).toList();
          emit(state.copyWith(tasks: progressUpdatedTasks));
        },
      );

      // 标记任务完成
      final completedTasks = state.tasks.map((t) => t.id == task.id ? t.copyWith(status: DownloadStatus.completed) : t).toList();
      emit(state.copyWith(tasks: completedTasks));
    } catch (e) {
      // 标记任务失败
      final failedTasks = state.tasks.map((t) => t.id == task.id ? t.copyWith(status: DownloadStatus.failed, errorMessage: e.toString()) : t).toList();
      emit(state.copyWith(tasks: failedTasks));
    } finally {
      _activeTasks--;
      _cancelTokens.remove(task.id);
      // 启动下一个等待任务
      _startPendingTasks();
    }
  }

  // 取消指定任务
  void cancelDownload(String taskId) {
    _cancelTokens[taskId]?.cancel("用户取消下载");
    final updatedTasks = state.tasks.map((t) => t.id == taskId ? t.copyWith(status: DownloadStatus.failed, errorMessage: "已取消") : t).toList();
    emit(state.copyWith(tasks: updatedTasks));
  }

  @override
  Future<void> close() {
    // 清理所有取消令牌
    _cancelTokens.values.forEach((token) => token.cancel("Cubit已关闭"));
    _cancelTokens.clear();
    return super.close();
  }
}

3. UI层动态渲染任务列表

监听Cubit状态,实时更新任务展示:

BlocBuilder<DownloadCubit, DownloadState>(
  builder: (context, state) {
    return ListView.builder(
      itemCount: state.tasks.length,
      itemBuilder: (context, index) {
        final task = state.tasks[index];
        return ListTile(
          title: Text("文件ID: ${task.id}"),
          subtitle: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              if (task.status == DownloadStatus.downloading)
                LinearProgressIndicator(value: task.progress),
              Text(_getStatusText(task.status)),
              if (task.errorMessage != null)
                Text(task.errorMessage!, style: TextStyle(color: Colors.red)),
            ],
          ),
          trailing: task.status == DownloadStatus.downloading
              ? IconButton(
                  icon: Icon(Icons.cancel),
                  onPressed: () => context.read<DownloadCubit>().cancelDownload(task.id),
                )
              : null,
        );
      },
    );
  },
);

String _getStatusText(DownloadStatus status) {
  switch (status) {
    case DownloadStatus.waiting:
      return "等待下载";
    case DownloadStatus.downloading:
      return "下载中";
    case DownloadStatus.completed:
      return "下载完成";
    case DownloadStatus.failed:
      return "下载失败";
  }
}

三、额外优化建议

  • 断点续传:下载前读取已下载文件长度,在dio.download中添加headers: {"Range": "bytes=${downloadedLength}-"}
  • 任务持久化:用hive保存未完成任务,应用重启后可恢复下载
  • 移动端通知:结合flutter_local_notifications在通知栏展示下载进度
  • 错误重试:为失败任务添加重试按钮,触发重新下载逻辑

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 19:03:18