Flutter: How to get upload / download progress for http requests
Asked Answered
C

4

30

I am writing an app that uploads an image to a server, and instead of just showing a spinner, I'd love to be able to get progress on the status of that upload.

Additionally, I want to do this without using Multipart form data. This is the code I'm currently using - but it appears to be stalling out with a broken pipe, and I have zero feedback as to whether data is being sent to the server:

Future<String> _uploadFile(File assetFile) async {
  final url = <removed>;

  final stream = await assetFile.openRead();
  int length = assetFile.lengthSync();

  final client = new HttpClient();

  final request = await client.postUrl(Uri.parse(url));
request.headers.add(HttpHeaders.CONTENT_TYPE,  "application/octet-stream");
  request.contentLength = length;

  await request.addStream(stream);
  final response = await request.close();
  // response prociessing.
}

Is it possible to send large data as a stream without reading it into memory, and can I get progress on that upload with current dart / flutter APIs?

Configuration answered 21/5, 2018 at 19:5 Comment(1)
Can you post the code in full? I'm getting the following error: The method 'send' isn't defined for the type 'HttpClient'.Phial
M
15

The way that you are already using Stream means that you are not reading the whole file into memory. It's being read in as, probably, 64k chunks.

You could intercept the stream between the producer (File) and consumer (HttpClient) with a StreamTransformer, like this:

  int byteCount = 0;
  Stream<List<int>> stream2 = stream.transform(
    new StreamTransformer.fromHandlers(
      handleData: (data, sink) {
        byteCount += data.length;
        print(byteCount);
        sink.add(data);
      },
      handleError: (error, stack, sink) {},
      handleDone: (sink) {
        sink.close();
      },
    ),
  );
....
  await request.addStream(stream2);

You should see byteCount incrementing in 64k chunks.

Metsky answered 21/5, 2018 at 20:30 Comment(2)
From testing (and based on my understanding of the code) this does not indicate upload progress as the poster requested it indicates file loading progress. I suspect there are better options, I'll post if I work something out.Latoyalatoye
for me, it's not giving the desired progress in Flutter Web.Fillagree
B
29

Screenshot (Null Safe):

enter image description here


This solution

  1. Downloads an image from server.
  2. Shows downloading progress.
  3. After download, the image is saved to device storage.

Code:

import 'package:http/http.dart' as http;

class _MyPageState extends State<MyPage> {
  int _total = 0, _received = 0;
  late http.StreamedResponse _response;
  File? _image;
  final List<int> _bytes = [];

