What’s new in InAppWebView 5? Null-safety, new features, bug fixes

Lorenzo Pichilli
Mar 23 · 8 min read
Flutter InAppWebView.

Finally, after a lot of work, the new version 5 of the flutter_inappwebview plugin is out (at the time of this writing, the latest release is 5.2.0)!

So, what’s new? What changed?

Well.. a lot!

Null-safety support

Android Hybrid Composition support

Android WebView with Hybrid Composition.

No more URL as a String = fewer problems

Also, there is the new class URLRequest that represents, well, a URL load request that uses Uri as the type of the url property.
This class is used when you want to use the loadUrl method or in the new WebView property initialUrlRequest (that replaces the old initialUrl and initialHeaders properties). A simple usage example is:

child: InAppWebView(
initialUrlRequest: URLRequest(
url: Uri.parse("https://flutter.dev/")
),
),

Furthermore, with URLRequest , you can make an initial POST request, such as:

child: InAppWebView(
initialUrlRequest: URLRequest(
url: Uri.parse("https://example.com"),
method: 'POST',
body: Uint8List.fromList(utf8.encode("name=FooBar")),
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
),
),

Unfortunately, on Android, POST requests will ignore the headers property because there isn’t any native API to load POST requests with headers such as loadUrl . That’s because the native method android.webkit.WebView.postUrl is the only one that can send this type of request.

Limited Cookies support on iOS < 11.0

User Scripts

However, I should make a precision here for Android. What I said is guaranteed on iOS, but not on Android (do you know that iOS != Android? 🤷‍♂️)!
That’s because the corresponding native class/feature doesn’t exist on the Android side, so InAppWebView tries to inject all the user scripts as soon as possible. You can think of that as something like this:

Android WebView when injects User Scripts into the webpage.

To manage UserScripts you can use the corresponding methods, such as addUserScript, removeUserScript, etc.

Content Worlds

But, as I said before, iOS != Android and, on Android, this concept doesn’t exist natively. So, it has been implemented with the usage of <iframe> HTML elements.

As a famous Italian said once: “First reaction, SHOCK!”.

You may ask why I didn’t use something like the LiquidCore library or something similar to the JavaScriptCore iOS framework to implement it.
The problem with using these libraries/framework is that you can’t access the window or document JavaScript objects of the current webpage, of course.
Instead, with WKContentWorld, you can access these objects and, so, you can interact with the webpage itself.
Using iframes on Android gives you the ability to create a new JavaScript context without conflicting with the main JavaScript context of the webpage (you can have, for example, 2 variables with the same name, because they exist in 2 different contexts/content worlds) and implement this sort of Content World such as on iOS. So, this plugin will create and append an <iframe> with id attribute equals to flutter_inappwebview_[Content World Name HERE] to the webpage’s content that contains only the inline scripts in order to define a new scope of execution for JavaScript code.

Obviously, this comes with some limitations/disadvantages:

  • for any ContentWorld, except ContentWorld.PAGE (that is the webpage itself), if you need to access the window or document global Object, you need to use window.top and window.top.document because the code runs inside an iframe;
  • the execution of the inline scripts could be blocked by the Content-Security-Policy header.

A simple example:

child: InAppWebView(
initialUrlRequest: URLRequest(
url: Uri.parse("https://flutter.dev"),
),
onLoadStop: (controller, url) async {
await controller.evaluateJavascript(source: "var foo = 49;");
await controller.evaluateJavascript(source: "var bar = 19;",
contentWorld: ContentWorld.PAGE);
print(await controller.evaluateJavascript(source: "foo + bar;"));

print(await controller.evaluateJavascript(source: "bar;",
contentWorld: ContentWorld.DEFAULT_CLIENT));
await controller.evaluateJavascript(source: "var bar = 2;",
contentWorld: ContentWorld.DEFAULT_CLIENT);
print(await controller.evaluateJavascript(source: "bar;",
contentWorld: ContentWorld.DEFAULT_CLIENT));

if (Platform.isIOS) {
await controller.evaluateJavascript(
source: "document.body.innerHTML = 'LOL';",
contentWorld: ContentWorld.world(name: "MyWorld"));
} else {
await controller.evaluateJavascript(
source: "window.top.document.body.innerHTML = 'LOL';",
contentWorld: ContentWorld.world(name: "MyWorld"));
}
},
onConsoleMessage: (controller, consoleMessage) {
print(consoleMessage);
},
),

