NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
pub.dev · #829 most downloaded on pub.dev
A multi-platform background file downloader and uploader. Define the task, enqueue and monitor progress
Last release 8 days ago
25 Sep 2026
Release timing varies
gaps range from 8 days to 3 months
Nearly every release is documented
notes for 60 of the last 60 stable releases
Nothing withdrawn
no release was ever pulled
4 years old
138 releases · first in 2022
Fix issue #208 concurrentModificationException on Android and similar in iOS
Fix issue #208 concurrentModificationException on Android and similar in iOS
Permissions must now be explicitly checked and requested to improve user experience and give control to developer
SharedStorage.images or SharedStorage.videoSqlitePersistentStorage backing database moved to separate package background_downloader_sql to reduce app size for defaultBaseDirectory.root to allow absolute file path (use with care!)mimeType and charSet to TaskStatusUpdateRequest.cookieHeader to parse 'Set-Cookie' response headerplatformVersion methodready getter to wait for initialization if neededPermissions are no longer automatically requested. You need to explicitly check, and if necessary ask for permissions ahead of calling methods that use them.
User permissions may be needed to display notifications, to move files to shared storage (on Android) and to add images or video to the iOS Photo Library. These permissions should be checked and if needed requested before executing those operations.
You can use a package like permission_handler, or use the FileDownloader().permissions object, which has three methods:
status: returns a PermissionsStatus. On Android this is either granted or denied. If you have not asked for permission yet, then Android returns denied and iOS returns .undetermined. iOS can also return .partialrequest: to request the actual permission. Only do this if you have confirmed that the permission is not already grantedshouldShowRationale: for Android only, if true you should show a UI element (e.g. a dialog) to explain to the user why this permission is necessaryAll three methods take one PermissionType parameter:
notifications, to display notificationsandroidSharedStorage, to move files to external storage on Android, before API 29iosAddToPhotoLibrary, to move files to SharedStorage.images or SharedStorage.video on iOS, as this adds those files to the Photo LibraryiosChangePhotoLibrary, to access the path to files moved to the Photos LibraryFor example, to request permissions for notifications:
final permissionType = PermissionType.notifications;
var status = await FileDownloader().permissions.status(permissionType);
if (status != PermissionStatus.granted) {
if (await FileDownloader().permissions.shouldShowRationale(permissionType)) {
await showRationaleDialog(permissionType); // Show a dialog with rationale
}
status = await FileDownloader().permissions.request(permissionType);
debugPrint('Permission for $permissionType was $status');
}
The downloader will check permission status before each action, e.g. will not show notifications unless permissions for notifications have been granted.
Note that permissions are very platform and version dependent, e.g. notification permissions on Android are only required as of API 33, and iOS 14 introduced new Photo Library permissions. If you want to get into details, you can determine the platform version you're running by calling await FileDownloader().platformVersion().
Previously, .images and .video destinations were 'faked' on iOS. With this change, when calling moveToSharedStorage, the file is added to the Photos Library (provided the user grants that permission).
For .images and .video SharedStorage destinations, you need user permission to add to the Photos Library, which requires you to set the NSPhotoLibraryAddUsageDescription key in Info.plist. The returned String is not a filePath, but a unique identifier. If you only want to add the file to the Photos Library you can ignore this identifier. If you want to actually get access to the file (and filePath) in the Photos Library, then the user needs to grant an additional 'modify' permission, which requires you to set the NSPhotoLibraryUsageDescription in Info.plist. To get the actual filePath, call pathInSharedStorage and pass the identifier obtained via the call to moveToSharedStorage as the filePath parameter:
// assume we have permission
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');
}
The reason for this two-step approach is that typically you only want to add to the library (requires PermissionType.iosAddToPhotoLibrary), which does not require the user to give read/write access to their entire photos library (PermissionType.iosChangePhotoLibrary, required to get the filePath).
If you use the default PersistentStorage then nothing changes. Otherwise:
SqlitePersistentStorage moved to a separate package, and the migrator used is SqlPersistentStorageMigratorPersistentStorage is now an interface, not a class, and LocalStorePersistentStorage is the default implementationPersistentStorageMigrator is now an interface, and BasePersistentStorageMigrator is a basic implementation that can be extended to add migration options (as is done in SqlPersistentStorageMigrator)Add background_downloader_sql to your dependencies in pubspec.yaml to get SqlitePersistentStorage and SQLite related migration options back.
The reason for this change is that the sqflite dependency adds significant size to apps, even if they do not use the SQLite functionality.
If you download or upload multiple files simultaneously, you may not want a notification for every task, but one notification representing the group of tasks. To do this, set the groupNotificationId field in a notificationConfig and use that configuration for all tasks in this group. It is easiest to combine this with the group field of the task, e.g.:
FileDownloader.configureNotificationForGroup('bunchOfFiles',
running: const TaskNotification(
'{numFinished} out of {numTotal}', 'Progress = {progress}'),
complete:
const TaskNotification('Done!', 'Loaded {numTotal} files'),
error: const TaskNotification(
'Error', '{numFailed}/{numTotal} failed'),
progressBar: true,
groupNotificationId: 'myGroupNotification');
// start every task like this
await FileDownloader().enqueue(DownloadTask(
url: 'https://your_url.com',
filename: 'your_filename',
group: 'bunchOfFiles'));
All tasks in group bunchOfFiles will now use the notification group configuration with ID myNotificationGroup.
BaseDirectory.rootYou can now pass an absolute path to the downloader by using BaseDirectory.root combined with the path in directory. This allows you to reach any file destination on your platform. However, be careful: the reason you should not normally do this (and use e.g. BaseDirectory.applicationDocuments instead) is that the location of the app's documents directory may change between application starts (on iOS, and on Android in some cases), and may therefore fail for downloads that complete while the app is suspended. You should therefore never store permanently, or hard-code, an absolute path, unless you are absolutely sure that that path is 'stable'.
mimeType and charSet to TaskStatusUpdateIf the server provides this information via the Content-Type header then these fields will be non-null only for final states.
Servers may ask you to set a cookie (via the 'Set-Cookie' header in the response), to be passed along to the next request (in the 'Cookie' header). This may be needed for authentication, or for session state.
The method Request.cookieHeader makes it easy to insert cookies in a request. The first argument cookies is either a http.Response object (as returned by the FileDownloader().request method), a List<Cookie>, or a String value from a 'Set-Cookie' header. It returns a {'Cookie': '...'} header that can be added to the next request.
The second argument is the url you intend to use the cookies with. This is needed to filter the appropriate cookies based on domain and path.
For example:
final loginResponse = await FileDownloader()
.request(Request(url: 'https://server.com/login', headers: {'Auth': 'Token'}));
const downloadUrl = 'https://server.com/download';
// add the cookies from the response to the task
final task = DownloadTask(url: downloadUrl, headers: {
'Auth': 'Token',
...Request.cookieHeader(loginResponse, downloadUrl) // inserts the 'Cookie' header
});
Return the platform version as a String:
readyIf initializing a non-default PersistentStorage such as SqlitePersistentStorage you may need to wait for database initialization and perhaps migration to complete before using the downloader. Call await FileDowloader().ready before the first call that involves the persistent storage. Because initialization is often followed immediately by the trackTasks call, that call waits for ready, so this is valid:
await FileDownloader(persistentStorage: SqlitePersistentStorage()).trackTasks();
awaitGroupawaitGroup as the logic for the convenience methods such as download has changedmodifiedTasks in PersistentStorage interfaceupdates field to Updates.status - the default - or Updates.statusAndProgress)updates field to Updates.statusAndProgress)One column per quarter.
Issue #189 related to resume on Android versions prior to S, and to expediting a task prior to S
Issue #189 related to resume on Android versions prior to S, and to expediting a task prior to S
See https://stackoverflow.com/a/68468786/4172761
Fixes issue with parsing priority from JSON Fixes issue with setting expedited for Android versions prior to S. This effectively ignores priority (expedited) scheduling for tasks prior to Android S and defaults to normal.
Minor improvements to TaskQueue and MemoryTaskQueue
Minor improvements to TaskQueue and MemoryTaskQueue
Bug fix for web compilation
Bug fix for web compilation
The Task.priority field must be 0 <= priority <= 10 with 0 being the highest priority, and defaults to 5. On Desktop and iOS all priority levels are s
The Task.priority field must be 0 <= priority <= 10 with 0 being the highest priority, and defaults to 5. On Desktop and iOS all priority levels are supported. On Android, priority levels <5 are handled as 'expedited', and >=5 is handled as a normal task.
Once you enqueue a task with the FileDownloader it is added to an internal queue that is managed by the native platform you're running on (e.g. Android). Once enqueued, you have limited control over the execution order, the number of tasks running in parallel, etc, because all that is managed by the platform. If you want more control over the queue, you need to add a TaskQueue.
The MemoryTaskQueue bundled with the background_downloader allows:
minInterval, to avoid 'choking' the FileDownloader when adding a large number of tasksmaxConcurrentmaxConcurrentByHostTask.group, by setting maxConcurrentByGroupA TaskQueue conceptually sits 'in front of' the FileDownloader queue. To use it, add it to the FileDownloader and instead of enqueuing tasks with the FileDownloader, you now add tasks to the queue:
final tq = MemoryTaskQueue();
tq.maxConcurrent = 5; // no more than 5 tasks active at any one time
tq.maxConcurrentByHost = 2; // no more than two tasks talking to the same host at the same time
tq.maxConcurrentByGroup = 3; // no more than three tasks from the same group active at the same time
FileDownloader().add(tq); // 'connects' the TaskQueue to the FileDownloader
FileDownloader().updates.listen((update) { // listen to updates as per usual
print('Received update for ${update.task.taskId}: $update')
});
for (var n = 0; n < 100; n++) {
task = DownloadTask(url: workingUrl, metData: 'task #$n'); // define task
tq.add(task); // add to queue. The queue makes the FileDownloader().enqueue call
}
Because it is possible that an error occurs when the taskQueue eventually actually enqueues the task with the FileDownloader, you can listen to the enqueueErrors stream for tasks that failed to enqueue.
The default TaskQueue is the MemoryTaskQueue which, as the name suggests, keeps everything in memory. This is fine for most situations, but be aware that the queue may get dropped if the OS aggressively moves the app to the background. Tasks still waiting in the queue will not be enqueued, and will therefore be lost. If you want a TaskQueue with more persistence, subclass the MemoryTaskQueue and add persistence.
In addition, if your app is suspended by the OS due to resource constraints, tasks waiting in the queue will not be enqueued to the native platform and will not run in the background. TaskQueues are therefore best for situations where you expect the queue to be emptied while the app is still in the foreground.
Fix #164 for progress updates for uploads.
Fix #164 for progress updates for uploads.
Add configuration for Android to use external storage instead of internal storage. Either your app runs in default (internal storage) mode, or in exte
Add configuration for Android to use external storage instead of internal storage. Either your app runs in default (internal storage) mode, or in external storage. You cannot switch between internal and external, as the directory structure that - for example - BaseDirectory.applicationDocuments refers to is different in each mode. See the configuration document for important details and limitations
Use (Config.useExternalStorage, String whenToUse) with values 'never' or 'always'. Default is Config.never.
If you want the filename to be provided by the server (instead of assigning a value to filename yourself), you now have two options. The first is to create a DownloadTask that pings the server to determine the suggested filename:
final task = await DownloadTask(url: 'https://google.com')
.withSuggestedFilename(unique: true);
The method withSuggestedFilename returns a copy of the task it is called on, with the filename field modified based on the filename suggested by the server, or the last path segment of the URL, or unchanged if neither is feasible (e.g. due to a lack of connection). If unique is true, the filename will be modified such that it does not conflict with an existing filename by adding a sequence. For example "file.txt" would become "file (1).txt". You can now also supply a taskWithFilenameBuilder to suggest the filename yourself, based on response headers.
The second approach is to set the filename field of the DownloadTask to DownloadTask.suggestedFilename, to indicate that you would like the server to suggest the name. In this case, you will receive the name via the task's status and/or progress updates, so you have to be careful not to use the original task's filename, as that will still be DownloadTask.suggestedFilename. For example:
final task = await DownloadTask(url: 'https://google.com', filename: DownloadTask.suggestedFilename);
final result = await FileDownloader().download(task);
print('Suggested filename=${result.task.filename}'); // note we don't use 'task', but 'result.task'
print('Wrong use filename=${task.filename}'); // this will print '?' as 'task' hasn't changed
To provide progress updates (as a percentage of total file size) the downloader needs to know the size of the file when starting the download. Most servers provide this in the "Content-Length" header of their response. If the server does not provide the file size, yet you know the file size (e.g. because you have stored the file on the server yourself), then you can let the downloader know by providing a {'Range': 'bytes=0-999'} or a {'Known-Content-Length': '1000'} header to the task's header field. Both examples are for a content length of 1000 bytes. The downloader will assume this content length when calculating progress.
Partial Downloads, using a Range header, can now be properly paused on all platforms.
Add displayName field to Task that can be used to store and display a 'human readable' description of the task. It can be displayed in a notification
Add displayName field to Task that can be used to store and display a 'human readable' description of the task. It can be displayed in a notification using {displayName}.
Bug fix for regression in compiling for Web platform (through stubbing - no actual web functionality).
Add ParallelDownloadTask. Some servers may offer an option to download part of the same file from multiple URLs or have multiple parallel downloads of
Add ParallelDownloadTask. Some servers may offer an option to download part of the same file from multiple URLs or have multiple parallel downloads of part of a large file using a single URL. This can speed up the download of large files. To do this, create a ParallelDownloadTask instead of a regular DownloadTask and specify chunks (the number of pieces you want to break the file into, i.e. the number of downloads that will happen in parallel) and urls (as a list of URLs, or just one). For example, if you specify 4 chunks and 2 URLs, then the download will be broken into 8 pieces, four each for each URL.
Note that the implementation of this feature creates a regular DownloadTask for each chunk, with the group name 'chunk' which is now a reserved group. You will not get updates for this group, but you will get normal updates (status and/or progress) for the ParallelDownloadTask.
Enable compile for Web platform (through stubbing - no actual web functionality).
Enable compile for Web platform (through stubbing - no actual web functionality).
Automatically dismiss "complete" and "error" notifications when the user taps on the notification.
Bug fix for validating URLs to allow localhost URLs.
Bug fix for validating URLs to allow localhost URLs.
Update to Android Gradle Plugin 8.1.0
Add configuration Config.useCacheDir for Android and improved temp file logic. By default (Config.whenAble) the downloader will now use the applicatio
Add configuration Config.useCacheDir for Android and improved temp file logic. By default (Config.whenAble) the downloader will now use the application's cacheDir when the size of the file to download is less than half of the cacheQuotaBytes given to the app by Android, and use filesDir otherwise. If you find that downloads do not complete (or cannot be resumed when paused) this indicates the OS is removing the temp file from the cacheDir due to low memory conditions. In that situation, consider using Config.never to force the use of filesDir, but make sure to clean up remnant temp files in filesDir, as the OS does not do that for you.
Fix for Android 33 related to the new predictive back gesture navigation
Documentation updates
Failed download tasks can now be resumed (under certain conditions) even if the Task.allowPause field is false. Resuming a failed task will attempt to
Failed download tasks can now be resumed (under certain conditions) even if the Task.allowPause field is false. Resuming a failed task will attempt to continue the download where the failure occurred. If retries are set to a value >0 then retries will also first attempt to resume, and only start from scratch if that fails.
Tasks can only resume if the ETag header provided by the server is strong, and equal to the ETag at the moment the download was paused/failed, or if it is not provided at all.
Add configuration of the downloader for several aspects:
Add configuration of the downloader for several aspects:
Please read the configuration document for details on how to configure.
Configuration is experimental, so please test thoroughly before using in production, and let me know if there are any issues.
TaskStatusUpdateTaskStatusUpdate now has fields networkSpeed (in MB/s) and timeRemaining. Check the associated hasNetworkSpeed and hasTimeRemaining before using the values in these fields. Use networkSpeedAsString and timeRemainingAsString for human readable versions of these values.
TaskRecord entries by status: allRecordsWithStatusThe database now has method allRecordsWithStatus to filter records based on their TaskStatus
Bug fix for taskNotificationTapCallback: convenience methods that await a result, such as download (but not enqueue), now use the default taskNotifica
Bug fix for taskNotificationTapCallback: convenience methods that await a result, such as download (but not enqueue), now use the default taskNotificationTapCallback, even though those tasks are in the awaitGroup, because that behavior is more in line with expectations. If you need a separate callback for the awaitGroup, then set it after setting the default callback. You set the default callback by omitting the group parameter in the registerCallbacks call.
Added field responseBody to TaskStatusUpdate that, if not null, contains the server response for uploads, and for downloads that are not complete (e.g
Added field responseBody to TaskStatusUpdate that, if not null, contains the server response for uploads, and for downloads that are not complete (e.g. .notFound). In those instances, the server response may contain useful information (e.g. a url where the uploaded file can be found, or the reason for the 'not found' status as provided by the server)
Improved handling of notification tap callbacks.
Bug fix for Flutter Downloader migration on iOS, issue #86
Bug fix for Flutter Downloader migration on iOS, issue #86
If you need to upload multiple files in a single request, create a MultiUploadTask instead of an UploadTask. It has similar parameters as the UploadTa
If you need to upload multiple files in a single request, create a MultiUploadTask instead of an UploadTask. It has similar parameters as the UploadTask, except you specify a list of files to upload as the files argument of the constructor, and do not use fileName, fileField and mimeType. Each element in the files list is either:
"file1.txt"). The fileField for that file will be set to the base name (i.e. "file1" for "file1.txt") and the mime type will be derived from the extension (i.e. "text/plain" for "file1.txt")(fileField, filename), e.g. ("document", "file1.txt"). The fileField for that file will be set to "document" and the mime type derived from the file extension (i.e. "text/plain" for "file1.txt")(filefield, filename, mimeType), e.g. ("document", "file1.txt", "text/plain")The baseDirectory and directory fields of the MultiUploadTask determine the expected location of the file referenced, unless the filename used in any of the 3 formats above is an absolute path (e.g. "/data/user/0/com.my_app/file1.txt"). In that case, the absolute path is used and the baseDirectory and directory fields are ignored for that element of the list.
Once the MultiUpoadTask is created, the fields fileFields, filenames and mimeTypes will contain the parsed items, and the fields fileField, filename and mimeType contain those lists encoded as a JSON string.
Use the MultiTaskUpload object in the upload and enqueue methods as you would a regular UploadTask.
Bug fixes related to migration from Flutter Downloader (see version 7.6.0). The migration is still experimental, so please test thoroughly before relying on the migration in your app.
Fixed a bug on iOS related to NSNull Json decoding
Added SqlitePersistentStorage as an alternative backing storage for the downloader, and implemented migration of a pre-existing database from the Flut
Added SqlitePersistentStorage as an alternative backing storage for the downloader, and implemented migration of a pre-existing database from the Flutter Downloader package. We use the sqflite package, so this is only supported iOS and Android.
To use the downloader with SQLite backing and migration from Flutter Downloader, initialize the FileDownloader at the very beginning of your app:
final sqlStorage = SqlitePersistentStorage(migrationOptions: ['flutter_downloader', 'local_store']);
FileDownloader(persistentStorage: sqlStorage);
// start using the FileDownloader
This will migrate from either Flutter Downloader or the default LocalStore.
Added an optional parameter to the tasksFinished method that allows you to use it the moment you receive a status update for a task, like this:
void downloadStatusCallback(TaskStatusUpdate update) async {
// process your status update, then check if all tasks are finished
final bool allTasksFinished = update.status.isFinalState &&
await FileDownloader().tasksFinished(ignoreTaskId: update.task.taskId) ;
print('All tasks finished: $allTasksFinished');
}
This excludes the task that is currently finishing up from the test. Without this, it's possible tasksFinished returns false as that currently finishing task may not have left the queue yet.
Added pathInSharedStorage method, which obtains the path to a file moved to shared storage.
Added pathInSharedStorage method, which obtains the path to a file moved to shared storage.
To check if a file exists in shared storage, obtain the path to the file by calling
pathInSharedStorage and, if not null, check if that file exists.
On Android 29+: If you have generated a version with an indexed name (e.g. 'myFile (1).txt'), then only the most recently stored version is available this way, even if an earlier version actually does exist. Also, only files stored by your app will be returned via this call, as you don't have access to files stored by other apps.
On iOS: To make files visible in the Files browser, do not move them to shared storage. Instead, download the file to the BaseDirectory.applicationDocuments and add the following to your Info.plist:
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>UIFileSharingEnabled</key>
<true/>
This will make all files in your app's Documents directory visible to the Files browser.
Bug fixes:
moveToSharedStorage on iOS: shared storage is now 'faked' on iOS, creating
subdirectories of the regular Documents directory, as iOS apps do not have access to shared
media and download directoriesBug fix for type cast errors and for thread safety on iOS for notifications
Bug fix for type cast errors and for thread safety on iOS for notifications
Added method expectedFileSize() to DownloadTask, and added field expectedFileSize to TaskProgressUpdate (provided to callbacks or listeners during dow
Added method expectedFileSize() to DownloadTask, and added field expectedFileSize to
TaskProgressUpdate (provided to callbacks or listeners during download), and TaskRecord
entries in the database.
Note that this field is only valid when 0 < progress < 1. It is -1 if file size cannot be determined.
Improved DownloadProgressIndicator widget:
Improved DownloadProgressIndicator widget:
maxExpandable to 0. When set to 1, the indicator collapses only when the second download starts. When set greater than 1, the indicator expands to show multiple simultaneous downloads.Added usage examples upfront in the readme
Added DownloadProgressIndicator widget and modified the example app to show how to wire it up.
Added DownloadProgressIndicator widget and modified the example app to show how to wire it up.
The widget is configurable (e.g. pause and cancel buttons) and can show multiple downloads simultaneously in either an expanded or collapsed mode.
If tracking downloads in persistent storage, pausing a file now does not override the stored progress with progressPaused.
Fixed bugs.
Added option to use a different persistent storage solution than the one provided by default. The downloader stores a few things in persistent storage
Added option to use a different persistent storage solution than the one provided by default. The downloader stores a few things in persistent storage, and uses a modified version of the localstore package by default. To use a different persistent storage solution, create a class that implements the PersistentStorage interface, and initialize the downloader by calling FileDownloader(persistentStorage: yourStorageClass()) as the first use of the FileDownloader.
A simple example is included in the example app (using the sqflite package).
Fixed a few bugs.
Added tasksFinished method that returns true if all tasks in the group have finished
Added tasksFinished method that returns true if all tasks in the group have finished
Fixed bug related to allTasks method
Added namespace to Android build.gradle and removed irrelevant log messages
Added namespace to Android build.gradle and removed irrelevant log messages
Fixed permission bug on Android 10
Changed class modifiers to allow mocking with Mockito
Migrating the persistent data from the documents directory to the support directory, so it is no longer visible in - for example - the iOS Files app,
Migrating the persistent data from the documents directory to the support directory, so it is no longer visible in - for example - the iOS Files app, or the Linux home directory.
Further Dart 3 changes (not visible to user).
Migration to Dart 3 - not other functional change or API change. If you use Dart 2 please use version 6.1.1 of this plugin, which will be maintained u
Migration to Dart 3 - not other functional change or API change. If you use Dart 2 please use version 6.1.1 of this plugin, which will be maintained until the end of 2023.
Most classes in the package are now final classes, and under the hood we use the new Records and Pattern matching features of Dart 3. None of this should matter if you've used the package as intended.
Nothing published for this version
Fixed a bug on iOS related to NSNull Json decoding
Fixed a bug on iOS related to NSNull Json decoding
Added an optional parameter to the tasksFinished method that allows you to use it the moment you receive a status update for a task, like this: `dart
Added an optional parameter to the tasksFinished method that allows you to use it the moment you receive a status update for a task, like this:
void downloadStatusCallback(TaskStatusUpdate update) async {
// process your status update, then check if all tasks are finished
final bool allTasksFinished = update.status.isFinalState &&
await FileDownloader().tasksFinished(ignoreTaskId: update.task.taskId) ;
print('All tasks finished: $allTasksFinished');
}
This excludes the task that is currently finishing up from the test. Without this, it's possible tasksFinished returns false as that currently finishing task may not have left the queue yet.
Added pathInSharedStorage method, which obtains the path to a file moved to shared storage.
Added pathInSharedStorage method, which obtains the path to a file moved to shared storage.
To check if a file exists in shared storage, obtain the path to the file by calling
pathInSharedStorage and, if not null, check if that file exists.
On Android 29+: If you have generated a version with an indexed name (e.g. 'myFile (1).txt'), then only the most recently stored version is available this way, even if an earlier version actually does exist. Also, only files stored by your app will be returned via this call, as you don't have access to files stored by other apps.
On iOS: To make files visible in the Files browser, do not move them to shared storage. Instead, download the file to the BaseDirectory.applicationDocuments and add the following to your Info.plist:
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>UIFileSharingEnabled</key>
<true/>
This will make all files in your app's Documents directory visible to the Files browser.
Bug fixes:
moveToSharedStorage on iOS: shared storage is now 'faked' on iOS, creating
subdirectories of the regular Documents directory, as iOS apps do not have access to shared
media and download directoriesBug fix for type cast errors and for thread safety on iOS for notifications
Bug fix for type cast errors and for thread safety on iOS for notifications
Added tasksFinished method that returns true if all tasks in the group have finished
Added tasksFinished method that returns true if all tasks in the group have finished
Fixed bug related to allTasks method
Fixed permission bug on Android 10
Fixed permission bug on Android 10
Added namespace to Android build.gradle and removed irrelevant log messages
Added namespace to Android build.gradle and removed irrelevant log messages
Migrating the persistent data from the documents directory to the support directory, so it is no longer visible in - for example - the iOS Files app,
Migrating the persistent data from the documents directory to the support directory, so it is no longer visible in - for example - the iOS Files app, or the Linux home directory.
Bug fix for request method where the httpRequestMethod override was not taken into account properly.
Bug fix for request method where the httpRequestMethod override was not taken into account properly.
Added unregisterCallBacks to remove callbacks if you no longer want updates, and resetUpdates to reset the updates stream so it can be listened to aga
Added unregisterCallBacks to remove callbacks if you no longer want updates, and resetUpdates to reset the updates stream so it can be listened to again.
Bug fix for DownloadTask.withSuggestedFilename for servers that do not follow case convention for the Content-Disposition header.
The TaskStatusCallback and TaskProgressCallback now take a single argument (TaskStatusUpdate and TaskProgressUpdate respectively) instead of multiple
Breaking changes:
TaskStatusCallback and TaskProgressCallback now take a single argument (TaskStatusUpdate and TaskProgressUpdate respectively) instead of multiple arguments. This aligns the callback API with the updates listener API, and makes it easier to add data to an update in the future. For example, in this version we add an exception property to programmatically handle exceptionsdownload and upload methods now return a TaskStatusUpdate instead of a TaskStatustaskStatus property of the TaskRecord (used to store task information in a persistent database) is renamed to statustrackTasks method no longer takes a group argument, and starts tracking for all tasks, regardless of group. If you need tracking only for a specific group, call the new trackTasksInGroup methodOther changes (non-breaking):
httpRequestMethod used for requests by setting it in the Request, DownloadTask or UploadTask. By default, requests and downloads use GET (unless post is set) and uploads use POSTdownload, upload, downloadBatch and uploadBatch methods now take an optional onElapsedTime callback that is called at regular intervals (defined by the optional elapsedTimeInterval which defaults to 5 seconds) with the time elapsed since the call was made. This can be used to trigger UI warnings (e.g. 'this is taking rather long') or to cancel the task if it does not complete within a desired time. For performance reasons the elapsedTimeInterval should not be set to a value less than one second, and this mechanism should not be used to indicate progress.TaskStatusUpdate will contain a TaskException that provides information about the type of exception (e.g. a TaskFileSystemException indicates an issue with storing or retrieving the file) and contains a description and (for TaskHttpException only) the httpResponseCode. If tasks are tracked, the The following TaskException subtypes may occur:
TaskException (general exception)TaskFileSystemException (issue retrieving or storing the file)TaskUrlException (issue with the url)TaskConnectionException (issue with the connection to the server)TaskResumeException (issue with pausing or resuming a task)TaskHttpException (issue with the HTTP connection, e.g. we received an error response from the server, captured in httpResponseCode)Fixed a few bugs.
Adds handler for when the user taps a notification, and an openFile method to open a file using the platform-specific convention.
Adds handler for when the user taps a notification, and an openFile method to open a file using the platform-specific convention.
To handle notification taps, register a callback that takes Task and NotificationType as parameters:
FileDownloader().registerCallbacks(
taskNotificationTapCallback: myNotificationTapCallback);
void myNotificationTapCallback(Task task, NotificationType notificationType) {
print('Tapped notification $notificationType for taskId ${task.taskId}');
}
To open a file, call FileDownloader().openFile and supply either a Task or a full filePath (but not both) and optionally a mimeType to assist the Platform in choosing the right application to use to open the file.
The file opening behavior is platform dependent, and while you should check the return value of the call to openFile, error checking is not fully consistent.
Note that on Android, files stored in the BaseDirectory.applicationDocuments cannot be opened. You need to download to a different base directory (e.g. .applicationSupport) or move the file to shared storage before attempting to open it.
If all you want to do on notification tap is to open the file, you can simplify the process by
adding tapOpensFile: true to your call to configureNotifications, and you don't need to
register a taskNotificationTapCallback.
Adds withSuggestedFilename for DownloadTask. Use: ` final task = await DownloadTask(url: 'https://google.com') .withSuggestedFilename(unique: true); `
Adds withSuggestedFilename for DownloadTask. Use:
final task = await DownloadTask(url: 'https://google.com')
.withSuggestedFilename(unique: true);
The method withSuggestedFilename returns a copy of the task it is called on, with the filename field modified based on the filename suggested by the server, or the last path segment of the URL, or unchanged if neither is feasible. If unique is true, the filename will be modified such that it does not conflict with an existing filename by adding a sequence. For example "file.txt" would become "file (1).txt".
Bug fixes:
Fix issue #34 with moveToSharedStorage on iOS
Fix issue #34 with moveToSharedStorage on iOS
An invalid url in the Task now results in false being returned from the enqueue call on all platforms. Previously, the behavior was inconsistent.
An invalid url in the Task now results in false being returned from the enqueue call on
all platforms. Previously, the behavior was inconsistent.
Added optional properties to UploadTask related to multi-part uploads:
Added optional properties to UploadTask related to multi-part uploads:
fileField is the field name used to indicate the file (default to "file")mimeType overrides the mimeType derived from the filename extensionfields is a Map<String, String> containing form field name/value pairs that will be uploaded along with the file in a multi-part uploadAdded optional mimeType parameter for calls to moveToSharedStorage and moveFileToSharedStorage. This sets the mimeType directly, instead of relying on
Added optional mimeType parameter for calls to moveToSharedStorage and
moveFileToSharedStorage. This sets the mimeType
directly, instead of relying on the system to determine the mime type based on the file extension.
Note that this may change the filename - for example, when moving the test file google.html to
SharedStorage.images while setting mimeType to 'images/jpeg', the path to the file in shared
storage becomes /storage/emulated/0/Pictures/google.html.jpg (note the added .jpg).
Better permissions management, implementation of moveToSharedStorage for Android versions below Q
Better permissions management, implementation of moveToSharedStorage for Android versions below Q
Minor fixes
Minor fixes
The download directories specified in the BaseDirectory enum are all local to the app. To make downloaded files available to the user outside of the a
The download directories specified in the BaseDirectory enum are all local to the app. To make downloaded files available to the user outside of the app, or to other apps, they need to be moved to shared or scoped storage, and this is platform dependent behavior. For example, to move the downloaded file associated with a DownloadTask to a shared 'Downloads' storage destination, execute the following after the download has completed:
final newFilepath = await FileDownloader().moveToSharedStorage(task, SharedStorage.downloads);
if (newFilePath == null) {
... // handle error
} else {
... // do something with the newFilePath
}
Because the behavior is very platform-specific, not all SharedStorage destinations have the same result. The options are:
.downloads - implemented on all platforms, but on iOS files in this directory are not accessible to other users.images - implemented on Android and iOS only. On iOS files in this directory are not accessible to other users.video - implemented on Android and iOS only. On iOS files in this directory are not accessible to other users.audio - implemented on Android and iOS only. On iOS files in this directory are not accessible to other users.files - implemented on Android only.external - implemented on Android onlyOn MacOS, for the .downloads to work you need to enable App Sandbox entitlements and set the key com.apple.security.files.downloads.read-write to true.
On Android, depending on what SharedStorage destination you move a file to, and depending on the OS version your app runs on, you may require extra permissions WRITE_EXTERNAL_STORAGE and/or READ_EXTERNAL_STORAGE . See here for details on the new scoped storage rules starting with Android API version 30, which is what the plugin is using.
Methods moveToSharedStorage and the similar moveFileToSharedStorage also take an optional directory argument for a subdirectory in the SharedStorage destination.
Thanks to @rebaz94 for implementing scoped storage on Android.
The BaseDirectory enum now also supports .applicationLibrary. On iOS and MacOS this is the directory provided by the path_provider package's getLibraryDirectory() call. On Other platforms, for consistency, this is the subdirectory 'Library' of the directory returned byn the getApplicationSupportDirectory() call.
Fixed a bug with iOS cancellation in non-US locales.
On iOS and Android, for downloads only, the downloader can generate notifications to keep the user informed of progress also when the app is in the ba
On iOS and Android, for downloads only, the downloader can generate notifications to keep the user informed of progress also when the app is in the background, and allow pause/resume and cancellation of an ongoing download from those notifications.
Configure notifications by calling FileDownloader().configureNotification and supply a TaskNotification object for different states. For example, the following configures notifications to show only when actively running (i.e. download in progress), disappearing when the download completes or ends with an error. It will also show a progress bar and a 'cancel' button, and will substitute {filename} with the actual filename of the file being downloaded.
FileDownloader().configureNotification(
running: TaskNotification('Downloading', 'file: {filename}'),
progressBar: true)
To also show a notifications for other states, add a TaskNotification for complete, error and/or paused. If paused is configured and the task can be paused, a 'Pause' button will show for the running notification, next to the 'Cancel' button.
There are three possible substitutions of the text in the title or body of a TaskNotification:
TaskTask.metaData fieldNotifications on iOS follow Apple's guidelines, notably:
running notification is shown and it is not updated until the download state changesWhile notifications are possible on desktop platforms, there is no true background mode, and progress updates and indicators can be shown within the app. Notifications are therefore ignored on desktop platforms.
The configureNotification call configures notification behavior for all download tasks. You can specify a separate configuration for a group of tasks by calling configureNotificationForGroup and for a single task by calling configureNotificationForTask. A Task configuration overrides a group configuration, which overrides the default configuration.
When attempting to show its first notification, the downloader will ask the user for permission to show notifications (platform version dependent) and abide by the user choice. For Android, starting with API 33, you need to add <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> to your app's AndroidManifest.xml. Also on Android you can localize the button text by overriding string resources bg_downloader_cancel, bg_downloader_pause, bg_downloader_resume and descriptions bg_downloader_notification_channel_name, bg_downloader_notification_channel_description. Localization on iOS is not currently supported.
Better persistence for tasks that execute while the app is suspended by the operating system. To ensure your callbacks or listener capture events that
Better persistence for tasks that execute while the app is suspended by the operating system.
To ensure your callbacks or listener capture events that may have happened when your app was
suspended in the background, call FileDownloader().resumeFromBackground() right after registering
your callbacks or listener.
Previously, Android file downloads were limited to 8 minutes. Now, long downloads are possible provided the DownloadTask.allowPause field is set to tr
Previously, Android file downloads were limited to 8 minutes. Now, long downloads are possible provided the DownloadTask.allowPause field is set to true. Just before the download times out, the downloader will pause and then resume the task in a new worker, effectively resetting the 9 minute clock. As a result, the download will eventually complete
To pause or resume a task, call:
To pause or resume a task, call:
pause to attempt to pause a task. Whether a task can be canceled or not depends primarily on the server. Soon after the task is running (TaskStatus.running) you can call taskCanResume which will return a Future that resolves to true if the server appears capable of pause & resume. If that returns false, then calling pause will return false as well, and the call is ignoredresume to resume a previously paused task, which returns true if resume appears feasible. The taskStatus will follow the same sequence as a newly enqueued task. If resuming turns out to be not feasible (e.g. the operating system deleted the temp file with the partial download) then the task will either restart as a normal download, or fail.This adds TaskStatus.paused which may require updating switch statements to remain exhaustive, though this status will never appear unless you use pause.
Adds status and progress callbacks for individual files in a batch. This is breaking if you used a batch progress callback earlier, as that is now a named parameter. Change:
final result = await FileDownloader().downloadBatch(tasks, (succeeded, failed) {
print('$succeeded files succeeded, $failed have failed');
print('Progress is ${(succeeded + failed) / tasks.length} %');
});
to
final result = await FileDownloader().downloadBatch(tasks, batchProgressCallback: (succeeded, failed) {
...
});
To also monitor status and progress for each file in the batch, add a taskStatusCallback (taking Task and TaskStatus as arguments) and/or a taskProgressCallback (taking Task` and a double as arguments).
To improve Swift code readability and maintenance, the minimum iOS version has moved from 11.0 to 13.0
Fixed another bug with database.allRecords if taskId contains illegal filename characters (like '/'). For tracking record id purposes those are now re
Fixed another bug with database.allRecords if taskId contains illegal filename characters (like '/'). For
tracking record id purposes those are now replaced with '_'
Fixed bug with database.allRecords if taskId contains illegal filename characters (like '/'). For tracking record id purposes those are now replaced w
Fixed bug with database.allRecords if taskId contains illegal filename characters (like '/'). For
tracking record id purposes those are now replaced with '_'
Upgraded dependency to address issue with Windows platform database performance
Upgraded dependency to address issue with Windows platform database performance
Added creationTime field to Request and Task.
Added creationTime field to Request and Task.
Added allRecordsOlderThan(Duration age, {String? group}) to database, making it easy to extract
the TaskRecord entries that are stale.
Adds optional tracking of task status and progress in a persistent database.
Adds optional tracking of task status and progress in a persistent database.
To keep track of the status and progress of all tasks, even after they have completed, activate tracking by calling trackTasks() and use the database field to query. For example:
// at app startup, start tracking
await FileDownloader().trackTasks();
// somewhere else: enqueue a download
final task = DownloadTask(
url: 'https://google.com',
filename: 'testfile.txt');
final successfullyEnqueued = await FileDownloader().enqueue(task);
// somewhere else: query the task status by getting a `TaskRecord`
// from the database
final record = await FileDownloader().database.recordForId(task.taskId);
print('Taskid ${record.taskId} with task ${record.task} has '
'status ${record.taskStatus} and progress ${record.progress}'
You can interact with the database using allRecords, recordForId, deleteAllRecords, deleteRecordWithId etc. Note that only tasks that you asked to be tracked (using trackTasks, which activates tracking for all tasks in a group) will be in the database. All active tasks in the queue, regardless of tracking, can be queried via the FileDownloader.taskForId call etc, but those will only return the task itself, not its status or progress, as those are expected to be monitored via listener or callback. Note: tasks that are started using download, upload, batchDownload or batchUpload are assigned a special group name 'await', as callbacks for these tasks are handled within the FileDownloader. If you want to track those tasks in the database, call FileDownloader().trackTasks(FileDownloader.awaitGroup) at the start of your app.
Your coding agent can read these notes before it upgrades. Set up the MCP server →