Flutter后台下载管理插件background_downloader_sql的使用

发布于 1周前 作者 gougou168 来自 Flutter

Flutter后台下载管理插件 background_downloader_sql 的使用

1. 插件简介

background_downloader_sqlbackground_downloader 的扩展包,它为下载器添加了基于 SQLite 的持久化存储功能。默认情况下,background_downloader 使用的是基于文件系统的 LocalStorePersistentStorage,而 background_downloader_sql 则使用 SQLite 数据库来存储下载任务的状态和其他相关信息。

2. 使用 SqlitePersistentStorage

要使用 SqlitePersistentStorage,你需要在第一次调用 FileDownloader 时传递一个 SqlitePersistentStorage 实例。这可以通过以下代码实现:

final sqlStorage = SqlitePersistentStorage(migrationOptions: ['local_store', 'flutter_downloader']);
FileDownloader(persistentStorage: sqlStorage);

这段代码会初始化 FileDownloader,并使用 SQLite 数据库作为持久化存储。你还可以通过 migrationOptions 参数指定从其他存储方式(如 LocalStoreFlutter Downloader)进行数据迁移。

3. 数据库结构

SqlitePersistentStorage 使用简单的表结构来存储不同类型的数据,每个表都有一个 taskId 列和一个 objectJsonMap 列,后者用于存储对象的 JSON 字符串表示。你可以通过 retrieveTaskRecords 方法查询数据库中的任务记录。

4. 迁移选项

background_downloader_sql 支持从 LocalStoreFlutter Downloader 的 SQLite 数据库进行数据迁移。你可以通过在 SqlitePersistentStorage 构造函数中传递 migrationOptions 来启用这些迁移选项。

final sqlStorage = SqlitePersistentStorage(migrationOptions: ['local_store', 'flutter_downloader']);

5. 完整示例 Demo

下面是一个完整的示例应用,展示了如何使用 background_downloader_sql 进行后台下载管理。这个示例包括下载、暂停、恢复、取消等功能,并且展示了如何处理通知和权限请求。

import 'dart:async';
import 'dart:io';
import 'dart:math';

import 'package:background_downloader/background_downloader.dart';
import 'package:background_downloader_sql/background_downloader_sql.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';

