background_downloader Flutter Plugin

repository·main·Indexed 42 days ago

https://github.com/781flyingdutchman/background_downloader

A multi-platform background file transfer plugin for Flutter supporting downloads and uploads on iOS, Android, MacOS, Windows, and Linux. It ensures transfers continue even if the app is suspended or terminated. Features include a modern Transfer API with reactive handles, support for parallel chunked downloads, native notifications, and persistent task tracking via a dual-layer persistence strategy.

Tokens
33.4K
Snippets
79
Records
121
Agent score
53%

What's inside background_downloader

  1. Handle persistent URI permissions

    main

    On Android and iOS, you can request persistent access to a picked file or directory by setting persistedUriPermission: true in pickDirectory(), pickFile(), pickFiles(), or createDirectory().

    Workflow for Persistent URIs

    1. Request Permission: Use the persistedUriPermission: true flag when picking.
    2. Store URI: Save the returned URI in a database for later use.
    3. Activate URI: When you need to use the stored URI in a future session, you must call activate() to get a usable URI for the current session.

    Platform Specifics

    • Android: The URI is registered with the OS, allowing access in future sessions without re-prompting the user.
    • iOS: Persistent URIs use the urlbookmark:// scheme. To access the actual file, you must call await downloader.uri.activate(persistentUri), which returns a usable file:// URI for the current session.
  2. Understand TaskStatus and possible task states

    main

    The TaskStatus enum represents the current state of a task.

    Active/Transient States:

    • enqueued: Waiting on the native platform to start (waiting for resources or network).
    • running: Actively downloading or uploading.
    • waitingToRetry: Task failed and is waiting for exponential backoff before retrying.
    • paused: Task is paused and may be resumable.

    Final States:

    • complete: Task finished successfully.
    • notFound: Task failed because the URL returned a 404.
    • failed: Task failed due to an exception (connection, disk space, or non-404 HTTP errors).
    • canceled: Task was canceled by the user or system.
  3. Handle offline resilience and network constraints

    main

    The background_downloader leverages native operating system schedulers (WorkManager/JobScheduler on Android, background URLSession on iOS) to handle transfers when the device is offline or lacks required connectivity (e.g., when requiresWiFi is true).

    When a transfer is held due to network constraints, you can use the transfer.holdReasonNotifier to reactively update your UI. This notifier emits TransferHoldReason values such as:

    • TransferHoldReason.offline
    • TransferHoldReason.waitingForWiFi

    This allows you to show specific status messages like "Waiting for Wi-Fi" to the user while the task waits for suitable connectivity, even if the Flutter application is in the background or terminated.

  4. Isolate tasks using Scoped namespaces

    main

    If you are building a plugin or a modular feature, use FileDownloader.scoped(namespace) to isolate your tasks and callbacks. This prevents conflicts with the main application or other packages.

    // Isolated downloader instance for a specific module
    final downloader = FileDownloader.scoped('my_audio_player');
    
    final transfer = await downloader.transfers.start(task);
    
    // downloader.transfers.all(), downloader.reset(), and callbacks 
    // only affect the 'my_audio_player' namespace.

    Each call to FileDownloader.scoped('namespace') returns the same cached instance for that namespace. Task groups are prefixed internally (e.g., "my_audio_player.default"), but the API strips these prefixes so your code uses clean names.

    final downloader = FileDownloader.scoped('my_audio_player');
    final transfer = await downloader.transfers.start(task);
  5. How the Transfer API works

    main

    The Transfer object acts as a reactive controller for an individual file transfer. It bridges the gap between background OS processes and your Flutter UI.

    Key Capabilities:

    • Awaitable Futures: You can await transfer.file (the resulting File), transfer.result (the TaskStatusUpdate), or transfer.responseBody (server response text).
    • Reactive UI: It provides several ValueNotifier objects for direct binding to Flutter widgets:
      • transfer.progressNotifier: A value from 0.0 to 1.0.
      • transfer.statusNotifier: The current status of the task.
      • transfer.networkSpeedNotifier: Current download/upload speed.
      • transfer.timeRemainingNotifier: Estimated time left.
      • transfer.notificationTapNotifier: Notifies when the user taps the native notification.
    • Direct Controls: You can call await transfer.pause(), await transfer.resume(), or await transfer.cancel() without needing to manage task IDs manually.
    • Batching: Use FileDownloader().transfers.startAll(tasks, onProgress: ...) to manage multiple transfers with aggregate progress.
  6. Understanding task persistence and resumption

    main

    The plugin uses a dual-layer persistence strategy to ensure tasks can be tracked and resumed:

    1. Dart-side Persistence: Uses localstore (a file-based NoSQL database) to track all tasks, providing access to methods like allTasks and tasksFinished.
    2. Native-side Persistence (Mobile): To survive app process death or device reboots, mobile platforms store minimal necessary data:
      • Android: Uses SharedPreferences and WorkManager Data to persist ResumeData and task status.
      • iOS: Uses UserDefaults and URLSessionTask metadata to persist ResumeData.

    Note for Desktop: Desktop implementation does not have system-level persistence. If the app is closed, the Isolate running the task dies. However, the Dart-side Database still maintains the task record, which can be used to attempt resumption (e.g., via Range headers) when the app restarts.

  7. When to use URIs instead of file paths

    main

    While traditional file paths (baseDirectory/directory/filename) work for many cases, you should use the URI-based methods in background_downloader when:

    • Using Pickers: You are using file or directory pickers to obtain user-selected files or locations (e.g., content:// URIs on Android).
    • Shared Storage: You need to access shared storage locations like Downloads or Documents.
    • Platform Abstraction: You want to write platform-independent code for file operations.
    • Android Direct Downloads: You want to download directly to an external storage destination on Android, bypassing the temporary file used in the traditional approach.
    • Non-file paths: You are working with files that do not have a direct file path representation (e.g., content providers on Android).

    Important Limitation: URI-based downloads (UriDownloadTask) cannot be paused or resumed.

  8. Configure file storage locations

    main

    To ensure file paths remain valid across app restarts (especially on iOS and Android), use the combination of BaseDirectory, directory, and filename.

    • BaseDirectory: Choose from .applicationDocuments, .temporary, .applicationSupport, or .applicationLibrary.
    • directory: An optional subdirectory within the chosen base directory.
    • filename: The specific name of the file. You can use DownloadTask.suggestedFilename or '?' to automatically use the server's Content-Disposition header.
  9. Handle iOS urlbookmark:// and media:// URIs

    main

    iOS uses specialized URI schemes for security and media handling:

    urlbookmark://

    When persistedUriPermission is set to true, iOS returns a urlbookmark:// URI. This URI contains necessary security information for persistent access. While you can convert it to a file:// URI using activate(), it is safer and recommended to pass the urlbookmark:// URI directly to uri methods and tasks.

    media://

    When using the media picker (pickFile with SharedStorage.images or SharedStorage.videos), iOS returns a media:// URI pointing to a copy of the file in the application's cache.

    • You can use this URI directly with uri methods or as the fileUri in a UriUploadTask.
    • Cleanup Required: You are responsible for deleting the temporary file after use by calling FileDownloader().uri.deleteFile.
    • To access the actual file path, use activate() to convert it to a file:// URI.
  10. Optimize transfers with TransferHint

    main

    Use transferHints in your Task constructor to automatically configure priority, pause capability, and network behavior.

    TransferHintEffect
    TransferHint.userInitiatedSets priority: 0 (Android 14+ UIDT & iOS max priority) and enables allowPause: true. Requires a notification.
    TransferHint.largeFileEnables allowPause: true (improves resilience for large files).
    TransferHint.smallFileSets updates: Updates.status to reduce MethodChannel overhead.
    TransferHint.lowPrioritySets priority: 10 (lowest priority) for background syncing.
    TransferHint.useSuggestedFilenameUses server Content-Disposition headers for the filename.
    TransferHint.binaryUploadFor UploadTask, sends raw binary bytes instead of multipart/form-data.

    ⚠️ Android 14+ UIDT Requirement

    If you use TransferHint.userInitiated (which sets priority: 0), you must provide a notification. Android 14+ requires an active, user-visible notification for User-Initiated Data Transfer (UIDT) jobs. Without it, the system may cancel the task.

    Example with Task-specific notification:

    final task = DownloadTask(
      url: 'https://example.com/large_video.mp4',
      filename: 'video.mp4',
      transferHints: {TransferHint.userInitiated, TransferHint.largeFile},
      notificationConfig: const TaskNotificationConfig(
        running: TaskNotification('Downloading', '{filename}'),
        complete: TaskNotification('Complete', '{filename}'),
        progressBar: true,
      ),
    );
  11. Configure custom database storage engines

    main

    By default, FileDownloader uses a modified version of the localstore package. If you require a different storage solution (like SQLite), you must implement the PersistentStorage interface and initialize the downloader before its first use.

    For a ready-to-use SQLite implementation, use the background_downloader_sql package.

    // Example of initializing with a custom storage engine
    // This must be done BEFORE the first use of FileDownloader
    FileDownloader(persistentStorage: MyPersistentStorage());
  12. Handle Android Content URIs

    main

    On Android, files obtained via the Storage Access Framework often use the content:// scheme. These are distinct from standard file paths and cannot be converted to file:// URIs. Use them directly with the library's URI-based methods.

    Important Note for Android Downloads: When using UriDownloadTask on Android, the library downloads directly to the final destination, bypassing the temporary file stage used in standard file path downloads. Consequently, the presence of a file at a destination URI does not guarantee a successful download; it may be a partial file. Always rely on the task status rather than checking for file existence.