webviewx

pub package

A feature-rich cross-platform webview using webview_flutter for mobile and iframe for web. JS interop-ready.

Getting started


Mobile

Web


Basic usage

  1. Create a WebViewXController inside your stateful widget
late WebViewXController webviewController;
  1. Add the WebViewX widget inside the build method, and set the onWebViewCreated callback in order to retrieve the controller when the webview is initialized
WebViewX(
    initialContent: '<h2> Hello, world! </h2>',
    initialSourceType: SourceType.HTML,
    onWebViewCreated: (controller) => webviewController = controller,
    ...
    ... other options
);

Important !

If you need to add other widgets on top of the webview (e.g. inside a Stack widget), you MUST wrap those widgets with a WebViewAware widget. This does nothing on mobile, but on web it allows widgets on top to intercept gestures. Otherwise, those widgets may not be clickable and/or the iframe will behave weird (unexpected refresh/reload - this is a well known issue).

Also, as a side note: If you happen to add widgets on top of the webview, wrap them and then you notice that the iframe still reloads unexpectedly, you should check if there are other widgets that sit on top without being noticed, or try to wrap InkWell, GestureRecognizer or Button widgets to see which one causes the problem.

  1. Interact with the controller (run the example app to see how does it work)
webviewController.loadContent(
    'https://flutter.dev',
    SourceType.URL,
);
webviewController.goBack();

webviewController.goForward();
...
...

Features

Note: For instructions on how to use theese features, please see each one's documentation, located inside it's coresponding class.

  • Widget properties

FeatureDetails
String initialContentInitial webview content
SourceType initialSourceTypeInitial webview content type (URL, URL_BYPASS or HTML)
String? userAgentUser agent ( issues on web )
double? widthWidget's width (if null, it takes all available space)
double? heightWidget's height (if null, it takes all available space)
Function(WebViewXController controller)? onWebViewCreatedCallback that gets executed when the webview has initialized
Set<EmbeddedJsContent> jsContentA set of EmbeddedJsContent, which is an object that defines some javascript which will be embedded in the page, once loaded (check the example app)
Set<DartCallback> dartCallBacksA set of DartCallback, which is an object that defines a dart callback function, which will be called from javascript (check the example app)
bool ignoreAllGesturesBoolean value that specifies if the widget should ignore all gestures right after it is initialized
JavascriptMode javascriptModeThis specifies if Javascript should be allowed to execute, or not (allowed by default, you must allow it in order to use above features)
AutoMediaPlaybackPolicy initialMediaPlaybackPolicyThis specifies if media content should be allowed to autoplay when initialized (i.e when the page is loaded)
void Function(String src)? onPageStartedCallback that gets executed when a page starts loading (e.g. after you change the content)
void Function(String src)? onPageFinishedCallback that gets executed when a page finishes loading
void Function(WebResourceError error)? onWebResourceErrorCallback that gets executed when there is an error when loading resources ( issues on web )
WebSpecificParams webSpecificParamsThis is an object that contains web-specific options. Theese are not available on mobile (yet)
MobileSpecificParams mobileSpecificParamsThis is an object that contains mobile-specific options. Theese are not available on web (yet)

  • Controller properties

FeatureUsage
Load URL that allows iframe embeddingwebviewController.loadContent(URL, SourceType.URL)
Load URL that doesnt allow iframe embeddingwebviewController.loadContent(URL, SourceType.URL_BYPASS)
Load URL that doesnt allow iframe embedding, with headerswebviewController.loadContent(URL, SourceType.URL_BYPASS, headers: {'x-something': 'value'})
Load HTML from stringwebviewController.loadContent(HTML, SourceType.HTML)
Load HTML from assetswebviewController.loadContent(HTML, SourceType.HTML, fromAssets: true)
Check if you can go back in historyawait webviewController.canGoBack()
Go back in historyawait webviewController.goBack()
Check if you can go forward in historyawait webviewController.canGoForward()
Go forward in historyawait webviewController.goForward()
Reload current contentwebviewController.reload()
Check if all gestures are ignoredwebviewController.ignoringAllGestures
Set ignore all gestureswebviewController.setIgnoreAllGestures(value)
Evaluate "raw" javascript codeawait webviewController.evalRawJavascript(JS)
Evaluate "raw" javascript code in global context ("page")await webviewController.evalRawJavascript(JS, inGlobalContext: true)
Call a JS methodawait webviewController.callJsMethod(METHOD_NAME, PARAMS_LIST)
Retrieve webview's contentawait webviewController.getContent()

Limitations and notes

While this package aims to put together the best of both worlds, there are differences between web and mobile.

  • Running and building

    First, this package was being developed while the default web renderer was html. Now(Flutter 2, Dart 2.12), the default renderer is canvaskit.

    From my experience, this package does behave a little bit weird on canvaskit, so you should use the html renderer.

    To do this, you have to run your ordinary flutter run -d chrome command with the --web-renderer html extra argument, like this:

    flutter run -d chrome --web-renderer html
    

    for running and

    flutter build web --web-renderer html
    

    for building.

  • Behaviour when loading content

    To make the web version (iframe) work as it is, I had to use this x-frame bypass in order to make a request to a pre-defined CORS proxy, which removes the headers that block iframe embeddings.

    This might seem like a hack, and it really is, but I couldn't find any other way to make the iframe behave similar to the mobile webview (which is some kind of an actual browser, that's why everything works there by default).

    Also, it might not work on Safari, as stated here.

  • Web navigation

    On web, the history navigation stack is built from scratch because I couldn't handle iframe's internal history the right way.


Known issues and TODOs

  • On web, user-agent and headers only work when using SourceType.URL_BYPASS, and they only have effect the first time being used (view/web.dart)

  • On web, it should be possible to send any errors caught when loading an URL_BYPASS to a dart callback, which will then be sent through the onWebResourceError callback, just like on the mobile version (utils/x_frame_options_bypass.dart)

  • On web, it should be possible to add a custom proxy list without the js null-checking mess (utils/x_frame_options_bypass.dart)

  • Eventually (if possible), most if not all properties from WebSpecificParams and MobileSpecificParams should merge and theese two objects may disappear

  • On mobile, the controller's value's source type becomes out of sync when moving back and forth in history. This happens because the url change is not yet intercepted and set the model accordingly (shouldn't be hard to fix).

  • On mobile, the controller's callJsMethod doesnt throw an error if the operation failed. Instead it only shows the error in console.

  • Add tests

  • List open, there may be others

Credits

This package wouldn't be possible without the following:

  • And last but not least, http://deversoft.ro (the company I work for) for motivating me throughout the development process

License

MIT

Libraries

webviewx