Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for download function in web #2230

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions dio/lib/src/options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ enum ResponseType {

/// Get the original bytes, the [Response.data] will be [List<int>].
bytes,

/// Get blob url.
/// This value is needed when you need content in the form of a blob url.
/// Only work in web.
///
/// the [Response.data] will be [String].
blobUrl,
}

/// {@template dio.options.ListFormat}
Expand Down
14 changes: 14 additions & 0 deletions example_flutter_app/lib/download_web/download_blob.dart
mbfakourii marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'dart:js_interop';
import 'package:web/web.dart';

void downloadBlob(String blobUrl, String name) {
final Document htmlDocument = document;
final HTMLAnchorElement anchor =
htmlDocument.createElement('a') as HTMLAnchorElement;
anchor.href = blobUrl;
anchor.style.display = name;
anchor.download = name;
document.body!.add(anchor);
anchor.click();
anchor.remove();
}
103 changes: 103 additions & 0 deletions example_flutter_app/lib/download_web/main_web_download.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';

import 'download_blob.dart';

late Dio dio;

void main() {
dio = Dio(
BaseOptions(
connectTimeout: const Duration(hours: 3),
),
);

dio.interceptors.add(LogInterceptor());
runApp(MyApp());
}

class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter web download blob Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter web download blob Demo'),
);
}
}

class MyHomePage extends StatefulWidget {
MyHomePage({
super.key,
this.title = '',
});

final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
var url =
'https://jsoncompare.org/LearningContainer/SampleFiles/Video/MP4/sample-mp4-file.mp4';

CancelToken cancelToken = CancelToken();

@override
void initState() {
super.initState();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
spacing: 20,
children: [
ElevatedButton(
child: const Text('Request'),
onPressed: () async {
if (cancelToken.isCancelled) {
cancelToken = CancelToken();
}

try {
// Fetch blob
final Response res = await dio.download(
url,
'',
onReceiveProgress: (count, total) {
print((count / total) * 100);
},
cancelToken: cancelToken,
);

// Download blob
downloadBlob(res.data, url.split('/').last);
print('fin');
} catch (e) {
print('error');
}
},
),
ElevatedButton(
child: const Text('Cancel'),
onPressed: () async {
cancelToken.cancel();
},
),
],
),
),
);
}
}
41 changes: 28 additions & 13 deletions plugins/web_adapter/lib/src/adapter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ class BrowserHttpClientAdapter implements HttpClientAdapter {
xhrs.add(xhr);
xhr
..open(options.method, '${options.uri}')
..responseType = 'arraybuffer';
..responseType =
options.responseType == ResponseType.blobUrl ? 'blob' : 'arraybuffer';

final withCredentialsOption = options.extra['withCredentials'];
if (withCredentialsOption != null) {
Expand Down Expand Up @@ -66,18 +67,32 @@ class BrowserHttpClientAdapter implements HttpClientAdapter {
final completer = Completer<ResponseBody>();

xhr.onLoad.first.then((_) {
final ByteBuffer body = (xhr.response as JSArrayBuffer).toDart;
completer.complete(
ResponseBody.fromBytes(
body.asUint8List(),
xhr.status,
headers: xhr.getResponseHeaders(),
statusMessage: xhr.statusText,
isRedirect: xhr.status == 302 ||
xhr.status == 301 ||
options.uri.toString() != xhr.responseURL,
),
);
if (options.responseType == ResponseType.blobUrl) {
completer.complete(
ResponseBody.fromString(
web.URL.createObjectURL(xhr.response as web.Blob),
xhr.status,
headers: xhr.getResponseHeaders(),
statusMessage: xhr.statusText,
isRedirect: xhr.status == 302 ||
xhr.status == 301 ||
options.uri.toString() != xhr.responseURL,
),
);
} else {
final ByteBuffer body = (xhr.response as JSArrayBuffer).toDart;
completer.complete(
ResponseBody.fromBytes(
body.asUint8List(),
xhr.status,
headers: xhr.getResponseHeaders(),
statusMessage: xhr.statusText,
isRedirect: xhr.status == 302 ||
xhr.status == 301 ||
options.uri.toString() != xhr.responseURL,
),
);
}
});

Timer? connectTimeoutTimer;
Expand Down
16 changes: 13 additions & 3 deletions plugins/web_adapter/lib/src/dio_impl.dart
Copy link
Member

@AlexV525 AlexV525 Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What will the result be after downloaded? Could you demonstrate how it works?

EDIT: Please also add an example somewhere in our examples so everyone can run and see how it works.

Copy link
Author

@mbfakourii mbfakourii Jan 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added an example in example_flutter_app.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To test on the local web, make sure CORS is disabled.

and use local dio and dio_web_adapter in example_flutter_app pubspec

dependency_overrides:
  dio_web_adapter:
    path: ...\dio\plugins\web_adapter
  dio:
    path: ...\dio\dio

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why CORS is required?

Copy link
Author

@mbfakourii mbfakourii Jan 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To download a file from an online server to a local server, the browser gives a CORS error. This has nothing to do with Dio. I said this for testing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, how about a local server to scope with the minimal functionality instead of an outer source?

Copy link
Author

@mbfakourii mbfakourii Jan 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean to put a file locally? If we do this, the download display will be very fast and the download percentage will not be clearly visible.

Are you sure about this? This is just an example to demonstrate a feature 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A demo would not require displaying the progress, correct? The file could be as small as possible too.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are some examples for pure Dart

Just need to run it on the web.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:dio/dio.dart';

import 'adapter.dart';
Expand Down Expand Up @@ -25,9 +27,17 @@ class DioForBrowser with DioMixin implements Dio {
String lengthHeader = Headers.contentLengthHeader,
Object? data,
Options? options,
}) {
throw UnsupportedError(
'The download method is not available in the Web environment.',
}) async {
return fetch(
RequestOptions(
baseUrl: urlPath,
data: data,
method: options?.method ?? 'GET',
responseType: ResponseType.blobUrl,
queryParameters: queryParameters,
cancelToken: cancelToken,
onReceiveProgress: onReceiveProgress,
),
);
}
}
14 changes: 14 additions & 0 deletions plugins/web_adapter/test/browser_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,18 @@ void main() {
browserAdapter.fetch(opts, testStream, cancelFuture);
expect(browserAdapter.xhrs.every((e) => e.withCredentials == true), isTrue);
});

test('ResponseType in blobUrl', () async {
final browserAdapter = BrowserHttpClientAdapter(withCredentials: true);
final opts = RequestOptions(responseType : ResponseType.blobUrl);
final testStream = Stream<Uint8List>.periodic(
const Duration(seconds: 1),
(x) => Uint8List(x),
AlexV525 marked this conversation as resolved.
Show resolved Hide resolved
);
final cancelFuture = opts.cancelToken?.whenCancel;

browserAdapter.fetch(opts, testStream, cancelFuture);
expect(browserAdapter.xhrs.every((e) => e.withCredentials == true), isTrue);
expect(opts.responseType, ResponseType.blobUrl);
});
}
Loading