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