void main() {
  Logger.root.onRecord.listen((LogRecord rec) {
    debugPrint(
        '${rec.loggerName}>${rec.level.name}: ${rec.time}: ${rec.message}');
  });

  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  [@override](/user/override)
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final log = Logger('ExampleApp');
  final buttonTexts = ['Download', 'Cancel', 'Pause', 'Resume', 'Reset'];

  ButtonState buttonState = ButtonState.download;
  bool downloadWithError = false;
  TaskStatus? downloadTaskStatus;
  DownloadTask? backgroundDownloadTask;
  StreamController<TaskProgressUpdate> progressUpdateStream = StreamController();

  bool loadAndOpenInProgress = false;
  bool loadABunchInProgress = false;

  [@override](/user/override)
  void initState() {
    super.initState();
    
    // 初始化 FileDownloader 并使用 SqlitePersistentStorage
    FileDownloader(persistentStorage: SqlitePersistentStorage());

    // 配置下载器
    FileDownloader().configure(globalConfig: [
      (Config.requestTimeout, const Duration(seconds: 100)),
    ], androidConfig: [
      (Config.useCacheDir, Config.whenAble),
    ], iOSConfig: [
      (Config.localize, {'Cancel': 'StopIt'}),
    ]).then((result) => debugPrint('Configuration result = $result'));

    // 注册回调并配置通知
    FileDownloader()
        .registerCallbacks(taskNotificationTapCallback: myNotificationTapCallback)
        .configureNotificationForGroup(FileDownloader.defaultGroup,
            running: const TaskNotification('Download {filename}', 'File: {filename} - {progress} - speed {networkSpeed} and {timeRemaining} remaining'),
            complete: const TaskNotification('{displayName} download {filename}', 'Download complete'),
            error: const TaskNotification('Download {filename}', 'Download failed'),
            paused: const TaskNotification('Download {filename}', 'Paused with metadata {metadata}'),
            progressBar: true)
        .configureNotificationForGroup('bunch',
            running: const TaskNotification('{numFinished} out of {numTotal}', 'Progress = {progress}'),
            complete: const TaskNotification("Done!", "Loaded {numTotal} files"),
            error: const TaskNotification('Error', '{numFailed}/{numTotal} failed'),
            progressBar: false,
            groupNotificationId: 'notGroup')
        .configureNotification(
            complete: const TaskNotification('Download {filename}', 'Download complete'),
            tapOpensFile: true); // dog can also open directly from tap

    // 监听更新
    FileDownloader().updates.listen((update) {
      switch (update) {
        case TaskStatusUpdate _:
          if (update.task == backgroundDownloadTask) {
            buttonState = switch (update.status) {
              TaskStatus.running || TaskStatus.enqueued => ButtonState.pause,
              TaskStatus.paused => ButtonState.resume,
              _ => ButtonState.reset
            };
            setState(() {
              downloadTaskStatus = update.status;
            });
          }

        case TaskProgressUpdate _:
          progressUpdateStream.add(update); // 传递给 widget 以显示进度
      }
    });
  }

  /// 处理用户点击通知
  void myNotificationTapCallback(Task task, NotificationType notificationType) {
    debugPrint(
        'Tapped notification $notificationType for taskId ${task.taskId}');
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.purple,
          brightness: Brightness.light,
        ),
      ),
      home: Scaffold(
          appBar: AppBar(
            title: const Text('background_downloader example app'),
          ),
          body: Center(
              child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Padding(
                  padding: const EdgeInsets.all(16),
                  child: Row(
                    children: [
                      Expanded(
                          child: Text('Force error',
                              style: Theme.of(context).textTheme.titleLarge)),
                      Switch(
                          value: downloadWithError,
                          onChanged: (value) {
                            setState(() {
                              downloadWithError = value;
                            });
                          })
                    ],
                  ),
                ),
                Center(
                    child: ElevatedButton(
                  onPressed: processButtonPress,
                  child: Text(
                    buttonTexts[buttonState.index],
                  ),
                )),
                Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Row(
                    children: [
                      const Expanded(child: Text('File download status:')),
                      Text('${downloadTaskStatus ?? "undefined"}')
                    ],
                  ),
                ),
                const Divider(
                  height: 30,
                  thickness: 5,
                  color: Colors.blueGrey,
                ),
                Center(
                    child: ElevatedButton(
                        onPressed:
                            loadAndOpenInProgress ? null : processLoadAndOpen,
                        child: Text(
                          Platform.isIOS ? 'Load, open and add' : 'Load & Open',
                        ))),
                Center(
                    child: Text(
                  loadAndOpenInProgress ? 'Busy' : '',
                )),
                const Divider(
                  height: 30,
                  thickness: 5,
                  color: Colors.blueGrey,
                ),
                Center(
                    child: ElevatedButton(
                        onPressed:
                            loadABunchInProgress ? null : processLoadABunch,
                        child: const Text('Load a bunch'))),
                Center(child: Text(loadABunchInProgress ? 'Enqueueing' : '')),
              ],
            ),
          )),
          bottomSheet: DownloadProgressIndicator(progressUpdateStream.stream,
              showPauseButton: true,
              showCancelButton: true,
              backgroundColor: Colors.grey,
              maxExpandable: 3)),
    );
  }

  /// 处理中心按钮点击(初始为 'Download',但根据状态变化)
  Future<void> processButtonPress() async {
    switch (buttonState) {
      case ButtonState.download:
        // 开始下载
        await getPermission(PermissionType.notifications);
        backgroundDownloadTask = DownloadTask(
            url: downloadWithError
                ? 'https://avmaps-dot-bbflightserver-hrd.appspot.com/public/get_current_app_data' // 返回 403 状态码
                : 'https://storage.googleapis.com/approachcharts/test/5MB-test.ZIP',
            filename: 'zipfile.zip',
            directory: 'my/directory',
            baseDirectory: BaseDirectory.applicationDocuments,
            updates: Updates.statusAndProgress,
            retries: 3,
            allowPause: true,
            metaData: '<example metaData>',
            displayName: 'My display name');
        await FileDownloader().enqueue(backgroundDownloadTask!);
        break;
      case ButtonState.cancel:
        // 取消下载
        if (backgroundDownloadTask != null) {
          await FileDownloader()
              .cancelTasksWithIds([backgroundDownloadTask!.taskId]);
        }
        break;
      case ButtonState.reset:
        downloadTaskStatus = null;
        buttonState = ButtonState.download;
        break;
      case ButtonState.pause:
        if (backgroundDownloadTask != null) {
          await FileDownloader().pause(backgroundDownloadTask!);
        }
        break;
      case ButtonState.resume:
        if (backgroundDownloadTask != null) {
          await FileDownloader().resume(backgroundDownloadTask!);
        }
        break;
    }
    if (mounted) {
      setState(() {});
    }
  }

  /// 处理 'Load & Open' 按钮
  ///
  /// 下载一张狗的图片并打开
  Future<void> processLoadAndOpen() async {
    if (!loadAndOpenInProgress) {
      await getPermission(PermissionType.notifications);
      var task = DownloadTask(
          url:
              'https://i2.wp.com/www.skiptomylou.org/wp-content/uploads/2019/06/dog-drawing.jpg',
          baseDirectory: BaseDirectory.applicationSupport,
          filename: 'dog.jpg');
      setState(() {
        loadAndOpenInProgress = true;
      });
      await FileDownloader().download(task);
      await FileDownloader().openFile(task: task);
      if (Platform.isIOS) {
        // 将图片添加到照片库并打印路径
        var auth = await FileDownloader()
            .permissions
            .status(PermissionType.iosChangePhotoLibrary);
        if (auth != PermissionStatus.granted) {
          auth = await FileDownloader()
              .permissions
              .request(PermissionType.iosChangePhotoLibrary);
        }
        if (auth == PermissionStatus.granted) {
          final identifier = await FileDownloader()
              .moveToSharedStorage(task, SharedStorage.images);
          if (identifier != null) {
            final path = await FileDownloader()
                .pathInSharedStorage(identifier, SharedStorage.images);
            debugPrint(
                'iOS path to dog picture in Photos Library = ${path ?? "permission denied"}');
          } else {
            debugPrint(
                'Could not add file to Photos Library, likely because permission denied');
          }
        } else {
          debugPrint('iOS Photo Library permission not granted');
        }
      }
      setState(() {
        loadAndOpenInProgress = false;
      });
    }
  }

  Future<void> processLoadABunch() async {
    if (!loadABunchInProgress) {
      setState(() {
        loadABunchInProgress = true;
      });
      await getPermission(PermissionType.notifications);
      for (var i = 0; i < 5; i++) {
        await FileDownloader().enqueue(DownloadTask(
            url:
                'https://storage.googleapis.com/approachcharts/test/5MB-test.ZIP',
            filename: 'File_${Random().nextInt(1000)}',
            group: 'bunch',
            updates: Updates.progress)); // 必须提供进度更新!
        await Future.delayed(const Duration(milliseconds: 500));
      }
      setState(() {
        loadABunchInProgress = false;
      });
    }
  }

  /// 尝试获取权限(如果尚未授予)
  Future<void> getPermission(PermissionType permissionType) async {
    var status = await FileDownloader().permissions.status(permissionType);
    if (status != PermissionStatus.granted) {
      if (await FileDownloader()
          .permissions
          .shouldShowRationale(permissionType)) {
        debugPrint('Showing some rationale');
      }
      status = await FileDownloader().permissions.request(permissionType);
      debugPrint('Permission for $permissionType was $status');
    }
  }
}

