Skip to main content
Version: 6.x.x

Communication

You can communicate with JavaScript and vice-versa. There 3 ways to communicate with JavaScript:

JavaScript Handlers

Supported Platforms: AndroidiOSmacOSWindows

To add a JavaScript handler, you can use InAppWebViewController.addJavaScriptHandler method, where you define the handlerName and a callback to be invoked when it is called by the JavaScript side. The callback can return data to be sent on the JavaScript side. If you need to manage JavaScript handlers as soon as the web page is loaded, InAppWebViewController.addJavaScriptHandler should be called when the InAppWebView is created.

info

Starting from plugin version >= 6.2.0-beta.1, you should use the function callback type JavaScriptHandlerFunction, specifying the callback parameter with the type JavaScriptHandlerFunctionData to get other info other than only the arguments, as the function callback type JavaScriptHandlerCallback is now deprecated but still works.

Without forcing the JavaScriptHandlerFunctionData parameter type, you will end up using the deprecated function callback type JavaScriptHandlerCallback.

Here is an example of how to register a JavaScript handler:

onWebViewCreated: (controller) {
// register a JavaScript handler with name "myHandlerName"
controller.addJavaScriptHandler(handlerName: 'myHandlerName', callback: (JavaScriptHandlerFunctionData data) {
// print arguments and other info coming from the JavaScript side!
print(data);

// return data to the JavaScript side!
return {
'bar': 'bar_value', 'baz': 'baz_value'
};
});
},

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.

info

The default JavaScript Bridge object name is flutter_inappwebview, available in the window object.

If you want to use a custom name, starting from plugin version >= 6.2.0-beta.1, you can use the await InAppWebViewController.setJavaScriptBridgeName(bridgeName) static method. This method should be called before any WebViews are created or when there are no WebViews. Calling this method after a WebView has been created will not change the current JavaScript Bridge object name and could lead to errors. Also, the bridgeName must be a non-empty string with only alphanumeric and underscore characters and it can't start with a number. It should be a valid JavaScript variable name.

To get the current JavaScript Bridge object name, you can use the await InAppWebViewController.getJavaScriptBridgeName() static method.

On Android, if the WebView doesn't support the feature WebViewFeature.DOCUMENT_START_SCRIPT (which you can check using await WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)), then 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.

