Flutter Handle Download In WebView (Normal & BLOB)
February 6, 2023
|
-- views(Updated on February 5, 2023)
February 6, 2023
|
-- views(Updated on February 5, 2023)
WebView is used to display websites without having to switch from application to browser, but the functionality of this WebView is limited if the website requires location access, storage, or the device's camera. The feature doesn't work right out of the box, so it needs to be handled manually. In this article, we will discuss how to handle file downloads, whether in the form of download links or BLOBs.
Don’t forget to set up each dependency for the platform you want to build. More info:
Below code is a placeholder for using WebView
DARTimport 'dart:developer';import 'dart:io';import 'package:flutter/material.dart';import 'package:flutter_downloader/flutter_downloader.dart';import 'package:flutter_inappwebview/flutter_inappwebview.dart';Future<void> main() async {WidgetsFlutterBinding.ensureInitialized();await FlutterDownloader.initialize(debug: false,);runApp(const MyApp());}class MyApp extends StatelessWidget {const MyApp({super.key});Widget build(BuildContext context) {return const MaterialApp(title: 'Webview Downloader',home: MyHomePage(),);}}class MyHomePage extends StatefulWidget {const MyHomePage({super.key});State<MyHomePage> createState() => _MyHomePageState();}class _MyHomePageState extends State<MyHomePage> {final url = "https://google.com";final normalDownloadUrl ="https://www.mediafire.com/file/amqyqnhnxtcyr5o/photo-1438761681033-6461ffad8d80.jpeg/file";final GlobalKey webViewKey = GlobalKey();InAppWebViewController? webViewController;late InAppWebViewGroupOptions options;late PullToRefreshController pullToRefreshController;void initState() {options = InAppWebViewGroupOptions(crossPlatform: InAppWebViewOptions(useShouldOverrideUrlLoading: true,mediaPlaybackRequiresUserGesture: false,useOnDownloadStart: true,javaScriptCanOpenWindowsAutomatically: true,javaScriptEnabled: true,),android: AndroidInAppWebViewOptions(useHybridComposition: true,),ios: IOSInAppWebViewOptions(allowsInlineMediaPlayback: true,),);pullToRefreshController = PullToRefreshController(onRefresh: () async {if (Platform.isAndroid) {webViewController?.reload();} else if (Platform.isIOS) {webViewController?.loadUrl(urlRequest: URLRequest(url: await webViewController?.getUrl()));}},);super.initState();}Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('Webview Downloader'),actions: [// Load normal or blob link download action],),body: InAppWebView(key: webViewKey,initialUrlRequest: URLRequest(url: Uri.parse(url)),initialOptions: options,pullToRefreshController: pullToRefreshController,onWebViewCreated: (controller) {webViewController = controller;},androidOnPermissionRequest: (controller, origin, resources) async {return PermissionRequestResponse(resources: resources,action: PermissionRequestResponseAction.GRANT,);},onDownloadStartRequest: (controller, downloadStartRequest) async {// Handle download here},onConsoleMessage: (controller, consoleMessage) {log('Console: ${consoleMessage.message}');},onLoadError: (controller, url, code, message) {log('Error: $message');},),);}}
In this article, I will save downloaded file in Downloads folder in Android and DocumentDirectory in iOS, to achieve at Android 10 or below, user needs to grant storage permission. Don’t forget to add below permission on AndroidManifest.xml
XML<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
And for Android 10, add requestLegacyExternalStorage inside application tag.
XMLandroid:requestLegacyExternalStorage="true"
On Android 11 or later, access to Downloads folder automatically granted using MediaStore API, more info about this, can be seen at here. Later, this app will ask about storage permission when application loaded, create function to request it.
DARTFuture<void> requestPermission() async {await Permission.storage.request();}
Add above function inside initState
DARTvoid initState() {requestPermission();// Other codesuper.initState();}
For normal download link, will use flutter_downloader for processing and show progress at notification, why BLOB not using this too? Currently, this package doesn’t support BLOB. First, add URL for normal download link, you can use below link or use your own link. Add inside HomePage.
DARTfinal normalDownloadUrl ="https://www.mediafire.com/file/amqyqnhnxtcyr5o/photo-1438761681033-6461ffad8d80.jpeg/file";
Register flutter_downloader callback, this is necessary, if you skip this part your app will crash when download started.
DARTimport 'dart:isolate';import 'dart:ui';final _port = ReceivePort();void initState() {// Other codeIsolateNameServer.registerPortWithName(_port.sendPort, 'downloader_send_port');_port.listen((dynamic data) {String id = data[0];DownloadTaskStatus status = data[1];int progress = data[2];debugPrint("Id: $id, Status: $status, progress: $progress");});FlutterDownloader.registerCallback(downloadCallback);super.initState();}('vm:entry-point')static void downloadCallback(String id, DownloadTaskStatus status, int progress) {final SendPort? send =IsolateNameServer.lookupPortByName('downloader_send_port');send?.send([id, status, progress]);}
Download callback has been registered, now time to load link inside AppBar actions
DARTTextButton(onPressed: () {webViewController?.loadUrl(urlRequest: URLRequest(url: Uri.parse(normalDownloadUrl),),);},child: const Text('Normal Link',style: TextStyle(color: Colors.white),),),
Create a function to handle normal download link
DARTFuture<void> _downloadNormalLink(DownloadStartRequest downloadStartRequest) async {final output = (await DownloadsPath.downloadsDirectory());if (output != null) {try {await FlutterDownloader.enqueue(url: downloadStartRequest.url.toString(),savedDir: output.path,fileName: downloadStartRequest.suggestedFilename,openFileFromNotification: true,);} catch (e) {log(e.toString());}}}
Implement above function inside onDownloadStartRequest. Separating normal link and blob link, for example blob link will look like this blob:somelink
DARTonDownloadStartRequest: (controller, downloadStartRequest) async {debugPrint("onDownloadStart ${downloadStartRequest.toMap()}");if (!downloadStartRequest.url.toString().contains('blob')) {await _downloadNormalLink(downloadStartRequest);} else {// BLOB link handler}},
Time for testing
Works great 👍👍👍
Overview of the download BLOB process.
First, create injected JavaScript for opening BLOB link using AJAX, more info about this can be read here.
DARTString blobHandlerJavascript(String url) {return '''let response = await new Promise(resolve => {var xhr = new XMLHttpRequest();console.log("$url");xhr.open("GET", "$url", true);xhr.responseType = 'blob';xhr.onload = function(e) {if (this.status == 200) {var blob = this.response;var reader = new FileReader();reader.readAsDataURL(blob);reader.onloadend = function() {var base64data = reader.result;var base64ContentArray = base64data.split(",");var decodedFile = base64ContentArray[1];resolve(decodedFile);};}};xhr.onerror = function () {resolve(null);console.log("** An error occurred during the XMLHttpRequest");};xhr.send();})return response;''';}
Create base64 string into a file converter, automatically open converted file after converting completed.
DARTFuture<void> _createFileFromBase64(String base64content, String fileName) async {debugPrint('Processing $fileName');var bytes = base64Decode(base64content.replaceAll('\n', ''));final output = (await DownloadsPath.downloadsDirectory());if (output != null) {final file = File("${output.path}/$fileName");await file.writeAsBytes(bytes.buffer.asUint8List());try {await OpenFile.open(file.path);} catch (e) {log(e.toString());}}}
Create function to inject a JavaScript code above into WebView.
DARTFuture<void> _downloadBlobLink(InAppWebViewController controller,DownloadStartRequest downloadStartRequest) async {final result = await controller.callAsyncJavaScript(functionBody:blobHandlerJavascript(downloadStartRequest.url.toString()));if (result != null && result.value != null) {await _createFileFromBase64(result.value,downloadStartRequest.suggestedFilename ??'${downloadStartRequest.url.toString().split('/').last}.${downloadStartRequest.mimeType != null?.split('/').last}',);}}
Implement above function inside onDownloadStartRequest.
DARTonDownloadStartRequest: (controller, downloadStartRequest) async {debugPrint("onDownloadStart ${downloadStartRequest.toMap()}");if (!downloadStartRequest.url.toString().contains('blob')) {await _downloadNormalLink(downloadStartRequest);} else {await _downloadBlobLink(controller, downloadStartRequest);}},
Since I don’t have a working blob link for demo, I will use trigger it manually using HTML data. Refer from here.
DARTString loadBlobDownloadLink() {return '''<a download="hello.txt" href='#' id="link">Download</a><script>let blob = new Blob(["Hello, world!"], {type: 'text/plain'});link.href = URL.createObjectURL(blob);</script>''';}
Notes: a download anchor attributes not supported on Android WebView, so the suggested file name will be a UUID v4 instead of a real name.
Add button inside AppBar actions
DARTTextButton(onPressed: () {webViewController?.loadData(data: loadBlobDownloadLink(),);},child: const Text('Blob Link',style: TextStyle(color: Colors.white),),),
Time for testing
Perfect, handle for BLOB link successfully working 🎉🎉. Full example code in this article listed below.
DARTimport 'dart:convert';import 'dart:developer';import 'dart:io';import 'dart:isolate';import 'dart:ui';import 'package:flutter/material.dart';import 'package:flutter_downloader/flutter_downloader.dart';import 'package:flutter_inappwebview/flutter_inappwebview.dart';import 'package:lecle_downloads_path_provider/lecle_downloads_path_provider.dart';import 'package:open_file/open_file.dart';import 'package:permission_handler/permission_handler.dart';Future<void> main() async {WidgetsFlutterBinding.ensureInitialized();await FlutterDownloader.initialize(debug: false,);runApp(const MyApp());}class MyApp extends StatelessWidget {const MyApp({super.key});Widget build(BuildContext context) {return const MaterialApp(title: 'Webview Downloader',home: MyHomePage(),);}}class MyHomePage extends StatefulWidget {const MyHomePage({super.key});State<MyHomePage> createState() => _MyHomePageState();}class _MyHomePageState extends State<MyHomePage> {final url = "https://google.com";final normalDownloadUrl ="https://www.mediafire.com/file/amqyqnhnxtcyr5o/photo-1438761681033-6461ffad8d80.jpeg/file";final GlobalKey webViewKey = GlobalKey();final _port = ReceivePort();InAppWebViewController? webViewController;late InAppWebViewGroupOptions options;late PullToRefreshController pullToRefreshController;void initState() {requestPermission();options = InAppWebViewGroupOptions(crossPlatform: InAppWebViewOptions(useShouldOverrideUrlLoading: true,mediaPlaybackRequiresUserGesture: false,useOnDownloadStart: true,javaScriptCanOpenWindowsAutomatically: true,javaScriptEnabled: true,),android: AndroidInAppWebViewOptions(useHybridComposition: true,),ios: IOSInAppWebViewOptions(allowsInlineMediaPlayback: true,),);pullToRefreshController = PullToRefreshController(onRefresh: () async {if (Platform.isAndroid) {webViewController?.reload();} else if (Platform.isIOS) {webViewController?.loadUrl(urlRequest: URLRequest(url: await webViewController?.getUrl()));}},);IsolateNameServer.registerPortWithName(_port.sendPort, 'downloader_send_port');_port.listen((dynamic data) {String id = data[0];DownloadTaskStatus status = data[1];int progress = data[2];debugPrint("Id: $id, Status: $status, progress: $progress");});FlutterDownloader.registerCallback(downloadCallback);super.initState();}Future<void> requestPermission() async {await Permission.storage.request();}Future<void> _createFileFromBase64(String base64content, String fileName) async {debugPrint('Processing $fileName');var bytes = base64Decode(base64content.replaceAll('\n', ''));final output = (await DownloadsPath.downloadsDirectory());if (output != null) {final file = File("${output.path}/$fileName");await file.writeAsBytes(bytes.buffer.asUint8List());try {await OpenFile.open(file.path);} catch (e) {log(e.toString());}}}String blobHandlerJavascript(String url) {return '''let response = await new Promise(resolve => {var xhr = new XMLHttpRequest();console.log("$url");xhr.open("GET", "$url", true);xhr.responseType = 'blob';xhr.onload = function(e) {if (this.status == 200) {var blob = this.response;var reader = new FileReader();reader.readAsDataURL(blob);reader.onloadend = function() {var base64data = reader.result;var base64ContentArray = base64data.split(",");var decodedFile = base64ContentArray[1];resolve(decodedFile);};}};xhr.onerror = function () {resolve(null);console.log("** An error occurred during the XMLHttpRequest");};xhr.send();})return response;''';}// Reference https://javascript.info/blobString loadBlobDownloadLink() {return '''<a download="hello.txt" href='#' id="link">Download</a><script>let blob = new Blob(["Hello, world!"], {type: 'text/plain'});link.href = URL.createObjectURL(blob);link.download = "hello.txt";</script>''';}void dispose() {IsolateNameServer.removePortNameMapping('downloader_send_port');super.dispose();}('vm:entry-point')static void downloadCallback(String id, DownloadTaskStatus status, int progress) {final SendPort? send =IsolateNameServer.lookupPortByName('downloader_send_port');send?.send([id, status, progress]);}Future<void> _downloadNormalLink(DownloadStartRequest downloadStartRequest) async {final output = (await DownloadsPath.downloadsDirectory());if (output != null) {try {await FlutterDownloader.enqueue(url: downloadStartRequest.url.toString(),savedDir: output.path,fileName: downloadStartRequest.suggestedFilename,openFileFromNotification: true,);} catch (e) {log(e.toString());}}}Future<void> _downloadBlobLink(InAppWebViewController controller,DownloadStartRequest downloadStartRequest) async {final result = await controller.callAsyncJavaScript(functionBody:blobHandlerJavascript(downloadStartRequest.url.toString()));if (result != null && result.value != null) {await _createFileFromBase64(result.value,downloadStartRequest.suggestedFilename ??'${downloadStartRequest.url.toString().split('/').last}.${downloadStartRequest.mimeType != null?.split('/').last}',);}}Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('Webview Downloader'),actions: [TextButton(onPressed: () {webViewController?.loadUrl(urlRequest: URLRequest(url: Uri.parse(normalDownloadUrl),),);},child: const Text('Normal Link',style: TextStyle(color: Colors.white),),),TextButton(onPressed: () {webViewController?.loadData(data: loadBlobDownloadLink(),);},child: const Text('Blob Link',style: TextStyle(color: Colors.white),),)],),body: InAppWebView(key: webViewKey,initialUrlRequest: URLRequest(url: Uri.parse(url)),initialOptions: options,pullToRefreshController: pullToRefreshController,onWebViewCreated: (controller) {webViewController = controller;},androidOnPermissionRequest: (controller, origin, resources) async {return PermissionRequestResponse(resources: resources,action: PermissionRequestResponseAction.GRANT,);},onDownloadStartRequest: (controller, downloadStartRequest) async {debugPrint("onDownloadStart ${downloadStartRequest.toMap()}");if (!downloadStartRequest.url.toString().contains('blob')) {await _downloadNormalLink(downloadStartRequest);} else {await _downloadBlobLink(controller, downloadStartRequest);}},onConsoleMessage: (controller, consoleMessage) {log('Console: ${consoleMessage.message}');},onLoadError: (controller, url, code, message) {log('Error: $message');},),);}}
Thank you for reading to the end, that's all for this article, I hope it's useful
👍
❤️
👏
🎉
Share this article
A periodic update about my life, recent blog posts, how-tos, and discoveries.
No spam - unsubscribe at any time!