Skip to content

Commit c1c59a0

Browse files
amirhandreidiaconu
authored andcommitted
Add WebView JavaScript channels (Dart side). (flutter#1116)
Allows JavaScript code running in the WebView to send messages that will be received by the Flutter app's Dart code. To keep the PRs smaller this does not include the platform side implementations which will be sent in following PRs. Right now there are no return values on the JavaScript side, we could think of returning a JavaScript promise (or a promise equivalent) in the future, but as a first step I start without it (`evaluateJavascript` can be used to pass messages in the other direction so this shouldn't be a blocking limitation). I believe that if we end up adding a return value on the JavaScript side this could be done without making a breaking change. ### Sample usage Dart: ```dart WebView( javascriptChannels: <JavascriptChannel>[ JavascriptChannel(name: 'Print', onMessageReceived: (String msg) {print(msg); }), ].toSet(), ), ``` JavaScript (running inside the WebView): ```javascript Print.postMessage('Hello!'); ``` Fixes: flutter/flutter#24837
1 parent ad0c63e commit c1c59a0

5 files changed

Lines changed: 397 additions & 30 deletions

File tree

packages/webview_flutter/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.3.1
2+
3+
* Added JavaScript channels to facilitate message passing from JavaScript code running inside
4+
the WebView to the Flutter app's Dart code.
5+
16
## 0.3.0
27

38
* **Breaking change**. Migrate from the deprecated original Android Support

packages/webview_flutter/example/lib/main.dart

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,34 @@ class WebViewExample extends StatelessWidget {
2323
SampleMenu(_controller.future),
2424
],
2525
),
26-
body: WebView(
27-
initialUrl: 'https://flutter.io',
28-
javascriptMode: JavascriptMode.unrestricted,
29-
onWebViewCreated: (WebViewController webViewController) {
30-
_controller.complete(webViewController);
31-
},
32-
),
26+
// We're using a Builder here so we have a context that is below the Scaffold
27+
// to allow calling Scaffold.of(context) so we can show a snackbar.
28+
body: Builder(builder: (BuildContext context) {
29+
return WebView(
30+
initialUrl: 'https://flutter.io',
31+
javascriptMode: JavascriptMode.unrestricted,
32+
onWebViewCreated: (WebViewController webViewController) {
33+
_controller.complete(webViewController);
34+
},
35+
javascriptChannels: <JavascriptChannel>[
36+
_toasterJavascriptChannel(context),
37+
].toSet(),
38+
);
39+
}),
3340
floatingActionButton: favoriteButton(),
3441
);
3542
}
3643

