Flutter WebView blob pdf download
Asked Answered
C

3

11

I am trying to download a file from a website using flutter, I first used evaluateJavaScript and changed some values before clicking a generate button which is supposed to download an appropriate pdf file to the phone.

Here is my code:

InAppWebView(
        initialUrl: '...',
        initialOptions: InAppWebViewGroupOptions(
          crossPlatform: InAppWebViewOptions(
              debuggingEnabled: true,
              useOnDownloadStart: true,
              javaScriptEnabled: true,
          ),
        ),
        //javascriptMode: JavascriptMode.unrestricted,
        onWebViewCreated: (InAppWebViewController webViewController) {
          _controller = webViewController;
          },
          onLoadStop: (_controller ,url) async {
          await _loadData();

          _controller.evaluateJavascript(source:
"console.log(document.getElementById('field-lastname').value = '${data[Field.name]}' );"
+"console.log(document.getElementById('generate-btn').click());"

           );
          },

          onDownloadStart:(_controller,url) async{
            print("onDownloadStart $url");
            final taskId = await FlutterDownloader.enqueue(
                url: url,
                savedDir: (await getExternalStorageDirectory()).path,
            showNotification: true, // show download progress in status bar (for Android)
            openFileFromNotification: true,);
          },

      ),

The URL printed is like this one

onDownloadStart blob:https://example.com/a2a5316c-9290-4da7-8a2a-9f899861046a

And here is the console:
And here is the console

Can someone help me?

Cerallua answered 16/11, 2020 at 21:28 Comment(0)
I
21

Downloading file with blob url is tricky and not supported out of the box in the current state of webviews in Flutter. There are 3 popular plugins

There is a note at README in community repository

We are working closely with the Flutter Team to integrate all the Community Plugin features in the Official WebView Plugin. We will try our best to resolve PRs and Bugfixes, but our priority right now is to merge our two code-bases. Once the merge is complete we will deprecate the Community Plugin in favor of the Official one

There is a lot of work yet to build fully working and bugfree webview. Currently for more challenging tasks like this mentioned here, the best attempt is to use flutter_inappwebview which is very popular and used by a lot people with success. There is issue associated with blob files. As we can see in your snippet you already used this plugin. To download blob file you can try convert blob:url to base64 like in this case Download Blob file from Website inside Android WebViewClient

Possible workaround

To your webview (_controller) add JavaScriptHandler. I would assume onWebViewCreated might be ok.

        controller.addJavaScriptHandler(
          handlerName: _webViewHandlerName,
          callback: (data) async {
            if (data.isNotEmpty) {
              final String receivedFileInBase64 = data[0];
              final String receivedMimeType = data[1];

              // NOTE: create a method that will handle your extensions
              final String yourExtension =
                  _mapMimeTypeToYourExtension(receivedMimeType); // 'pdf'

              _createFileFromBase64(
                  receivedFileInBase64, 'YourFileName', yourExtension);
            }
          },
        );

JavaScript handler will receive two values stored in array. First argument is file encoded in base64 and second one is mimeType e.g. application/pdf. Having information about mimeType allows us to get information about extension which should be used to save file with. They can be easly mapped application/pdf => 'pdf' etc.

  _createFileFromBase64(String base64content, String fileName, String yourExtension) async {
    var bytes = base64Decode(base64content.replaceAll('\n', ''));
    final output = await getExternalStorageDirectory();
    final file = File("${output.path}/$fileName.$yourExtension");
    await file.writeAsBytes(bytes.buffer.asUint8List());
    print("${output.path}/${fileName}.$yourExtension");
    await OpenFile.open("${output.path}/$fileName.$yourExtension");
    setState(() {});
  }

Finally where blob url can be handled the JavaScript is invoked.

       onDownloadStart: (controller, url) async {
        print("onDownloadStart $url");
        var jsContent = await rootBundle.loadString("assets/js/base64.js");
        await controller.evaluateJavascript(
            source: jsContent.replaceAll("blobUrlPlaceholder", url));
      },

Javascript (I prefer to load it as an asset base64.js, better than hardcoded in dart code) It opens blob url and pass to encodedBase64 data and mimeType as arguments to our handler blobToBase64Handler in dart.

var xhr = new XMLHttpRequest();
var blobUrl = "blobUrlPlaceholder";
console.log(blobUrl);
xhr.open('GET', blobUrl, 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 mimeType = base64ContentArray[0].match(/[^:\s*]\w+\/[\w-+\d.]+(?=[;| ])/)[0];
      var decodedFile = base64ContentArray[1];
      console.log(mimeType);
      window.flutter_inappwebview.callHandler('blobToBase64Handler', decodedFile, mimeType);
    };
  };
};
xhr.send();

source: Download Blob file from Website inside Android WebViewClient

source: How to decode base64 PDF string in Flutter?

It's not clean and looks hacky but could not find better and easier

