Resume Downloads in Flutter with Dio
Tags: flutter, downloads, plugins, dart
Published: 2023-04-26 20:46:09

Add the ability to resume downloads in your Flutter app

## **Summary**
By the end of this post, you will be able to download files, handle incomplete downloads, resume downloads, cancel, get current download status (percentage or size remaining) and merge all chunks into one file.

## **Current issue**
I was facing a problem with my current project. I need to handle large video files, sometimes the download would not complete and every time the user accesses this specific view the download starts again from the beginning.

At the time of writing this post, the [dio plugin](https://pub.dev/packages/dio) does not have the ability to append data to an existing file during the download.

## **So, what can we do?**
We are going to implement a customized procedure to handle our business logic.

## **Imports**
We are going to need to add some dependencies to help us with the implementation of our service.
- [dio: 5.1.1](https://pub.dev/packages/dio)
- [path: 1.8.3](https://pub.dev/packages/path)

```dart
import 'package:dio/dio.dart';
import 'package:path/path.dart' as path;
```

## **Procedure Parameters**
We will receive the **remote url of the file** (_fileUrl_) to download and the **local route of the file** (_fileLocalRouteStr_) in our user’s local storage.

```dart
Future<File?> getItemFileWithProgress({
  required String fileUrl, 
  required String fileLocalRouteStr,
}) async {
  ...
}
```

## **Local Variables**
Let’s first create an instance of the **Dio** class and the necessary variables to handle the file path so we can “play” with the filename.

```dart
Dio dio = new Dio();
File localFile = File(fileLocalRouteStr);
String dir = path.dirname(fileLocalRouteStr);
String basename = path.basenameWithoutExtension(fileLocalRouteStr);
String extension = path.extension(fileLocalRouteStr);

String localRouteToSaveFileStr = fileLocalRouteStr;
```
Now let’s check if the local file exists.
`bool existsSync = localFile.existsSync();`

If the local file **DO NOT EXIST**, then we are in the _best case scenario_ where we are going to start the download from scratch. But if the file **DO EXIST** then we need to do some magic.

## **The Magic ✨**
First let’s get the source file size, then the local file size and add it to a list containing all chunk (file part) sizes.

```dart
if(existsSync) {
  Response response = await dio.head(fileUrl);
  int totalBytes = int.parse(response.headers.value('content-length')!);
  
  int fileLocalSize = localFile.lengthSync();
  List<int> sizes = [fileLocalSize];
```

We will create as many chunks of the file as needed and of course this number will be unknown so we will iterate over time until the (temporary) chunk file does not exists and each iteration will modify the chunk file name and add this chunk size to our list of sizes (we’ll need to know the sum of all sizes eventually).

```
int i = 1;
localRouteToSaveFileStr = '$dir/$basename''_$i$extension';
File _f = File(localRouteToSaveFileStr);
while (_f.existsSync()) {
  sizes.add(_f.lengthSync());
  i++;
  localRouteToSaveFileStr = '$dir/$basename''_$i$extension';
  _f = File(localRouteToSaveFileStr);
}
```

When the code exits the _while loop_, we have the new chunk-file ready to store the remaining bytes of the file. So we’ll need to add up the sizes so far and create the _Options_ for the header in the download.

```
  int sumSizes = sizes.fold(0, (p, c) => p + c);
  Options options = Options(
    headers: {'Range': 'bytes=$sumSizes-'},
  );
}
```
_We’re saying here, on the next download fetch only from this byte (sumSizes) forward to the end of the file (we could also specify the end range of bytes, but it’s not necessary in this case)_

**End of The magic ;)**

## **Merge all chunks of the file**
Wait, we still have to do a few things with all the chunk files. If the _localFile_ exists, then we need to merge all the small pieces of the original file into one and delete the chunks after.

```dart
if (existsSync) {
  var raf = await localFile.open(mode: FileMode.writeOnlyAppend);
  
  int i = 1;
  String filePartLocalRouteStr = '$dir/$basename''_$i$extension';
  File _f = File(filePartLocalRouteStr);
  while (_f.existsSync()) {
    raf = await raf.writeFrom(await _f.readAsBytes());
    await _f.delete();
  
    i++;
    filePartLocalRouteStr = '$dir/$basename''_$i$extension';
    _f = File(filePartLocalRouteStr);
  }
  await raf.close();
}

return localFile;
```
We’ll open the _localFile_ in write mode, but we’ll only adding the new bytes at the end (append), so we won’t overwrite what’s already there. Very similar to what we did before, we’ll iterate until the chunk filename doesn’t exist and then return the **FULL FILE. 🥳**

## **BONUS: Download progress & Cancel download**
Let’s add 2 more variables to the environment

```dart
CancelToken cancelToken = CancelToken();
final percentNotifier = ValueNotifier<double?>(null);
```

The first one will be “**_cancelToken_**” to give us the possibility to cancel the current download, and the “**_percentNotifier_**” will help us to listen only to the percent changes so we don’t have to redraw all the screen, instead of only the desired widget.

Now we’ll need 2 more procedures to handle this new logic.

```dart
_cancel() {
  cancelToken.cancel();
  percentNotifier.value = null;
}

_onReceiveProgress(int received, int total) {
  if (!cancelToken.isCancelled) {
    int sum = sizes.fold(0, (p, c) => p + c);
    received += sum;
    percentNotifier.value = received / total;
  }
}
```
Before executing the download we’ll need to check if the cancel token was already used and if so, refresh the variable with a new value.

```dart
if (cancelToken.isCancelled) {
  cancelToken = CancelToken();
}

await dio.download(
  fileUrl,
  localRouteToSaveFileStr,
  options: options,
  cancelToken: cancelToken,
  deleteOnError: false,
...
```
_“**deleteOnError**” parameter in **false** will allow us to cancel the download and left the incomplete file in the user’s storage_

Now we’ll listen to the **_Dio_** provided callback “**_onReceiveProgress_**” to update our **notifier**.

```dart
await dio.download(
  fileUrl,
  localRouteToSaveFileStr,
  options: options,
  cancelToken: cancelToken,
  deleteOnError: false,
  onReceiveProgress: (int received, int total) {
    _onReceiveProgress(received, fileOriginSize);
  },
);
```

## **Take a look at the example repo in GitHub:**
https://github.com/rlazom/resumeDownload

If this was helpful to you please clap your hands a bit and follow me for more content. 👏👏👏