  Future<void> _downloadImage() async {
    _response = await http.Client()
        .send(http.Request('GET', Uri.parse('https://upload.wikimedia.org/wikipedia/commons/f/ff/Pizigani_1367_Chart_10MB.jpg')));
    _total = _response.contentLength ?? 0;

    _response.stream.listen((value) {
      setState(() {
        _bytes.addAll(value);
        _received += value.length;
      });
    }).onDone(() async {
      final file = File('${(await getApplicationDocumentsDirectory()).path}/image.png');
      await file.writeAsBytes(_bytes);
      setState(() {
        _image = file;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton.extended(
        label: Text('${_received ~/ 1024}/${_total ~/ 1024} KB'),
        icon: Icon(Icons.file_download),
        onPressed: _downloadImage,
      ),
      body: Padding(
        padding: const EdgeInsets.all(20.0),
        child: Center(
          child: SizedBox.fromSize(
            size: Size(400, 300),
            child: _image == null ? Placeholder() : Image.file(_image!, fit: BoxFit.fill),
          ),
        ),
      ),
    );
  }
}
Bundelkhand answered 3/4, 2020 at 21:57 Comment(9)
This scenario Will be same if download file .apk from my server? My case , i want download .apk from url and showing progres download.Amphetamine
after try it , it possible handle if connection internet disconnect/unstable ? something like pause/resume download ? i try to disconnect internet and my app crash with this error Unhandled Exception: type '(HttpException) => Null' is not a subtype of type '(dynamic) => dynamic'Amphetamine
@Bundelkhand your answer showing me progress but giving me error when writing file into device, can you look ate the question hereChancelor
This does not seem to be working on the web with http '0.13.4' - calling .send(http.Request('GET', ... seems to download the entire file, and the first and only call to the stream has all of the file data. I opened an issue on github here: github.com/dart-lang/http/issues/631Jacobine
@Bundelkhand the part _response = await http.Client() the keyword await there. Why doesn't it pause the process? The listening part _response.stream.listen is after await, isn't it where the fetching is done? The code works but the code sequence looks like we're listening to the process after it has been already done.Claudine
@alexsmith We need StreamedResponse to listen to the process, for that we will first need to get its value, for that we use _response = await http.Client() and this doesn't take too long because this is not where we are listening. The listening happens once we have StreamedResponse ready.Bundelkhand
@Bundelkhand before the listen part, there's this: _total = _response.contentLength ?? 0; thecontentLength is coming from the response headers. So the request has been initiated and we have the response headers but not yet the body?Claudine
How do you return from listening if an error occurs?Bioscope
@Bioscope You can set onError when listening to the stream.Bundelkhand
M
15

The way that you are already using Stream means that you are not reading the whole file into memory. It's being read in as, probably, 64k chunks.

You could intercept the stream between the producer (File) and consumer (HttpClient) with a StreamTransformer, like this:

  int byteCount = 0;
  Stream<List<int>> stream2 = stream.transform(
    new StreamTransformer.fromHandlers(
      handleData: (data, sink) {
        byteCount += data.length;
        print(byteCount);
        sink.add(data);
      },
      handleError: (error, stack, sink) {},
      handleDone: (sink) {
        sink.close();
      },
    ),
  );
....
  await request.addStream(stream2);

You should see byteCount incrementing in 64k chunks.

Metsky answered 21/5, 2018 at 20:30 Comment(2)
From testing (and based on my understanding of the code) this does not indicate upload progress as the poster requested it indicates file loading progress. I suspect there are better options, I'll post if I work something out.Latoyalatoye
for me, it's not giving the desired progress in Flutter Web.Fillagree
M
14

Try dio library. The onSendProgress callback would be helpful.

example:

response = await dio.post(
  "http://www.example.com",
  data: data,
  onSendProgress: (int sent, int total) {
    print("$sent $total");
  },
);

Reference: https://github.com/flutterchina/dio/issues/103

Mixtec answered 13/4, 2020 at 15:14 Comment(1)
Can we return this as a stream?Deutschland
K
3

Update:

A day after posting this I realized that this is not actually measuring the upload progress, but only the progress of reading the bytes from the local JSON payload, which is almost instantaneous. If/when I figure out how to actually measure the upload progress, I'll update this answer.


Original Answer:

This works for me, without using Multipart:

import 'dart:convert';
import 'package:http/http.dart' as http;

// ...

final jsonPayload = {'base64File': 'abc123', 'something': 'else'};

// We are using a StreamedRequest so we can track the upload progress
final streamedRequest = http.StreamedRequest("POST", apiUri);
streamedRequest.headers['content-type'] = 'application/json';

// Length transferred (to calculate upload progress)
var transferredLength = 0;
// Upload progress (from 0.0 to 1.0)
var uploadProgress = 0.0;
// The stringified JSON payload
var stringEncodedPayload = jsonEncode(jsonPayload);
// Total length (to calculate upload progress)
var totalLength = stringEncodedPayload.length;

// Create a stream of the payload string
Stream.value(stringEncodedPayload)
  // Transform the string-stream to a byte stream (List<int>)
  .transform(utf8.encoder)
  // Start reading the stream in chunks, submitting them to the streamedRequest for upload
  .listen((chunk) {
    transferredLength += chunk.length;
    uploadProgress = transferredLength / totalLength;
    print("Chunk: ${chunk.length}, transferred: $transferredLength, progress: $uploadProgress");
    streamedRequest.sink.add(chunk);
  }, onDone: () {
    print("Done. Total: $totalLength, transferred: $transferredLength, progress: $uploadProgress");
    streamedRequest.sink.close();
  });

final result = await client.send(streamedRequest).then(http.Response.fromStream);


print("----------->");
print(result.statusCode);
print(result.body);
print("<-----------");

The output:

flutter: Chunk: 1024, transferred: 1024, progress: 0.0008807503580198599
flutter: Chunk: 1024, transferred: 2048, progress: 0.0017615007160397197
flutter: Chunk: 1024, transferred: 3072, progress: 0.0026422510740595796
...
flutter: Chunk: 1024, transferred: 1159168, progress: 0.9970094052784814
flutter: Chunk: 1024, transferred: 1160192, progress: 0.9978901556365013
flutter: Chunk: 1024, transferred: 1161216, progress: 0.9987709059945211
flutter: Chunk: 1024, transferred: 1162240, progress: 0.9996516563525409
flutter: Chunk: 405, transferred: 1162645, progress: 1.0
flutter: Done. Total: 1162645, transferred: 1162645, progress: 1.0
Kirkendall answered 31/1, 2022 at 22:22 Comment(1)
I am wondering if you've actually managed to find the correct way to measure the upload progress (not the progress of reading bytes)?Ledford

© 2022 - 2024 — McMap. All rights reserved.