Ignaciaignacio answered 18/11, 2020 at 22:20 Comment(18)
Thank you for your very complete answer ! I tried your proposition, but it doesn't not download the file, there is no notification or anything... Did I miss something ?Cerallua
As you can see I do not use FlutterDownloader in onDownloadStart. We save pdf directly as a file. File is saved in getExternalStorageDirectory() folder. Thats why there is no notification. There is problem with java.lang.IllegalStateException: Data cannot occupy more than 10240 bytes when serialized when downloader manager is used. To inform user that file is downloaded you can show toast. Debug your logs to see where exactly the file is saved.Ignaciaignacio
My bad, I moved my base64.js file and forgot about it in my code ! It is working and I can finaly see my pdf !!! I am so happy right now, thank you very much for your help ! Your answer is extremely complete and educative, thank you so much !Cerallua
this method is working on my PocoF1 mobile but it doesn't works in Redmi go mobile, may i know what will be the reason ???Nonagon
@Ignaciaignacio is this method also available for spreadsheet like .xls or .xlsx?Sauropod
@Nazar yes its possible to download other type of files aswell. The blob gives us information about size and mimeType. In my answer the code is hardcoded for 'application/pdf', but I also use more flexible solution with passing the mimeType to the download function and save file with extension that depends on mimeType from blob. In your case it would be something like this 'application/vnd.ms-excel'. I will update my answer to be more generic in spare time.Ignaciaignacio
@Ignaciaignacio I've tried to content type form application/pdf to application/vnd.ms-excel and change file type from .pdf to .xls but I got this error [ERROR:flutter/lib/ui/ui_dart_state.cc(186)] Unhandled Exception: FormatException: Invalid character (at character 23) formats-officedocument.spreadsheetml.sheet;base64,UEsDBAoAAAAAAPJukFIAAAAAA...Sauropod
@Ignaciaignacio maybe you can take a look at my question at #67125601 so I can accept your answerSauropod
@Nazar I will add clean solution soon, for this moment I updated first snippet for you. You can try it. It's not based on length anymore just split data and take what is needed.Ignaciaignacio
@Ignaciaignacio okay, I will wait for it. Thank you in advance.Sauropod
@Ignaciaignacio I got an error at _webViewHandlerName and _mapMimeTypeToYourExtension, it said the method isn't defined. maybe you forgot to add some code?Sauropod
@Nazar I left the method _mapMimeTypeToYourExtension to write on your own. The method takes String mimeType as a argument and should return String with extension that file should be saved withIgnaciaignacio
it's not working on iOS. the XMLHttpRequest seems didn't work his job. do you have any idea why?Mammalogy
With the new iOS version there is another way to achieve blob download, I will try to update my answer in spare timeIgnaciaignacio
@axumnemonic You can try my solution below,and it works for both Android and iOS.Muchness
why mime type always return application/pdf? @IgnaciaignacioSociology
Thank you so much. It works for me. I saw your answer as one of the first, but I avoided it because it looks to "hacky", but after hours of trying other solution, (including Gemini, ChatGPT...) this is the only thing that worksBumgardner
Worked for me too! Very very thanks. Just an important side help for those who wanna use this solution, Define the handler name like that: handlerName: 'blobToBase64Handler',Copperplate
M
3

I wrote a solution for both Android and iOS. which may support any type of files.

  1. Build a simple backend using Fluter with Jaguar or other backend framework
  2. Convert the blob as formData and post it to Flutter.

The full code: https://github.com/guoguoguilai/flutter-webview-blob-download

Muchness answered 21/9, 2021 at 1:15 Comment(2)
how to use it? please provide an example.Sociology
Refer to github.com/guoguoguilai/flutter-webview-blob-download/blob/main/…Muchness
E
0

Just in case someone will find this question, my solution for blob download:

    controller = WebViewController()
      ..addJavaScriptChannel('Mobile', onMessageReceived: (JavaScriptMessage message) {
          Map<String, dynamic> data = jsonDecode(message.message);
          if (data['action'] == 'fileDownload') {
            saveToFile(data['data']);
          }
        })
      ..setNavigationDelegate(
        NavigationDelegate(
          onPageFinished: (String url) {
            controller.runJavaScript("""
if (!window.mobilelistener) {
  window.mobilelistener = true;
  document.addEventListener('click', async function(event) {
    var element = event.target; 
    if (element.tagName === 'A' && element.href.startsWith("blob")) {
      event.preventDefault();
      var reader = new FileReader();
      reader.onloadend = function() {
        var base64data = reader.result;
        Mobile.postMessage(JSON.stringify(
          {
            "action": "fileDownload",
            "data": base64data
          }
        ));
      }
      fetch(element.href)
        .then(r => r.blob())
        .then(blob => reader.readAsDataURL(blob));
    }
  });
}
            """);
          },
          onNavigationRequest: (NavigationRequest request) {
            if (request.url.startsWith('blob:')) {
              return NavigationDecision.prevent;
            }

And save to file is something like this:

void saveToFile(String content) async {
    content = content.substring(content.indexOf('base64,') + 7);
    Uint8List bytes = base64Decode(content);

    String? path = await FileSaver.instance.saveFile(
      name: 'file',
      bytes: bytes,
      ext: 'pdf',
      mimeType: MimeType.pdf);
}
Equi answered 10/11, 2023 at 6:51 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.