44+
JavascriptChannel _toasterJavascriptChannel(BuildContext context) {
45+
return JavascriptChannel(
46+
name: 'Toaster',
47+
onMessageReceived: (JavascriptMessage message) {
48+
Scaffold.of(context).showSnackBar(
49+
SnackBar(content: Text(message.message)),
50+
);
51+
});
52+
}
53+
3754
Widget favoriteButton() {
3855
return FutureBuilder<WebViewController>(
3956
future: _controller.future,
@@ -44,7 +61,7 @@ class WebViewExample extends StatelessWidget {
4461
onPressed: () async {
4562
final String url = await controller.data.currentUrl();
4663
Scaffold.of(context).showSnackBar(
47-
SnackBar(content: Text("Favorited $url")),
64+
SnackBar(content: Text('Favorited $url')),
4865
);
4966
},
5067
child: const Icon(Icons.favorite),
@@ -56,7 +73,7 @@ class WebViewExample extends StatelessWidget {
5673
}
5774

5875
enum MenuOptions {
59-
evaluateJavascript,
76+
showUserAgent,
6077
toast,
6178
}
6279

@@ -73,8 +90,8 @@ class SampleMenu extends StatelessWidget {
7390
return PopupMenuButton<MenuOptions>(
7491
onSelected: (MenuOptions value) {
7592
switch (value) {
76-
case MenuOptions.evaluateJavascript:
77-
_onEvaluateJavascript(controller.data, context);
93+
case MenuOptions.showUserAgent:
94+
_onShowUserAgent(controller.data, context);
7895
break;
7996
case MenuOptions.toast:
8097
Scaffold.of(context).showSnackBar(
@@ -87,8 +104,8 @@ class SampleMenu extends StatelessWidget {
87104
},
88105
itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[
89106
PopupMenuItem<MenuOptions>(
90-
value: MenuOptions.evaluateJavascript,
91-
child: const Text('Evaluate JavaScript'),
107+
value: MenuOptions.showUserAgent,
108+
child: const Text('Show user agent'),
92109
enabled: controller.hasData,
93110
),
94111
const PopupMenuItem<MenuOptions>(
@@ -101,15 +118,12 @@ class SampleMenu extends StatelessWidget {
101118
);
102119
}
103120

104-
void _onEvaluateJavascript(
121+
void _onShowUserAgent(
105122
WebViewController controller, BuildContext context) async {
106-
final String result = await controller
107-
.evaluateJavascript("document.body.style.backgroundColor = 'red'");
108-
Scaffold.of(context).showSnackBar(
109-
SnackBar(
110-
content: Text('JavaScript evaluated, the result is: $result'),
111-
),
112-
);
123+
// Send a message with the user agent string to the Toaster JavaScript channel we registered
124+
// with the WebView.
125+
controller.evaluateJavascript(
126+
'Toaster.postMessage("User Agent: " + navigator.userAgent);');
113127
}
114128
}
115129

packages/webview_flutter/lib/webview_flutter.dart

Lines changed: 169 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,51 @@ enum JavascriptMode {
1919
unrestricted,
2020
}
2121

22+
/// A message that was sent by JavaScript code running in a [WebView].
23+
class JavascriptMessage {
24+
/// Constructs a JavaScript message object.
25+
///
26+
/// The `message` parameter must not be null.
27+
const JavascriptMessage(this.message) : assert(message != null);
28+
29+
/// The contents of the message that was sent by the JavaScript code.
30+
final String message;
31+
}
32+
33+
/// Callback type for handling messages sent from Javascript running in a web view.
34+
typedef void JavascriptMessageHandler(JavascriptMessage message);
35+
36+
final RegExp _validChannelNames = RegExp('^[a-zA-Z_][a-zA-Z0-9]*\$');
37+
38+
/// A named channel for receiving messaged from JavaScript code running inside a web view.
39+
class JavascriptChannel {
40+
/// Constructs a Javascript channel.
41+
///
42+
/// The parameters `name` and `onMessageReceived` must not be null.
43+
JavascriptChannel({
44+
@required this.name,
45+
@required this.onMessageReceived,
46+
}) : assert(name != null),
47+
assert(onMessageReceived != null),
48+
assert(_validChannelNames.hasMatch(name));
49+
50+
/// The channel's name.
51+
///
52+
/// Passing this channel object as part of a [WebView.javascriptChannels] adds a channel object to
53+
/// the Javascript window object's property named `name`.
54+
///
55+
/// The name must start with a letter or underscore(_), followed by any combination of those
56+
/// characters plus digits.
57+
///
58+
/// Note that any JavaScript existing `window` property with this name will be overriden.
59+
///
60+
/// See also [WebView.javascriptChannels] for more details on the channel registration mechanism.
61+
final String name;
62+
63+
/// A callback that's invoked when a message is received through the channel.
64+
final JavascriptMessageHandler onMessageReceived;
65+
}
66+
2267
/// A web view widget for showing html content.
2368
class WebView extends StatefulWidget {
2469
/// Creates a new web view.
@@ -32,6 +77,7 @@ class WebView extends StatefulWidget {
3277
this.onWebViewCreated,
3378
this.initialUrl,
3479
this.javascriptMode = JavascriptMode.disabled,
80+
this.javascriptChannels,
3581
this.gestureRecognizers,
3682
}) : assert(javascriptMode != null),
3783
super(key: key);
@@ -56,6 +102,35 @@ class WebView extends StatefulWidget {
56102
/// Whether Javascript execution is enabled.
57103
final JavascriptMode javascriptMode;
58104

105+
/// The set of [JavascriptChannel]s available to JavaScript code running in the web view.
106+
///
107+
/// For each [JavascriptChannel] in the set, a channel object is made available for the
108+
/// JavaScript code in a window property named [JavascriptChannel.name].
109+
/// The JavaScript code can then call `postMessage` on that object to send a message that will be
110+
/// passed to [JavascriptChannel.onMessageReceived].
111+
///
112+
/// For example for the following JavascriptChannel:
113+
///
114+
/// ```dart
115+
/// JavascriptChannel(name: 'Print', onMessageReceived: (String message) { print(message); });
116+
/// ```
117+
///
118+
/// JavaScript code can call:
119+
///
120+
/// ```javascript
121+
/// Print.postMessage('Hello');
122+
/// ```
123+
///
124+
/// To asynchronously invoke the message handler which will print the message to standard output.
125+
///
126+
/// Adding a new JavaScript channel only takes affect after the next page is loaded.
127+
///
128+
/// Set values must not be null. A [JavascriptChannel.name] cannot be the same for multiple
129+
/// channels in the list.
130+
///
131+
/// A null value is equivalent to an empty set.
132+
final Set<JavascriptChannel> javascriptChannels;
133+
59134
@override
60135
State<StatefulWidget> createState() => _WebViewState();
61136
}
@@ -103,45 +178,79 @@ class _WebViewState extends State<WebView> {
103178
'$defaultTargetPlatform is not yet supported by the webview_flutter plugin');
104179
}
105180

181+
@override
182+
void initState() {
183+
super.initState();
184+
_assertJavascriptChannelNamesAreUnique();
185+
}
186+
106187
@override
107188
void didUpdateWidget(WebView oldWidget) {
108189
super.didUpdateWidget(oldWidget);
109-
_updateSettings(_WebSettings.fromWidget(widget));
190+
_assertJavascriptChannelNamesAreUnique();
191+
_updateConfiguration(_WebSettings.fromWidget(widget));
110192
}
111193

112-
Future<void> _updateSettings(_WebSettings settings) async {
194+
Future<void> _updateConfiguration(_WebSettings settings) async {
113195
_settings = settings;
114196
final WebViewController controller = await _controller.future;
115197
controller._updateSettings(settings);
198+
controller._updateJavascriptChannels(widget.javascriptChannels);
116199
}
117200

118201
void _onPlatformViewCreated(int id) {
119-
final WebViewController controller =
120-
WebViewController._(id, _WebSettings.fromWidget(widget));
202+
final WebViewController controller = WebViewController._(
203+
id,
204+
_WebSettings.fromWidget(widget),
205+
widget.javascriptChannels,
206+
);
121207
_controller.complete(controller);
122208
if (widget.onWebViewCreated != null) {
123209
widget.onWebViewCreated(controller);
124210
}
125211
}
212+
213+
void _assertJavascriptChannelNamesAreUnique() {
214+
if (widget.javascriptChannels == null ||
215+
widget.javascriptChannels.isEmpty) {
216+
return;
217+
}
218+
assert(_extractChannelNames(widget.javascriptChannels).length ==
219+
widget.javascriptChannels.length);
220+
}
221+
}
222+
223+
Set<String> _extractChannelNames(Set<JavascriptChannel> channels) {
224+
final Set<String> channelNames = channels == null
225+
? Set<String>()
226+
: channels.map((JavascriptChannel channel) => channel.name).toSet();
227+
return channelNames;
126228
}
127229

128230
class _CreationParams {
129-
_CreationParams({this.initialUrl, this.settings});
231+
_CreationParams(
232+
{this.initialUrl, this.settings, this.javascriptChannelNames});
130233

131234
static _CreationParams fromWidget(WebView widget) {
132235
return _CreationParams(
133236
initialUrl: widget.initialUrl,
134237
settings: _WebSettings.fromWidget(widget),
238+
javascriptChannelNames:
239+
_extractChannelNames(widget.javascriptChannels).toList(),
135240
);
136241
}
137242

138243
final String initialUrl;
244+
139245
final _WebSettings settings;
140246

247+
final List<String> javascriptChannelNames;
248+
141249
Map<String, dynamic> toMap() {
142250
return <String, dynamic>{
143251
'initialUrl': initialUrl,
144252
'settings': settings.toMap(),
253+
'javascriptChannelNames': javascriptChannelNames,
145254
};
146255
}
147256
}
@@ -178,14 +287,32 @@ class _WebSettings {
178287
/// A [WebViewController] instance can be obtained by setting the [WebView.onWebViewCreated]
179288
/// callback for a [WebView] widget.
180289
class WebViewController {
181-
WebViewController._(int id, _WebSettings settings)
182-
: _channel = MethodChannel('plugins.flutter.io/webview_$id'),
183-
_settings = settings;
290+
WebViewController._(
291+
int id, this._settings, Set<JavascriptChannel> javascriptChannels)
292+
: _channel = MethodChannel('plugins.flutter.io/webview_$id') {
293+
_updateJavascriptChannelsFromSet(javascriptChannels);
294+
_channel.setMethodCallHandler(_onMethodCall);
295+
}
184296

185297
final MethodChannel _channel;
186298

187299
_WebSettings _settings;
188300

301+
// Maps a channel name to a channel.
302+
Map<String, JavascriptChannel> _javascriptChannels =
303+
<String, JavascriptChannel>{};
304+
305+
Future<void> _onMethodCall(MethodCall call) async {
306+
switch (call.method) {
307+
case 'javascriptChannelMessage':
308+
final String channel = call.arguments['channel'];
309+
final String message = call.arguments['message'];
310+
_javascriptChannels[channel]
311+
.onMessageReceived(JavascriptMessage(message));
312+
break;
313+
}
314+
}
315+
189316
/// Loads the specified URL.
190317
///
191318
/// `url` must not be null.
@@ -279,6 +406,40 @@ class WebViewController {
279406
return _channel.invokeMethod('updateSettings', updateMap);
280407
}
281408

409+
Future<void> _updateJavascriptChannels(
410+
Set<JavascriptChannel> newChannels) async {
411+
final Set<String> currentChannels = _javascriptChannels.keys.toSet();
412+
final Set<String> newChannelNames = _extractChannelNames(newChannels);
413+
final Set<String> channelsToAdd =
414+
newChannelNames.difference(currentChannels);
415+
final Set<String> channelsToRemove =
416+
currentChannels.difference(newChannelNames);
417+
if (channelsToRemove.isNotEmpty) {
418+
// TODO(amirh): remove this when the invokeMethod update makes it to stable Flutter.
419+
// https://github.com/flutter/flutter/issues/26431
420+
// ignore: strong_mode_implicit_dynamic_method
421+
_channel.invokeMethod(
422+
'removeJavascriptChannels', channelsToRemove.toList());
423+
}
424+
if (channelsToAdd.isNotEmpty) {
425+
// TODO(amirh): remove this when the invokeMethod update makes it to stable Flutter.
426+
// https://github.com/flutter/flutter/issues/26431
427+
// ignore: strong_mode_implicit_dynamic_method
428+
_channel.invokeMethod('addJavascriptChannels', channelsToAdd.toList());
429+
}
430+
_updateJavascriptChannelsFromSet(newChannels);
431+
}
432+
433+
void _updateJavascriptChannelsFromSet(Set<JavascriptChannel> channels) {
434+
_javascriptChannels.clear();
435+
if (channels == null) {
436+
return;
437+
}
438+
for (JavascriptChannel channel in channels) {
439+
_javascriptChannels[channel.name] = channel;
440+
}
441+
}
442+
282443
/// Evaluates a JavaScript expression in the context of the current page.
283444
///
284445
/// On Android returns the evaluation result as a JSON formatted string.

packages/webview_flutter/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: webview_flutter
22
description: A Flutter plugin that provides a WebView widget on Android and iOS.
3-
version: 0.3.0
3+
version: 0.3.1
44
author: Flutter Team <flutter-dev@googlegroups.com>
55
homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter
66

0 commit comments

Comments
 (0)