Flutter WebView JavaScript Communication — InAppWebView 5

Lorenzo Pichilli
Flutter Community
Published in
8 min readApr 19, 2021
Flutter InAppWebView JavaScript 2-way Communication.

In this in-depth tutorial, I’m going to explain how you can communicate from Dart (Flutter WebView) to JavaScript and vice-versa using my flutter_inappwebview plugin (at the time of this writing, the latest release is 5.3.2).

There are 3 main ways to enable this 2-way communication:

  • JavaScript Handlers;
  • Web Message Channels;
  • Web Message Listeners.

JavaScript Handlers

The JavaScript Handler concept is similar to the native Android WebView JavaScript Interface and the iOS WKWebView JavaScript Message Handler ones, but it offers a cross-platform way of it.

Android and iOS together toward the cross-platform way.

For each JavaScript Handler, you can define its name and a callback that will be invoked when it is called by the JavaScript side. Also, the callback can return data to the JavaScript side as a Promise type.

To add a JavaScript Handler, you can use the InAppWebViewController.addJavaScriptHandler method. If you need to manage JavaScript Handlers as soon as the web page is loaded, this method should be called when the InAppWebView is created, like this:

onWebViewCreated: (controller) {
// register a JavaScript handler with name "myHandlerName"
controller.addJavaScriptHandler(handlerName: 'myHandlerName', callback: (args) {
// print arguments coming from the JavaScript side!
print(args);
// return data to the JavaScript side!
return {
'bar': 'bar_value', 'baz': 'baz_value'
};
});
},

The return type of the callback will be automatically JSON encoded using thejsonEncode function of the dart:convert library. So, you can send back to JavaScript all JSON encodable types.

Instead, on the JavaScript side, to execute the callback handler and send data to Flutter, you need to use the window.flutter_inappwebview.callHandler(handlerName, ...args) method, where handlerName is a string that represents the handler name that you are calling and args are optional arguments that you can send to the Flutter side.

Note that if you want a different name instead of window.flutter_inappwebview, you can simply wrap it inside another JavaScript function or object. For example: window.myCustomObj = { callHandler: window.flutter_inappwebview.callHandler }; and, then, you can use window.myCustomObj.callHandler in the same way.

Also, you can wrap a whole specific handler this way:

const myHandlerName = function(...args) {
return window.flutter_inappwebview.callHandler('myHandlerName', ...args);
};
// and then use it
myHandlerName();

In order to call window.flutter_inappwebview.callHandler properly inside a JavaScript file or a <script> HTML tag, you need to wait and listen to the flutterInAppWebViewPlatformReady JavaScript event. This event will be dispatched as soon as the platform is ready to handle the callHandler method. You can also use a global flag variable that is set when the flutterInAppWebViewPlatformReady event is dispatch and use it before calling the window.flutter_inappwebview.callHandler method.

Here is a JavaScript example code:

// execute inside the "flutterInAppWebViewPlatformReady" event listener
window.addEventListener("flutterInAppWebViewPlatformReady", function(event) {
const args = [1, true, ['bar', 5], {foo: 'baz'}];
window.flutter_inappwebview.callHandler('myHandlerName', ...args);
});
// or using a global flag variable
var isFlutterInAppWebViewReady = false;
window.addEventListener("flutterInAppWebViewPlatformReady", function(event) {
isFlutterInAppWebViewReady = true;
});
// then, somewhere in your code
if (isFlutterInAppWebViewReady) {
const args = [1, true, ['bar', 5], {foo: 'baz'}];
window.flutter_inappwebview.callHandler('myHandlerName', ...args);
}

Instead, if you are evaluating JavaScript code from the Dart (Flutter) side, you can call window.flutter_inappwebview.callHandler directly on the onLoadStop event (or after that) without listening to the flutterInAppWebViewPlatformReady JavaScript event because, at that time, it will be already fired. For example:

onLoadStop: (controller, url) async {
await controller.evaluateJavascript(source: """
const args = [1, true, ['bar', 5], {foo: 'baz'}];
window.flutter_inappwebview.callHandler('myHandlerName', ...args);
""");
},