// 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 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);
""");
},

window.flutter_inappwebview.callHandler returns a JavaScript Promise that can be used to get the json result returned by the callback. In this case, simply return data that you want to send and it will be automatically json encoded using jsonEncode from the dart:convert library.

Here is an example of communication:

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';

WebViewEnvironment? webViewEnvironment;

Future main() async {
WidgetsFlutterBinding.ensureInitialized();

if (!kIsWeb && defaultTargetPlatform == TargetPlatform.windows) {
final availableVersion = await WebViewEnvironment.getAvailableVersion();
assert(availableVersion != null,
'Failed to find an installed WebView2 Runtime or non-stable Microsoft Edge installation.');

webViewEnvironment = await WebViewEnvironment.create(
settings:
WebViewEnvironmentSettings(userDataFolder: 'YOUR_CUSTOM_PATH'));
}

if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
await InAppWebViewController.setWebContentsDebuggingEnabled(kDebugMode);
}

runApp(new MyApp());
}

class MyApp extends StatefulWidget {

_MyAppState createState() => new _MyAppState();
}

class _MyAppState extends State<MyApp> {
InAppWebViewSettings settings = InAppWebViewSettings(
isInspectable: kDebugMode,
mediaPlaybackRequiresUserGesture: false,
allowsInlineMediaPlayback: true,
iframeAllow: "camera; microphone",
iframeAllowFullscreen: true);


Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text("JavaScript Handlers")),
body: SafeArea(
child: Column(children: <Widget>[
Expanded(
child: InAppWebView(
webViewEnvironment: webViewEnvironment,
initialData: InAppWebViewInitialData(data: """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
</head>
<body>
<h1>JavaScript Handlers</h1>
<script>
window.addEventListener("flutterInAppWebViewPlatformReady", function(event) {
window.flutter_inappwebview.callHandler('handlerFoo')
.then(function(result) {
// print to the console the data coming
// from the Flutter side.
console.log(JSON.stringify(result));

window.flutter_inappwebview
.callHandler('handlerFooWithArgs', 1, true, ['bar', 5], {foo: 'baz'}, result);
});
});
</script>
</body>
</html>
"""),
initialSettings: settings,
onWebViewCreated: (controller) {
controller.addJavaScriptHandler(
handlerName: 'handlerFoo',
callback: (JavaScriptHandlerFunctionData data) {
// return data to the JavaScript side!
return {'bar': 'bar_value', 'baz': 'baz_value'};
});

controller.addJavaScriptHandler(
handlerName: 'handlerFooWithArgs',
callback: (JavaScriptHandlerFunctionData data) {
print(data);
// it will print:
// JavaScriptHandlerFunctionData{args: [1, true, [bar, 5], {foo: baz}, {bar: bar_value, baz: baz_value}], isMainFrame: true, origin: , requestUrl: about:blank}
});
},
onConsoleMessage: (controller, consoleMessage) {
print(consoleMessage);
// it will print: {message: {"bar":"bar_value","baz":"baz_value"}, messageLevel: 1}
},
),
),
]))),
);
}
}

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}
},

JavaScript Handlers Security

Starting from plugin version >= 6.2.0-beta.1, you can restrict execution of all the JavaScript Handlers using the InAppWebViewSettings.javaScriptHandlersForMainFrameOnly and InAppWebViewSettings.javaScriptHandlersOriginAllowList parameters.

If InAppWebViewSettings.javaScriptHandlersForMainFrameOnly is set to true, then it will allow all the JavaScript Handlers only on the main frame. This will affect also the internal JavaScript Handlers used by the plugin itself. The default value is false.

InAppWebViewSettings.javaScriptBridgeOriginAllowList represents a Set of Regular Expression Patterns that will be used on native side to match the allowed origins that are able to execute all the JavaScript Handlers defined for the current WebView. This will affect also the internal JavaScript Handlers used by the plugin itself. An empty Set will block every origin. The default value is null and will allow every origin.

Also, starting from plugin version >= 6.2.0-beta.1, you should use the function callback type JavaScriptHandlerFunction, specifying the callback parameter with the type JavaScriptHandlerFunctionData, when adding a new JavaScript Handler. Without forcing the JavaScriptHandlerFunctionData parameter type, you will end up using the deprecated function callback type JavaScriptHandlerCallback. The JavaScriptHandlerFunctionData contains info about the origin and the full url of the frame that made the request and if the request is coming from the main frame or a sub-frame. This way, you can apply specific validity logic for security reasons.

As an example:

onWebViewCreated: (controller) {
// register a JavaScript handler with name "myHandlerName"
controller.addJavaScriptHandler(handlerName: 'myHandlerName', callback: (JavaScriptHandlerFunctionData data) {
// validity check
if (!data.isMainFrame || data.origin.host != 'validhost.com') {
throw Exception('Invalid host');
}

// print arguments and other info coming from the JavaScript side!
print(data);

// return data to the JavaScript side!
return {
'bar': 'bar_value', 'baz': 'baz_value'
};
});
},

JavaScript Bridge Security

Starting from plugin version >= 6.2.0-beta.1, you can enable or disable the JavaScript Bridge feature using InAppWebViewSettings.javaScriptBridgeEnabled.

The default value is true. Setting it to false will disable the JavaScript Bridge completely. This will affect also all the internal plugin UserScripts that are using the JavaScript Bridge to work.

info

Setting or changing this value after the WebView has been created won't have any effect. It should be set when initializing the WebView through PlatformWebViewCreationParams.initialSettings parameter.

You can also restrict access to it using InAppWebViewSettings.javaScriptBridgeForMainFrameOnly and InAppWebViewSettings.javaScriptBridgeOriginAllowList.

If InAppWebViewSettings.javaScriptBridgeForMainFrameOnly is set to true, then it will allow the JavaScript Bridge only on the main frame. The default value is false.

InAppWebViewSettings.javaScriptBridgeOriginAllowList represents a Set of patterns that will be used to match the allowed origins where the JavaScript Bridge could be used. Adding '*' as an allowed origin or setting this to null, it means it will allow every origin. Instead, an empty Set will block every origin and, in this case, it will force the behaviour of the InAppWebViewSettings.javaScriptBridgeEnabled parameter, as it was set to false.

info

Setting or changing these both values after the WebView has been created won't have any effect. It should be set when initializing the WebView through PlatformWebViewCreationParams.initialSettings parameter.

NOTE for Android: each origin pattern MUST follow the table rule of PlatformInAppWebViewController.addWebMessageListener.

NOTE for iOS, macOS, Windows: each origin pattern will be used as a Regular Expression Pattern that will be used on JavaScript side using RegExp.

Web Message Channels

Supported Platforms: AndroidiOSmacOS

Web Message Channels 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.

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, otherwise, the WebMessageChannel won't work.

if (defaultTargetPlatform != TargetPlatform.android || await WebViewFeature.isFeatureSupported(WebViewFeature.CREATE_WEB_MESSAGE_CHANNEL)) {
var webMessageChannel = await controller.createWebMessageChannel();
var port1 = webMessageChannel!.port1;
var port2 = webMessageChannel.port2;
}
Note for Android

This method should only be called if WebViewFeature.isFeatureSupported returns true for WebViewFeature.CREATE_WEB_MESSAGE_CHANNEL.

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

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 an example of communication:

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';

Future main() async {
WidgetsFlutterBinding.ensureInitialized();

if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
await InAppWebViewController.setWebContentsDebuggingEnabled(true);
}

runApp(new MyApp());
}

class MyApp extends StatefulWidget {

_MyAppState createState() => new _MyAppState();
}

class _MyAppState extends State<MyApp> {
InAppWebViewSettings settings = InAppWebViewSettings();


Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text("Web Message Channels")),
body: SafeArea(
child: Column(children: <Widget>[
Expanded(
child: InAppWebView(
initialData: InAppWebViewInitialData(data: """
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebMessageChannel Test</title>
</head>
<body>
<!-- when you click this button, it will send a message to the Dart side -->
<button id="button" onclick="port.postMessage(input.value);" />Send</button>
<br />
<input id="input" type="text" value="JavaScript To Native" />

