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.
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.
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.
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
.
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 firstWebMessagePort
;port2
, that is the secondWebMessagePort
.
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;
}
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")));
},
),
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:
Rule | Description | Example |
---|---|---|
http/https with hostname |
|
|
http/https with pattern matching |
|
|
http/https with IP literal |
|
|
Custom scheme |
|
|
* | 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")));
},
),
Did you find it useful? Consider making a donation to support this project and leave a star on GitHub . Your support is really appreciated!