如何基于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
相关产品推荐
相关产品推荐