<script>
// variable that will represents the port used to communicate with the Dart side
var port;
// listen for messages
window.addEventListener('message', function(event) {
if (event.data == 'capturePort') {
// capture port2 coming from the Dart side
if (event.ports[0] != null) {
// the port is ready for communication,
// so you can use port.postMessage(message); wherever you want
port = event.ports[0];
// To listen to messages coming from the Dart side, set the onmessage event listener
port.onmessage = function (event) {
// event.data contains the message data coming from the Dart side
console.log(event.data);
};
}
}
}, false);
</script>
</body>
</html>
"""),
initialSettings: settings,
onConsoleMessage: (controller, consoleMessage) {
print(
"Message coming from the Dart side: ${consoleMessage.message}");
},
onLoadStop: (controller, url) async {
if (defaultTargetPlatform != TargetPlatform.android ||
await WebViewFeature.isFeatureSupported(
WebViewFeature.CREATE_WEB_MESSAGE_CHANNEL)) {
// wait until the page is loaded, and then create the Web Message Channel
var webMessageChannel =
await controller.createWebMessageChannel();
var port1 = webMessageChannel!.port1;
var port2 = webMessageChannel.port2;

// set the web message callback for the port1
await port1.setWebMessageCallback((message) async {
print(
"Message coming from the JavaScript side: $message");
// when it receives a message from the JavaScript side, respond back with another message.
await port1.postMessage(
WebMessage(data: message! + " and back"));
});

// transfer port2 to the webpage to initialize the communication
await controller.postWebMessage(
message:
WebMessage(data: "capturePort", ports: [port2]),
targetOrigin: WebUri("*"));
}
},
),
),
]))),
);
}
}

Web Message Listeners

Supported Platforms: AndroidiOSmacOS

Web Message Listeners are similar to the JavaScript Handlers and Web Message Channels. 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:

child: InAppWebView(
onWebViewCreated: (controller) async {
// add first all of your web message listeners
if (defaultTargetPlatform != TargetPlatform.android || await WebViewFeature.isFeatureSupported(WebViewFeature.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: WebUri("https://www.example.com")));
},
),
Note for Android

This method should only be called if WebViewFeature.isFeatureSupported returns true for WebViewFeature.WEB_MESSAGE_LISTENER.

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 the below table:

RuleDescriptionExample
http/https with hostname

SCHEME is http or https; HOSTNAME_<wbr>PATTERN is a regular hostname; PORT is optional, when not present, the rule will match port 80 for http and port 443 for https.

http/https with pattern matching

SCHEME is http or https; HOSTNAME_<wbr>PATTERN is a sub-domain matching pattern with a leading *.<wbr>; PORT is optional, when not present, the rule will match port 80 for http and port 443 for https.

http/https with IP literal

SCHEME is https or https; HOSTNAME_<wbr>PATTERN is IP literal; PORT is optional, when not present, the rule will match port 80 for http and port 443 for https.

  • https://127.0.0.1 - Matches https:// URL on port 443, whose IPv4 address is 127.0.0.1
  • https://[::1] or https://[0:0::1]- Matches any URL to the IPv6 loopback address with port 443.
  • https://[::1]:99 - Matches any https:// URL to the IPv6 loopback on port 99.
Custom scheme

SCHEME is a custom scheme; HOSTNAME_<wbr>PATTERN and PORT must not be present.

  • my-app-scheme:// - Matches any my-app-scheme:// URL.
*Wildcard rule, matches any origin.
  • *

Note that this is a powerful API, as the JavaScript object will be injected when the frame's origin matches any one of the allowed origins. The HTTPS scheme is strongly recommended for security; allowing HTTP origins exposes the injected object to any potential network-based attackers. If a wildcard "*" is provided, it will inject the JavaScript object to 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.

This method can be called multiple times to inject multiple JavaScript objects.

Let's say the injected JavaScript object is named myObject. We will have following methods on that object once it is available to use:

// Web page (in JavaScript)
// message needs to be a JavaScript String, MessagePorts is an optional parameter.
myObject.postMessage(message[, MessagePorts]) // on Android
myObject.postMessage(message) // on iOS

// To receive messages posted from the app side, assign a function to the "onmessage"
// property. This function should accept a single "event" argument. "event" has a "data"
// property, which is the message string from the app side.
myObject.onmessage = function(event) { ... }

// 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 for myObject. An event object will be passed to listener with a "data" property,
// which is the message string from the app side.
myObject.addEventListener(type, listener)

// To be compatible with DOM EventTarget's removeEventListener, it accepts type and listener
// parameters, where type can be only "message" type and listener can only be a JavaScript
// function for myObject.
myObject.removeEventListener(type, listener)

We start the communication between JavaScript and the app from the JavaScript side. In order to send message from the app to JavaScript, it needs to post a message from JavaScript first, so the app will have a JavaScriptReplyProxy object to respond. Example:

// 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 App
child: InAppWebView(
onWebViewCreated: (controller) async {
if (defaultTargetPlatform != TargetPlatform.android || await WebViewFeature.isFeatureSupported(WebViewFeature.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: WebUri("https://www.example.com")));
},
),