enum ButtonState { download, cancel, pause, resume, reset }

更多关于Flutter后台下载管理插件background_downloader_sql的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter后台下载管理插件background_downloader_sql的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是如何在Flutter项目中使用background_downloader_sql插件进行后台下载管理的示例代码。这个插件允许你在后台下载文件,并且使用SQLite数据库来管理下载任务。

1. 添加依赖

首先,你需要在pubspec.yaml文件中添加background_downloader_sql的依赖:

dependencies:
  flutter:
    sdk: flutter
  background_downloader_sql: ^latest_version  # 替换为实际最新版本号

2. 导入插件

在你的Dart文件中导入插件:

import 'package:background_downloader_sql/background_downloader_sql.dart';

3. 初始化插件

在你的应用启动时(例如在MainActivity.ktAppDelegate.swift中,以及Dart代码的main.dart中),初始化插件:

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  
  // 初始化BackgroundDownloaderSQL
  BackgroundDownloaderSQL.initialize().then((_) {
    runApp(MyApp());
  });
}

4. 创建下载任务

创建一个下载任务并启动它:

import 'package:flutter/material.dart';
import 'package:background_downloader_sql/background_downloader_sql.dart';

void startDownload() async {
  String url = "https://example.com/yourfile.zip";
  String saveDir = (await getApplicationDocumentsDirectory()).path;
  String filePath = "$saveDir/yourfile.zip";

  // 创建下载任务
  var task = await BackgroundDownloaderSQL.createDownloadTask(
    taskId: "task_1",
    url: url,
    savePath: filePath,
    headers: null, // 你可以添加HTTP headers
    showNotification: true,
    openFileFromNotification: true,
  );

  // 启动下载任务
  await BackgroundDownloaderSQL.start(taskId: "task_1");
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Background Downloader SQL Example'),
        ),
        body: Center(
          child: ElevatedButton(
            onPressed: startDownload,
            child: Text('Start Download'),
          ),
        ),
      ),
    );
  }
}

