Resume Downloads in Flutter with Dio

rlazom

Rene Lazo Mendez

Posted on April 26, 2023

Resume Downloads in Flutter with Dio

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 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.

import 'package:dio/dio.dart';
import 'package:path/path.dart' as path;
Enter fullscreen mode Exit fullscreen mode

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.

Future<File?> getItemFileWithProgress({
  required String fileUrl, 
  required String fileLocalRouteStr,
}) async {
  ...
}
Enter fullscreen mode Exit fullscreen mode

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.

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;
Enter fullscreen mode Exit fullscreen mode

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.

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];
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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-'},
  );
}
Enter fullscreen mode Exit fullscreen mode

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.

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;
Enter fullscreen mode Exit fullscreen mode

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

CancelToken cancelToken = CancelToken();
final percentNotifier = ValueNotifier<double?>(null);
Enter fullscreen mode Exit fullscreen mode

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.

_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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

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

await dio.download(
  fileUrl,
  localRouteToSaveFileStr,
  options: options,
  cancelToken: cancelToken,
  deleteOnError: false,
...
Enter fullscreen mode Exit fullscreen mode

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.

await dio.download(
  fileUrl,
  localRouteToSaveFileStr,
  options: options,
  cancelToken: cancelToken,
  deleteOnError: false,
  onReceiveProgress: (int received, int total) {
    _onReceiveProgress(received, fileOriginSize);
  },
);
Enter fullscreen mode Exit fullscreen mode

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. 👏👏👏

💖 💪 🙅 🚩
rlazom
Rene Lazo Mendez

Posted on April 26, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related