Here is an example of communication:

Another way to communicate with JavaScript is to evaluate some JavaScript code using, for example, the InAppWebViewController.evaluateJavascript method.

You could set up a message event listener (used with postMessage) or a custom event listener (see CustomEvent for details), such as:

// message event listener
window.addEventListener("message", (event) => {
console.log(event.data);
}, false);
// or custom event listener
window.addEventListener("myCustomEvent", (event) => {
console.log(event.detail);
}, false);

And then, you can dispatch the custom JavaScript event whenever and wherever you want:

// using postMessage method
window.postMessage({foo: 1, bar: false});
// or dispatching a custom event
const event = new CustomEvent("myCustomEvent", {
detail: {foo: 1, bar: false}
});
window.dispatchEvent(event);

So, you can set up these event listeners at runtime using the InAppWebViewController.evaluateJavascript method or inside the web app itself and dispatch these events using the same method, for example:

onLoadStop: (controller, url) async {
await controller.evaluateJavascript(source: """
window.addEventListener("myCustomEvent", (event) => {
console.log(JSON.stringify(event.detail));
}, false);
""");
await Future.delayed(Duration(seconds: 5)); controller.evaluateJavascript(source: """
const event = new CustomEvent("myCustomEvent", {
detail: {foo: 1, bar: false}
});
window.dispatchEvent(event);
""");
},
onConsoleMessage: (controller, consoleMessage) {
print(consoleMessage);
// it will print: {message: {"foo":1,"bar":false}, messageLevel: 1}
},

Web Message Channels

Another way is to use Web Message Channels, which are the representation of the HTML5 message channels. See Channel Messaging API for more details.

It allows you to create a new message channel and send data through it via its two WebMessagePort properties:

  • port1, that is the first WebMessagePort;
  • port2, that is the second WebMessagePort.
It’s all about connection. Yep.

To create a Web Message Channel, you need to use the InAppWebViewController.createWebMessageChannel method (the official native Android WebView API can be found here). This method should be called when the page is loaded, for example, when the onLoadStop event is fired, otherwise, the WebMessageChannel won’t work.

Note for Android: This method should only be called if AndroidWebViewFeature.isFeatureSupported returns true for AndroidWebViewFeature.CREATE_WEB_MESSAGE_CHANNEL.

Here is an example of it:

onLoadStop: (controller, url) async {
if (!Platform.isAndroid || await AndroidWebViewFeature.isFeatureSupported(AndroidWebViewFeature.CREATE_WEB_MESSAGE_CHANNEL)) {
var webMessageChannel = await controller.createWebMessageChannel();
var port1 = webMessageChannel!.port1;
var port2 = webMessageChannel.port2;
}
},

The Dart side that created the channel uses port1, and the JavaScript side at the other end of the port uses port2 — you send a message to port2, and transfer the port over to the other browsing context using the InAppWebViewController.postWebMessage method along with the message to send, and the object to transfer ownership of, in this case, the port itself.

When these transferable port objects are transferred, they are “neutered” on the previous context — the one they previously belonged to. For instance, a port, when is sent to JavaScript, cannot be used to send or receive messages at the Dart side anymore. Also, different from HTML5 Spec, a port cannot be transferred if one of these has ever happened:

  • a message callback was set;
  • a message was posted on it.

I know what you are thinking: “blah blah blah.. come on, let me see the code!”. Don’t worry, we are almost there!

A transferred port cannot be closed by the application, since the ownership is also transferred.

To listen for the messages on a port from the Dart side, you need to set the WebMessageCallback using the WebMessagePort.setWebMessageCallback method. You could then respond by sending a message back to the original document using the WebMessagePort.postMessage method.

When you want to stop sending messages down the channel, you can invoke WebMessagePort.close to close the ports. A closed port cannot be transferred or cannot be reopened to send messages.

To be able to listen to messages from the JavaScript side, you need to first “capture” the port coming from the Dart side.

Here is finally the expected example of communication for you:

Web Message Listeners

Web Message Listeners are similar to the JavaScript Handlers and Web Message Channels. It allows injecting a JavaScript object into each frame that the WebMessageListener will listen on.

