Profile PictureYanuar Bimantoro
Open menu

Flutter Handle Download In WebView (Normal & BLOB)

February 6, 2023

|

-- views

(Updated on February 5, 2023)

article cover

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.

Create Project

  • Create new flutter project flutter create project webview_downloader
  • Add required dependencies flutter pub add flutter_downloader flutter_inappwebview lecle_downloads_path_provider open_file permission_handler
  • Don’t forget to set up each dependency for the platform you want to build. More info:

  • flutter_downloader
  • flutter_inappwebview
  • lecle_downloads_path_provider
  • open_file
  • permission_handler
  • Initialization

    Below code is a placeholder for using WebView

    DART
    import '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.

    XML
    android: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.

    DART
    Future<void> requestPermission() async {
    await Permission.storage.request();
    }

    Add above function inside initState

    DART
    void initState() {
    requestPermission();
    // Other code
    super.initState();
    }

    Handle Normal Download Link

    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.

    DART
    final 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.

    DART
    import 'dart:isolate';
    import 'dart:ui';
    final _port = ReceivePort();
    void initState() {
    // Other code
    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();
    }
    ('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

    DART
    TextButton(
    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

    DART
    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());
    }
    }
    }

    Implement above function inside onDownloadStartRequest. Separating normal link and blob link, for example blob link will look like this blob:somelink

    DART
    onDownloadStartRequest: (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 👍👍👍

    Handle BLOB Download Link

    Overview of the download BLOB process.

    A visual depiction of what is being written about

    First, create injected JavaScript for opening BLOB link using AJAX, more info about this can be read here.

    DART
    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;
    ''';
    }

    Create base64 string into a file converter, automatically open converted file after converting completed.

    DART
    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());
    }
    }
    }

    Create function to inject a JavaScript code above into WebView.

    DART
    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}',
    );
    }
    }

    Implement above function inside onDownloadStartRequest.

    DART
    onDownloadStartRequest: (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.

    DART
    String 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

    DART
    TextButton(
    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.

    DART
    import '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/blob
    String 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

    Updates delivered to your inbox!

    A periodic update about my life, recent blog posts, how-tos, and discoveries.

    No spam - unsubscribe at any time!

    Related articles