Link

Download Progress

In this post we will build a simple file downloader that shows a visual indication of progress.


Creating the UI

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  var downloading = false;
  var done = false;
  double progress;

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(
      backgroundColor: Colors.transparent,
      elevation: 0,
      centerTitle: true,
      title: Text('Downloader'),
      actions: [
        IconButton(
          icon: Icon(Icons.settings),
          onPressed: () {},
        )
      ],
    ),
    body: SizedBox.expand(child: Align(child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      crossAxisAlignment: CrossAxisAlignment.center,
      children: [
        if (downloading)
          SizedBox(
            width: 120,
            child: LinearProgressIndicator(
              value: progress,
            ),
          )
        else if (done) ...[
          Image.network('https://i.tst.sh/XaY4i.jpg'),
          Padding(
            padding: EdgeInsets.all(8),
            child: RaisedButton(
              child: Text('Reset'),
              onPressed: () {
                setState(() {
                  done = false;
                });
              },
            ),
          )
        ] else
          RaisedButton(
            child: Text('Start'),
            onPressed: () async {
              setState(() {
                downloading = true;
                progress = null;
              });

              for (int i = 0; i <= 100; i++) {
                setState(() {
                  progress = i / 100;
                });
                await Future.delayed(Duration(milliseconds: 25));
              }

              setState(() {
                downloading = false;
                done = true;
              });
            },
          ),
      ],
    ))),
  );
}

This is just a wireframe and does not actually download data.


Download with progress

Using package:http we can do a streaming download rather than loading it all into memory, this is much more efficient regardless since the data is just going into a file.

Here is a helper class which provides progress updates to a StreamedResponse:

class DownloadTask {
  /// The length of the download, or null if indeterminate.
  final int length;
  
  /// The current progress of the download, in bytes.
  final ValueListenable<int> progress;
  
  /// The resulting stream of data.
  final Stream<List<int>> stream;

  DownloadTask._({
    this.length,
    this.progress,
    this.stream,
  });

  factory DownloadTask(http.StreamedResponse response) {
    var progress = ValueNotifier(0);
    return DownloadTask._(
      length: response.contentLength,
      progress: progress,
      stream: response.stream.map((event) {
        progress.value += event.length;
        return event;
      }),
    );
  }

  Future<void> save(File file) async {
    var f = file.openWrite();
    await f.addStream(stream);
    await f.close();
  }
}

Hooking up the UI

First, set up new fields in the page state:

  /// File path to where the download saves.
  String filePath;

  /// An in-progress download task.
  DownloadTask download;
  
  /// Whether or not the download has finished.
  var done = false;

Then update the progress bar so that it listens to the progress:

        if (download != null)
          SizedBox(
            width: 120,
            child: ValueListenableBuilder(
              valueListenable: download.progress,
              builder: (context, bytes, child) =>
                LinearProgressIndicator(
                  value: download.length == null
                    ? null : bytes / download.length,
                ),
            ),
          )

Make our image actually read from the file:

        else if (done) ...[
          Image.file(File(filePath)),

Finally, implement the download button:

          RaisedButton(
            child: Text('Start'),
            onPressed: () async {
              // Ignore button press if a download is already in progress.
              if (download != null) return;

              // The http client must be disposed after use, we use a
              // try/finally to make sure it gets disposed properly.
              http.Client client;
              try {
                client = http.Client();

                // Start the download task using client.send.
                download = await DownloadTask(await client.send(http.Request(
                  'GET', Uri.parse('https://i.tst.sh/XaY4i.jpg')
                )));

                // Safely notify the UI that we have a download in progress.
                if (mounted) setState(() {});
                
                // Compute the file path with package:path and package:path_provider.
                filePath = path.join(
                  (await getApplicationDocumentsDirectory()).path,
                  'birb.jpg',
                );

                // Pipe the download into a file at filePath.
                await download.save(File(filePath));

                // Safely notify the UI that the download is complete.
                if (mounted) setState(() {
                  download = null;
                  done = true;
                });
              } catch (e, bt) {
                print('Error: $e\n$bt');

                // An error has occurred, cancel the download.
                if (mounted) setState(() {
                  download = null;
                  done = false;
                });
              } finally {
                client?.close();
              }
            },
          ),

Final result

This video shows the app downloading and displaying a real image:

Source code: https://gist.github.com/PixelToast/a9d539511726fb445d272a13f1f2729d