Example of a Web Message Listener.

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 onWebViewCreated event is fired.

Note for Android: Similar to the Web Message Channel, this method should only be called if AndroidWebViewFeature.isFeatureSupported returns true for AndroidWebViewFeature.WEB_MESSAGE_LISTENER.

child: InAppWebView(
onWebViewCreated: (controller) async {
// add first all of your web message listeners
if (!Platform.isAndroid || await AndroidWebViewFeature.isFeatureSupported(AndroidWebViewFeature.WEB_MESSAGE_LISTENER)) {
await controller.addWebMessageListener(WebMessageListener(
jsObjectName: "myObject",
allowedOriginRules: Set.from(["https://*.example.com"]),
onPostMessage: (message, sourceOrigin, isMainFrame, replyProxy) {
replyProxy.postMessage("Got it!");
},
));
}

// then load your URL
await controller.loadUrl(urlRequest: URLRequest(url: Uri.parse("https://www.example.com")));
},
),

The injected JavaScript object will be named WebMessageListener.jsObjectName in the global scope. This will inject the JavaScript object in any frame whose origin matches WebMessageListener.allowedOriginRules for every navigation after this call, and the JavaScript object will be available immediately when the page begins to load.

Each WebMessageListener.allowedOriginRules entry must follow the format SCHEME "://" [ HOSTNAME_PATTERN [ ":" PORT ] ], each part is explained in this table.

The HTTPS scheme is strongly recommended for security because the JavaScript object will be injected when the frame’s origin matches any one of the allowed origins. Allowing HTTP origins exposes the injected object to any potential network-based attackers.

If a wildcard “*” is provided, it will inject the JavaScript object into all frames. A wildcard should only be used if the app wants any third-party web page to be able to use the injected object. When using a wildcard, the app must treat received messages as untrustworthy and validate any data carefully.

You can call this method multiple times to inject multiple JavaScript objects!

Each injected JavaScript object will have the following methods/properties:

  • postMessage(message[, MessagePorts]) on Android and postMessage(message) on iOS, where the message is a String;
  • onmessage: To receive messages posted from the Flutter App side, assign a function to this property. This function should accept a single "event" argument (onmessage = function(event) { ... }). "event" has a "data" property, which is the message string from the Flutter App side;
  • addEventListener(type, listener): To be compatible with DOM EventTarget's addEventListener, it accepts type and listener parameters where type can be only "message" type and listener can only be a JavaScript function that has the same functionality of the onmessage property function;
  • removeEventListener(type, listener): To be compatible with DOM EventTarget's removeEventListener, it accepts type and listener parameters. It is used to remove an event listener added using the addEventListener method.

The communication must be initialized on the JavaScript side first, posting a message, so the Flutter App will have a JavaScriptReplyProxy object to respond.

JavaScript side:

// Web page (in JavaScript)
myObject.onmessage = function(event) {
// prints "Got it!" when we receive the app's response.
console.log(event.data);
}
myObject.postMessage("I'm ready!");

Flutter side:

// Flutter App
child: InAppWebView(
onWebViewCreated: (controller) async {
if (!Platform.isAndroid || await AndroidWebViewFeature.isFeatureSupported(AndroidWebViewFeature.WEB_MESSAGE_LISTENER)) {
await controller.addWebMessageListener(WebMessageListener(
jsObjectName: "myObject",
onPostMessage: (message, sourceOrigin, isMainFrame, replyProxy) {
// do something about message, sourceOrigin and isMainFrame.
replyProxy.postMessage("Got it!");
},
));
}
await controller.loadUrl(urlRequest: URLRequest(url: Uri.parse("https://www.example.com")));
},
),

Conclusion

Now, after this tutorial, you can start experimenting with the different 2-way communication methods and choose which one is best for you and fits your needs.

That’s all for today!

As always, thanks to all people that are supporting this project! 💙

--

--

Lorenzo Pichilli
Flutter Community

I’m a Software Engineer mostly focused on Web (FullStack) and Mobile Development. JavaScript, TypeScript & Flutter enthusiast 💙.