The proof of this example is left to the reader.

Apple Pay API

Check the official flutter_inappwebview documentation for the full list of API affected by this!

Evaluate Async JavaScript code

On iOS, you can use this function starting from iOS 10.3+ because, as stated here: async function Browser Compatibility, async functions should be already supported from that version. So, for iOS versions inside the range [10.3, 14.0), it has been implemented using the evaluateJavascript method and using a Map that contains an identifier and a callback that will be called at the end of the async function execution with the returned result. On Android, it has been implemented the same way!

The return type is not the same as the evaluateJavascript method, but a CallAsyncJavaScriptResult instance, where the value property contains the success value (if any) and the error property contains a String representing the failure value (if any).

Here is an example:

child: InAppWebView(
initialUrlRequest: URLRequest(
url: Uri.parse("https://flutter.dev"),
),
onLoadStop: (controller, url) async {
final String functionBody = """
var p = new Promise(function (resolve, reject) {
window.setTimeout(function() {
if (x >= 0) {
resolve(x);
} else {
reject(y);
}
}, 1000);
});
await p;
return p;
""";

var result = await controller.callAsyncJavaScript(
functionBody: functionBody,
arguments: {'x': 49, 'y': 'error message'});
print(result);

result = await controller.callAsyncJavaScript(
functionBody: functionBody,
arguments: {'x': -49, 'y': 'error message'});
print(result);
},
),

that will print {value: 49, error: null} and {value: null, error: "error message"} respectively.

Service Worker API

Future main() async {
WidgetsFlutterBinding.ensureInitialized();
if (Platform.isAndroid) {
await AndroidInAppWebViewController.setWebContentsDebuggingEnabled(true);
var swAvailable = await AndroidWebViewFeature.isFeatureSupported(AndroidWebViewFeature.SERVICE_WORKER_BASIC_USAGE);
var swInterceptAvailable = await AndroidWebViewFeature.isFeatureSupported(AndroidWebViewFeature.SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST);
if (swAvailable && swInterceptAvailable) {
AndroidServiceWorkerController serviceWorkerController = AndroidServiceWorkerController.instance();
serviceWorkerController.serviceWorkerClient = AndroidServiceWorkerClient(
shouldInterceptRequest: (request) async {
print(request);
return null;
},
);
}
}
runApp(MyApp());
}

Instead, on iOS, the JavaScript Service Worker API is available starting from iOS 14.0+.

To enable this JavaScript API on iOS there are only 2 ways:

  • using “App-Bound Domains”
  • your App proposes itself as a possible “Default Browser” such as iOS Safari or Google Chrome

App-Bound Domains: read the WebKit — App-Bound Domains article for details. You can specify up to 10 “app-bound” domains using the new Info.plist key WKAppBoundDomains, for example:

<dict>
<key>WKAppBoundDomains</key>
<array>
<string>flutter.dev</string>
<string>github.com</string>
</array>
</dict>

After that, you need to set to true the limitsNavigationsToAppBoundDomains iOS-specific WebView option, for example:

InAppWebViewGroupOptions(
ios: IOSInAppWebViewOptions(
limitsNavigationsToAppBoundDomains: true
)
)

iOS Default Browser: read the Preparing Your App to be the Default Browser or Email Client article for details.

Web Message Channels and Web Message Listeners

  • Web Message Channels: they are the representation of the HTML5 message channels. See Channel Messaging API for more details. To create a Web Message Channel, you need to use the InAppWebViewController.createWebMessageChannel method. This method should be called when the page is loaded, for example, when the WebView.onLoadStop event is fired.
  • Web Message Listeners: It allows to inject a JavaScript object into each frame that the WebMessageListener will listen on. To add a Web Message Listener, you need to use the InAppWebViewController.addWebMessageListener method. This method should be called before the webpage that uses it is loaded, for example when the WebView.onWebViewCreated event is fired.

Official WebSite

Also, it contains a Showcase section with an open list of apps built with Flutter and Flutter InAppWebView. At this time, because the website is new, there is only one App, that is the Flutter Browser App.

Are you using this plugin? Submit your app through the Submit App page and follow the instructions!

Conclusion

That’s all for today!

I want to thank all the people that are supporting the project in any way! Thanks a lot to all of you! 💙

Flutter Community

Articles and Stories from the Flutter Community

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store