5. 监听下载状态

你可以通过监听下载状态来更新UI或执行其他操作:

void listenToDownloadStatus() {
  BackgroundDownloaderSQL.downloadStatusStream.listen((status) {
    if (status.taskId == "task_1") {
      print("Download Status: ${status.status}");
      print("Progress: ${status.progress}%");
      
      // 你可以在这里更新UI
    }
  });
}

确保在合适的时机调用listenToDownloadStatus,例如在MyAppinitState中:

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    listenToDownloadStatus();
  }

  @override
  Widget build(BuildContext context) {
    // ...(与之前相同)
  }
}

6. 处理下载完成和错误

你可以处理下载完成和错误事件:

void handleDownloadEvents() {
  BackgroundDownloaderSQL.downloadCompleteStream.listen((taskId) {
    if (taskId == "task_1") {
      print("Download completed for task: $taskId");
      // 执行下载完成后的操作
    }
  });

  BackgroundDownloaderSQL.downloadErrorStream.listen((error) {
    if (error.taskId == "task_1") {
      print("Download error for task: ${error.taskId}, Error: ${error.errorMessage}");
      // 执行下载错误后的操作
    }
  });
}

同样,在MyAppinitState中调用handleDownloadEvents

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    listenToDownloadStatus();
    handleDownloadEvents();
  }

  @override
  Widget build(BuildContext context) {
    // ...(与之前相同)
  }
}

这样,你就完成了在Flutter中使用background_downloader_sql插件进行后台下载管理的基本设置。这个示例展示了如何创建下载任务、启动任务、监听下载状态、处理下载完成和错误事件。

回到顶部