From 0e16deb085a91611f7290efc4ea2bea41deb828b Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Thu, 24 Jan 2019 16:06:14 -0800 Subject: [PATCH 01/11] Add WebView JavaScript channels (Dart side only). --- .../webview_flutter/lib/webview_flutter.dart | 151 ++++++++++++++++-- .../test/webview_flutter_test.dart | 131 ++++++++++++--- 2 files changed, 252 insertions(+), 30 deletions(-) diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index 43f88b6892e4..ccc2a438df1c 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -19,6 +19,32 @@ enum JavascriptMode { unrestricted, } +/// Callback type for handling messages sent from Javascript running in a web view. +typedef void JavascriptMessageHandler(String message); + +/// A named channel for receiving messaged from JavaScript code running inside a web view. +class JavascriptChannel { + /// Constructs a Javascript channel. + /// + /// The parameters `name` and `onMessageReceived` must not be null. + JavascriptChannel({ + @required this.name, + @required this.onMessageReceived, + }); + + /// The channel's name. + /// + /// + /// Passing this channel object as part of a [WebView.javascriptChannels] adds a channel object to + /// the Javascript window object's property named `name`. + /// + /// See also [WebView.javascriptChannels] for more details on the channel registration mechanism. + final String name; + + /// A callback that's invoked when a message is received through the channel. + final JavascriptMessageHandler onMessageReceived; +} + /// A web view widget for showing html content. class WebView extends StatefulWidget { /// Creates a new web view. @@ -32,6 +58,7 @@ class WebView extends StatefulWidget { this.onWebViewCreated, this.initialUrl, this.javascriptMode = JavascriptMode.disabled, + this.javascriptChannels, this.gestureRecognizers, }) : assert(javascriptMode != null), super(key: key); @@ -56,13 +83,39 @@ class WebView extends StatefulWidget { /// Whether Javascript execution is enabled. final JavascriptMode javascriptMode; + /// The set of [JavascriptChannel]s available to JavaScript code running in the web view. + /// + /// For each [JavascriptChannel] in the set, a channel object is made available for the + /// JavaScript code in a window property named [JavascriptChannel.name]. + /// The JavaScript code can then call `postMessage` on that object to send a message that will be + /// passed to [JavascriptChannel.onMessageReceived]. + /// + /// For example for the following JavascriptChannel: + /// + /// ```dart + /// JavascriptChannel(name: 'Print', onMessageReceived: (String message) { print(message); }); + /// ``` + /// + /// JavaScript code can call: + /// + /// ```javascript + /// Print.postMessage('Hello'); + /// ``` + /// + /// To asynchronously invoke the message handler which will print the message to standard output. + /// + /// Set values must not be null. A [JavascriptChannel.name] cannot be the same for multiple + /// channels in the list. + /// + /// A null value is equivalent to an empty set. + final Set javascriptChannels; + @override State createState() => _WebViewState(); } class _WebViewState extends State { - final Completer _controller = - Completer(); + final Completer _controller = Completer(); _WebSettings _settings; @@ -99,49 +152,78 @@ class _WebViewState extends State { creationParamsCodec: const StandardMessageCodec(), ); } - return Text( - '$defaultTargetPlatform is not yet supported by the webview_flutter plugin'); + return Text('$defaultTargetPlatform is not yet supported by the webview_flutter plugin'); + } + + @override + void initState() { + super.initState(); + _assertJavascriptChannelNamesAreUnique(); } @override void didUpdateWidget(WebView oldWidget) { super.didUpdateWidget(oldWidget); - _updateSettings(_WebSettings.fromWidget(widget)); + _assertJavascriptChannelNamesAreUnique(); + _updateConfiguration(_WebSettings.fromWidget(widget)); } - Future _updateSettings(_WebSettings settings) async { + Future _updateConfiguration(_WebSettings settings) async { _settings = settings; final WebViewController controller = await _controller.future; controller._updateSettings(settings); + controller._updateJavascriptChannels(widget.javascriptChannels); } void _onPlatformViewCreated(int id) { - final WebViewController controller = - WebViewController._(id, _WebSettings.fromWidget(widget)); + final WebViewController controller = WebViewController._( + id, + _WebSettings.fromWidget(widget), + widget.javascriptChannels, + ); _controller.complete(controller); if (widget.onWebViewCreated != null) { widget.onWebViewCreated(controller); } } + + void _assertJavascriptChannelNamesAreUnique() { + if (widget.javascriptChannels == null || widget.javascriptChannels.isEmpty) { + return; + } + assert(_extractChannelNames(widget.javascriptChannels).length == widget.javascriptChannels.length); + } +} + +Set _extractChannelNames(Set channels) { + final Set channelNames = channels == null + ? Set() + : channels.map((JavascriptChannel channel) => channel.name).toSet(); + return channelNames; } class _CreationParams { - _CreationParams({this.initialUrl, this.settings}); + _CreationParams({this.initialUrl, this.settings, this.javascriptChannelNames}); static _CreationParams fromWidget(WebView widget) { return _CreationParams( initialUrl: widget.initialUrl, settings: _WebSettings.fromWidget(widget), + javascriptChannelNames: _extractChannelNames(widget.javascriptChannels).toList(), ); } final String initialUrl; + final _WebSettings settings; + final List javascriptChannelNames; + Map toMap() { return { 'initialUrl': initialUrl, 'settings': settings.toMap(), + 'javascriptChannelNames': javascriptChannelNames, }; } } @@ -178,14 +260,29 @@ class _WebSettings { /// A [WebViewController] instance can be obtained by setting the [WebView.onWebViewCreated] /// callback for a [WebView] widget. class WebViewController { - WebViewController._(int id, _WebSettings settings) - : _channel = MethodChannel('plugins.flutter.io/webview_$id'), - _settings = settings; + WebViewController._(int id, this._settings, Set javascriptChannels) + : _channel = MethodChannel('plugins.flutter.io/webview_$id') { + _updateJavascriptChannelsFromSet(javascriptChannels); + _channel.setMethodCallHandler(_onMethodCall); + } final MethodChannel _channel; _WebSettings _settings; + // Maps a channel name to a channel. + Map _javascriptChannels = {}; + + Future _onMethodCall(MethodCall call) async { + switch(call.method) { + case 'javascriptChannelMessage': + final String channel = call.arguments['channel']; + final String message = call.arguments['message']; + _javascriptChannels[channel].onMessageReceived(message); + break; + } + } + /// Loads the specified URL. /// /// `url` must not be null. @@ -279,6 +376,36 @@ class WebViewController { return _channel.invokeMethod('updateSettings', updateMap); } + Future _updateJavascriptChannels(Set newChannels) async { + final Set currentChannels = _javascriptChannels.keys.toSet(); + final Set newChannelNames = _extractChannelNames(newChannels); + final Set channelsToAdd = newChannelNames.difference(currentChannels); + final Set channelsToRemove = currentChannels.difference(newChannelNames); + if (channelsToRemove.isNotEmpty) { + // TODO(amirh): remove this when the invokeMethod update makes it to stable Flutter. + // https://github.com/flutter/flutter/issues/26431 + // ignore: strong_mode_implicit_dynamic_method + _channel.invokeMethod('removeJavascriptChannels', channelsToRemove.toList()); + } + if (channelsToAdd.isNotEmpty) { + // TODO(amirh): remove this when the invokeMethod update makes it to stable Flutter. + // https://github.com/flutter/flutter/issues/26431 + // ignore: strong_mode_implicit_dynamic_method + _channel.invokeMethod('addJavascriptChannels', channelsToAdd.toList()); + } + _updateJavascriptChannelsFromSet(newChannels); + } + + void _updateJavascriptChannelsFromSet(Set channels) { + if (channels == null) { + return; + } + _javascriptChannels.clear(); + for (JavascriptChannel channel in channels) { + _javascriptChannels[channel.name] = channel; + } + } + /// Evaluates a JavaScript expression in the context of the current page. /// /// On Android returns the evaluation result as a JSON formatted string. diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart index ae286bf5118e..ccd9546f6aba 100644 --- a/packages/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/test/webview_flutter_test.dart @@ -10,12 +10,11 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:webview_flutter/webview_flutter.dart'; void main() { - final _FakePlatformViewsController fakePlatformViewsController = - _FakePlatformViewsController(); + final _FakePlatformViewsController fakePlatformViewsController = _FakePlatformViewsController(); setUpAll(() { - SystemChannels.platform_views.setMockMethodCallHandler( - fakePlatformViewsController.fakePlatformViewsMethodHandler); + SystemChannels.platform_views + .setMockMethodCallHandler(fakePlatformViewsController.fakePlatformViewsMethodHandler); }); setUp(() { @@ -46,8 +45,7 @@ void main() { javascriptMode: JavascriptMode.unrestricted, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + final FakePlatformWebView platformWebView = fakePlatformViewsController.lastCreatedView; expect(platformWebView.javascriptMode, JavascriptMode.unrestricted); @@ -99,8 +97,7 @@ void main() { expect(await controller.currentUrl(), isNull); }); - testWidgets("Can't go back before loading a page", - (WidgetTester tester) async { + testWidgets("Can't go back before loading a page", (WidgetTester tester) async { WebViewController controller; await tester.pumpWidget( WebView( @@ -153,8 +150,7 @@ void main() { expect(canGoBackSecondPageLoaded, true); }); - testWidgets("Can't go forward before loading a page", - (WidgetTester tester) async { + testWidgets("Can't go forward before loading a page", (WidgetTester tester) async { WebViewController controller; await tester.pumpWidget( WebView( @@ -296,8 +292,7 @@ void main() { ), ); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + final FakePlatformWebView platformWebView = fakePlatformViewsController.lastCreatedView; expect(platformWebView.currentUrl, 'https://flutter.io'); expect(platformWebView.amountOfReloadsOnCurrentUrl, 0); @@ -323,8 +318,7 @@ void main() { }, ), ); - expect( - await controller.evaluateJavascript("fake js string"), "fake js string", + expect(await controller.evaluateJavascript("fake js string"), "fake js string", reason: 'should get the argument'); expect( () => controller.evaluateJavascript(null), @@ -332,8 +326,7 @@ void main() { ); }); - testWidgets('evaluate Javascript with JavascriptMode disabled', - (WidgetTester tester) async { + testWidgets('evaluate Javascript with JavascriptMode disabled', (WidgetTester tester) async { WebViewController controller; await tester.pumpWidget( WebView( @@ -353,6 +346,86 @@ void main() { throwsA(anything), ); }); + + testWidgets('Initial JavaScript channels', (WidgetTester tester) async { + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + javascriptChannels: [ + JavascriptChannel(name: 'Tts', onMessageReceived: (String msg) {}), + JavascriptChannel(name: 'Alarm', onMessageReceived: (String msg) {}), + ].toSet(), + ), + ); + + final FakePlatformWebView platformWebView = fakePlatformViewsController.lastCreatedView; + + expect(platformWebView.javascriptChannelNames, unorderedEquals(['Tts', 'Alarm'])); + }); + + testWidgets('Unique JavaScript channel names are required', (WidgetTester tester) async { + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + javascriptChannels: [ + JavascriptChannel(name: 'Alarm', onMessageReceived: (String msg) {}), + JavascriptChannel(name: 'Alarm', onMessageReceived: (String msg) {}), + ].toSet(), + ), + ); + expect(tester.takeException(), isNot(null)); + }); + + testWidgets('JavaScript channels update', (WidgetTester tester) async { + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + javascriptChannels: [ + JavascriptChannel(name: 'Tts', onMessageReceived: (String msg) {}), + JavascriptChannel(name: 'Alarm', onMessageReceived: (String msg) {}), + ].toSet(), + ), + ); + + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + javascriptChannels: [ + JavascriptChannel(name: 'Tts', onMessageReceived: (String msg) {}), + JavascriptChannel(name: 'Alarm2', onMessageReceived: (String msg) {}), + JavascriptChannel(name: 'Alarm3', onMessageReceived: (String msg) {}), + ].toSet(), + ), + ); + + final FakePlatformWebView platformWebView = fakePlatformViewsController.lastCreatedView; + + expect(platformWebView.javascriptChannelNames, unorderedEquals(['Tts', 'Alarm2', 'Alarm3'])); + }); + + testWidgets('JavaScript channel messages', (WidgetTester tester) async { + final List ttsMessagesReceived = []; + final List alarmMessagesReceived = []; + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + javascriptChannels: [ + JavascriptChannel(name: 'Tts', onMessageReceived: (String msg) {ttsMessagesReceived.add(msg); }), + JavascriptChannel(name: 'Alarm', onMessageReceived: (String msg) {alarmMessagesReceived.add(msg); }), + ].toSet(), + ), + ); + + final FakePlatformWebView platformWebView = fakePlatformViewsController.lastCreatedView; + + expect(ttsMessagesReceived, isEmpty); + expect(alarmMessagesReceived, isEmpty); + + platformWebView.fakeJavascriptPostMessage('Tts', 'Hello'); + platformWebView.fakeJavascriptPostMessage('Tts', 'World'); + + expect(ttsMessagesReceived, ['Hello', 'World']); + }); } class FakePlatformWebView { @@ -365,8 +438,10 @@ class FakePlatformWebView { } javascriptMode = JavascriptMode.values[params['settings']['jsMode']]; } - channel = MethodChannel( - 'plugins.flutter.io/webview_$id', const StandardMethodCodec()); + if (params.containsKey('javascriptChannelNames')) { + javascriptChannelNames = List.from(params['javascriptChannelNames']); + } + channel = MethodChannel('plugins.flutter.io/webview_$id', const StandardMethodCodec()); channel.setMockMethodCallHandler(onMethodCall); } @@ -378,6 +453,7 @@ class FakePlatformWebView { String get currentUrl => history.isEmpty ? null : history[currentPosition]; JavascriptMode javascriptMode; + List javascriptChannelNames; Future onMethodCall(MethodCall call) { switch (call.method) { @@ -416,9 +492,28 @@ class FakePlatformWebView { break; case 'evaluateJavascript': return Future.value(call.arguments); + break; + case 'addJavascriptChannels': + final List channelNames = List.from(call.arguments); + javascriptChannelNames.addAll(channelNames); + break; + case 'removeJavascriptChannels': + final List channelNames = List.from(call.arguments); + javascriptChannelNames.removeWhere((String channel) => channelNames.contains(channel)); + break; } return Future.sync(() {}); } + + void fakeJavascriptPostMessage(String jsChannel, String message) { + final StandardMethodCodec codec = const StandardMethodCodec(); + final Map arguments = { + 'channel': jsChannel, + 'message': message + }; + final ByteData data = codec.encodeMethodCall(MethodCall('javascriptChannelMessage', arguments)); + BinaryMessages.handlePlatformMessage(channel.name, data, (ByteData data) {}); + } } class _FakePlatformViewsController { From 015f025a24a1b39220434f6e38e92e923d62e3ce Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Fri, 25 Jan 2019 13:29:23 -0800 Subject: [PATCH 02/11] format --- .../webview_flutter/lib/webview_flutter.dart | 38 ++++++---- .../test/webview_flutter_test.dart | 72 +++++++++++++------ 2 files changed, 74 insertions(+), 36 deletions(-) diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index ccc2a438df1c..4ec173793618 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -115,7 +115,8 @@ class WebView extends StatefulWidget { } class _WebViewState extends State { - final Completer _controller = Completer(); + final Completer _controller = + Completer(); _WebSettings _settings; @@ -152,7 +153,8 @@ class _WebViewState extends State { creationParamsCodec: const StandardMessageCodec(), ); } - return Text('$defaultTargetPlatform is not yet supported by the webview_flutter plugin'); + return Text( + '$defaultTargetPlatform is not yet supported by the webview_flutter plugin'); } @override @@ -188,10 +190,12 @@ class _WebViewState extends State { } void _assertJavascriptChannelNamesAreUnique() { - if (widget.javascriptChannels == null || widget.javascriptChannels.isEmpty) { + if (widget.javascriptChannels == null || + widget.javascriptChannels.isEmpty) { return; } - assert(_extractChannelNames(widget.javascriptChannels).length == widget.javascriptChannels.length); + assert(_extractChannelNames(widget.javascriptChannels).length == + widget.javascriptChannels.length); } } @@ -203,13 +207,15 @@ Set _extractChannelNames(Set channels) { } class _CreationParams { - _CreationParams({this.initialUrl, this.settings, this.javascriptChannelNames}); + _CreationParams( + {this.initialUrl, this.settings, this.javascriptChannelNames}); static _CreationParams fromWidget(WebView widget) { return _CreationParams( initialUrl: widget.initialUrl, settings: _WebSettings.fromWidget(widget), - javascriptChannelNames: _extractChannelNames(widget.javascriptChannels).toList(), + javascriptChannelNames: + _extractChannelNames(widget.javascriptChannels).toList(), ); } @@ -260,7 +266,8 @@ class _WebSettings { /// A [WebViewController] instance can be obtained by setting the [WebView.onWebViewCreated] /// callback for a [WebView] widget. class WebViewController { - WebViewController._(int id, this._settings, Set javascriptChannels) + WebViewController._( + int id, this._settings, Set javascriptChannels) : _channel = MethodChannel('plugins.flutter.io/webview_$id') { _updateJavascriptChannelsFromSet(javascriptChannels); _channel.setMethodCallHandler(_onMethodCall); @@ -271,10 +278,11 @@ class WebViewController { _WebSettings _settings; // Maps a channel name to a channel. - Map _javascriptChannels = {}; + Map _javascriptChannels = + {}; Future _onMethodCall(MethodCall call) async { - switch(call.method) { + switch (call.method) { case 'javascriptChannelMessage': final String channel = call.arguments['channel']; final String message = call.arguments['message']; @@ -376,16 +384,20 @@ class WebViewController { return _channel.invokeMethod('updateSettings', updateMap); } - Future _updateJavascriptChannels(Set newChannels) async { + Future _updateJavascriptChannels( + Set newChannels) async { final Set currentChannels = _javascriptChannels.keys.toSet(); final Set newChannelNames = _extractChannelNames(newChannels); - final Set channelsToAdd = newChannelNames.difference(currentChannels); - final Set channelsToRemove = currentChannels.difference(newChannelNames); + final Set channelsToAdd = + newChannelNames.difference(currentChannels); + final Set channelsToRemove = + currentChannels.difference(newChannelNames); if (channelsToRemove.isNotEmpty) { // TODO(amirh): remove this when the invokeMethod update makes it to stable Flutter. // https://github.com/flutter/flutter/issues/26431 // ignore: strong_mode_implicit_dynamic_method - _channel.invokeMethod('removeJavascriptChannels', channelsToRemove.toList()); + _channel.invokeMethod( + 'removeJavascriptChannels', channelsToRemove.toList()); } if (channelsToAdd.isNotEmpty) { // TODO(amirh): remove this when the invokeMethod update makes it to stable Flutter. diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart index ccd9546f6aba..f87517a04582 100644 --- a/packages/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/test/webview_flutter_test.dart @@ -10,11 +10,12 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:webview_flutter/webview_flutter.dart'; void main() { - final _FakePlatformViewsController fakePlatformViewsController = _FakePlatformViewsController(); + final _FakePlatformViewsController fakePlatformViewsController = + _FakePlatformViewsController(); setUpAll(() { - SystemChannels.platform_views - .setMockMethodCallHandler(fakePlatformViewsController.fakePlatformViewsMethodHandler); + SystemChannels.platform_views.setMockMethodCallHandler( + fakePlatformViewsController.fakePlatformViewsMethodHandler); }); setUp(() { @@ -45,7 +46,8 @@ void main() { javascriptMode: JavascriptMode.unrestricted, )); - final FakePlatformWebView platformWebView = fakePlatformViewsController.lastCreatedView; + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; expect(platformWebView.javascriptMode, JavascriptMode.unrestricted); @@ -97,7 +99,8 @@ void main() { expect(await controller.currentUrl(), isNull); }); - testWidgets("Can't go back before loading a page", (WidgetTester tester) async { + testWidgets("Can't go back before loading a page", + (WidgetTester tester) async { WebViewController controller; await tester.pumpWidget( WebView( @@ -150,7 +153,8 @@ void main() { expect(canGoBackSecondPageLoaded, true); }); - testWidgets("Can't go forward before loading a page", (WidgetTester tester) async { + testWidgets("Can't go forward before loading a page", + (WidgetTester tester) async { WebViewController controller; await tester.pumpWidget( WebView( @@ -292,7 +296,8 @@ void main() { ), ); - final FakePlatformWebView platformWebView = fakePlatformViewsController.lastCreatedView; + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; expect(platformWebView.currentUrl, 'https://flutter.io'); expect(platformWebView.amountOfReloadsOnCurrentUrl, 0); @@ -318,7 +323,8 @@ void main() { }, ), ); - expect(await controller.evaluateJavascript("fake js string"), "fake js string", + expect( + await controller.evaluateJavascript("fake js string"), "fake js string", reason: 'should get the argument'); expect( () => controller.evaluateJavascript(null), @@ -326,7 +332,8 @@ void main() { ); }); - testWidgets('evaluate Javascript with JavascriptMode disabled', (WidgetTester tester) async { + testWidgets('evaluate Javascript with JavascriptMode disabled', + (WidgetTester tester) async { WebViewController controller; await tester.pumpWidget( WebView( @@ -358,12 +365,15 @@ void main() { ), ); - final FakePlatformWebView platformWebView = fakePlatformViewsController.lastCreatedView; + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; - expect(platformWebView.javascriptChannelNames, unorderedEquals(['Tts', 'Alarm'])); + expect(platformWebView.javascriptChannelNames, + unorderedEquals(['Tts', 'Alarm'])); }); - testWidgets('Unique JavaScript channel names are required', (WidgetTester tester) async { + testWidgets('Unique JavaScript channel names are required', + (WidgetTester tester) async { await tester.pumpWidget( WebView( initialUrl: 'https://youtube.com', @@ -398,9 +408,11 @@ void main() { ), ); - final FakePlatformWebView platformWebView = fakePlatformViewsController.lastCreatedView; + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; - expect(platformWebView.javascriptChannelNames, unorderedEquals(['Tts', 'Alarm2', 'Alarm3'])); + expect(platformWebView.javascriptChannelNames, + unorderedEquals(['Tts', 'Alarm2', 'Alarm3'])); }); testWidgets('JavaScript channel messages', (WidgetTester tester) async { @@ -410,13 +422,22 @@ void main() { WebView( initialUrl: 'https://youtube.com', javascriptChannels: [ - JavascriptChannel(name: 'Tts', onMessageReceived: (String msg) {ttsMessagesReceived.add(msg); }), - JavascriptChannel(name: 'Alarm', onMessageReceived: (String msg) {alarmMessagesReceived.add(msg); }), + JavascriptChannel( + name: 'Tts', + onMessageReceived: (String msg) { + ttsMessagesReceived.add(msg); + }), + JavascriptChannel( + name: 'Alarm', + onMessageReceived: (String msg) { + alarmMessagesReceived.add(msg); + }), ].toSet(), ), ); - final FakePlatformWebView platformWebView = fakePlatformViewsController.lastCreatedView; + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; expect(ttsMessagesReceived, isEmpty); expect(alarmMessagesReceived, isEmpty); @@ -439,9 +460,11 @@ class FakePlatformWebView { javascriptMode = JavascriptMode.values[params['settings']['jsMode']]; } if (params.containsKey('javascriptChannelNames')) { - javascriptChannelNames = List.from(params['javascriptChannelNames']); + javascriptChannelNames = + List.from(params['javascriptChannelNames']); } - channel = MethodChannel('plugins.flutter.io/webview_$id', const StandardMethodCodec()); + channel = MethodChannel( + 'plugins.flutter.io/webview_$id', const StandardMethodCodec()); channel.setMockMethodCallHandler(onMethodCall); } @@ -499,7 +522,8 @@ class FakePlatformWebView { break; case 'removeJavascriptChannels': final List channelNames = List.from(call.arguments); - javascriptChannelNames.removeWhere((String channel) => channelNames.contains(channel)); + javascriptChannelNames + .removeWhere((String channel) => channelNames.contains(channel)); break; } return Future.sync(() {}); @@ -507,12 +531,14 @@ class FakePlatformWebView { void fakeJavascriptPostMessage(String jsChannel, String message) { final StandardMethodCodec codec = const StandardMethodCodec(); - final Map arguments = { + final Map arguments = { 'channel': jsChannel, 'message': message }; - final ByteData data = codec.encodeMethodCall(MethodCall('javascriptChannelMessage', arguments)); - BinaryMessages.handlePlatformMessage(channel.name, data, (ByteData data) {}); + final ByteData data = codec + .encodeMethodCall(MethodCall('javascriptChannelMessage', arguments)); + BinaryMessages.handlePlatformMessage( + channel.name, data, (ByteData data) {}); } } From 452a45c17a72ee368640a0c2d76357cafe469164 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Mon, 28 Jan 2019 08:59:56 -0800 Subject: [PATCH 03/11] Enforce channel names --- .../webview_flutter/lib/webview_flutter.dart | 10 ++++++++-- .../test/webview_flutter_test.dart | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index 4ec173793618..0bea7de00c7f 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -22,6 +22,8 @@ enum JavascriptMode { /// Callback type for handling messages sent from Javascript running in a web view. typedef void JavascriptMessageHandler(String message); +final RegExp _validChannelNames = RegExp('^[a-zA-Z_][a-zA-Z0-9]*\$'); + /// A named channel for receiving messaged from JavaScript code running inside a web view. class JavascriptChannel { /// Constructs a Javascript channel. @@ -30,14 +32,18 @@ class JavascriptChannel { JavascriptChannel({ @required this.name, @required this.onMessageReceived, - }); + }) : assert(name != null), + assert(onMessageReceived != null), + assert(_validChannelNames.hasMatch(name)); /// The channel's name. /// - /// /// Passing this channel object as part of a [WebView.javascriptChannels] adds a channel object to /// the Javascript window object's property named `name`. /// + /// The name must start with a letter or underscore(_), followed by any combination of those + /// characters plus digits. + /// /// See also [WebView.javascriptChannels] for more details on the channel registration mechanism. final String name; diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart index f87517a04582..0f3c2fdd690f 100644 --- a/packages/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/test/webview_flutter_test.dart @@ -9,6 +9,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:webview_flutter/webview_flutter.dart'; +typedef void VoidCallback(); + void main() { final _FakePlatformViewsController fakePlatformViewsController = _FakePlatformViewsController(); @@ -372,6 +374,22 @@ void main() { unorderedEquals(['Tts', 'Alarm'])); }); + test('Only valid JavaScript channel names are allowed', () { + final JavascriptMessageHandler noOp = (String msg) {}; + JavascriptChannel(name: 'Tts1', onMessageReceived: noOp); + JavascriptChannel(name: '_Alarm', onMessageReceived: noOp); + + VoidCallback createChannel(String name) { + return () { + JavascriptChannel(name: name, onMessageReceived: noOp); + }; + } + + expect(createChannel('1Alarm'), throwsAssertionError); + expect(createChannel('foo.bar'), throwsAssertionError); + expect(createChannel(''), throwsAssertionError); + }); + testWidgets('Unique JavaScript channel names are required', (WidgetTester tester) async { await tester.pumpWidget( From 1699836e939b6365e50b28a25aa72f8d84f4c457 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Mon, 28 Jan 2019 15:31:27 -0800 Subject: [PATCH 04/11] Pass messages as JavascriptMessage objects. Add a note about overriding window properties. --- .../webview_flutter/lib/webview_flutter.dart | 17 +++++++++-- .../test/webview_flutter_test.dart | 28 +++++++++---------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index 0bea7de00c7f..75335a01bb59 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -19,8 +19,19 @@ enum JavascriptMode { unrestricted, } +/// A message that was sent by JavaScript code running in a [WebView]. +class JavascriptMessage { + /// Constructs a JavaScript message object. + /// + /// The `message` parameter must not be null. + const JavascriptMessage(this.message) : assert(message != null); + + /// The contents of the message that was sent by the JavaScript code. + final String message; +} + /// Callback type for handling messages sent from Javascript running in a web view. -typedef void JavascriptMessageHandler(String message); +typedef void JavascriptMessageHandler(JavascriptMessage message); final RegExp _validChannelNames = RegExp('^[a-zA-Z_][a-zA-Z0-9]*\$'); @@ -44,6 +55,8 @@ class JavascriptChannel { /// The name must start with a letter or underscore(_), followed by any combination of those /// characters plus digits. /// + /// Note that any JavaScript existing `window` property with this name will be overriden. + /// /// See also [WebView.javascriptChannels] for more details on the channel registration mechanism. final String name; @@ -292,7 +305,7 @@ class WebViewController { case 'javascriptChannelMessage': final String channel = call.arguments['channel']; final String message = call.arguments['message']; - _javascriptChannels[channel].onMessageReceived(message); + _javascriptChannels[channel].onMessageReceived(JavascriptMessage(message)); break; } } diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart index 0f3c2fdd690f..8b459d48c3f0 100644 --- a/packages/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/test/webview_flutter_test.dart @@ -361,8 +361,8 @@ void main() { WebView( initialUrl: 'https://youtube.com', javascriptChannels: [ - JavascriptChannel(name: 'Tts', onMessageReceived: (String msg) {}), - JavascriptChannel(name: 'Alarm', onMessageReceived: (String msg) {}), + JavascriptChannel(name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel(name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), ].toSet(), ), ); @@ -375,7 +375,7 @@ void main() { }); test('Only valid JavaScript channel names are allowed', () { - final JavascriptMessageHandler noOp = (String msg) {}; + final JavascriptMessageHandler noOp = (JavascriptMessage msg) {}; JavascriptChannel(name: 'Tts1', onMessageReceived: noOp); JavascriptChannel(name: '_Alarm', onMessageReceived: noOp); @@ -396,8 +396,8 @@ void main() { WebView( initialUrl: 'https://youtube.com', javascriptChannels: [ - JavascriptChannel(name: 'Alarm', onMessageReceived: (String msg) {}), - JavascriptChannel(name: 'Alarm', onMessageReceived: (String msg) {}), + JavascriptChannel(name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel(name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), ].toSet(), ), ); @@ -409,8 +409,8 @@ void main() { WebView( initialUrl: 'https://youtube.com', javascriptChannels: [ - JavascriptChannel(name: 'Tts', onMessageReceived: (String msg) {}), - JavascriptChannel(name: 'Alarm', onMessageReceived: (String msg) {}), + JavascriptChannel(name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel(name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), ].toSet(), ), ); @@ -419,9 +419,9 @@ void main() { WebView( initialUrl: 'https://youtube.com', javascriptChannels: [ - JavascriptChannel(name: 'Tts', onMessageReceived: (String msg) {}), - JavascriptChannel(name: 'Alarm2', onMessageReceived: (String msg) {}), - JavascriptChannel(name: 'Alarm3', onMessageReceived: (String msg) {}), + JavascriptChannel(name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel(name: 'Alarm2', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel(name: 'Alarm3', onMessageReceived: (JavascriptMessage msg) {}), ].toSet(), ), ); @@ -442,13 +442,13 @@ void main() { javascriptChannels: [ JavascriptChannel( name: 'Tts', - onMessageReceived: (String msg) { - ttsMessagesReceived.add(msg); + onMessageReceived: (JavascriptMessage msg) { + ttsMessagesReceived.add(msg.message); }), JavascriptChannel( name: 'Alarm', - onMessageReceived: (String msg) { - alarmMessagesReceived.add(msg); + onMessageReceived: (JavascriptMessage msg) { + alarmMessagesReceived.add(msg.message); }), ].toSet(), ), From 83be1fada4e164109cd8e9bf6809864496db22ef Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Mon, 28 Jan 2019 15:36:17 -0800 Subject: [PATCH 05/11] format --- .../webview_flutter/lib/webview_flutter.dart | 3 ++- .../test/webview_flutter_test.dart | 27 ++++++++++++------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index 75335a01bb59..c1afeabb2963 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -305,7 +305,8 @@ class WebViewController { case 'javascriptChannelMessage': final String channel = call.arguments['channel']; final String message = call.arguments['message']; - _javascriptChannels[channel].onMessageReceived(JavascriptMessage(message)); + _javascriptChannels[channel] + .onMessageReceived(JavascriptMessage(message)); break; } } diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart index 8b459d48c3f0..f7eb07b32370 100644 --- a/packages/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/test/webview_flutter_test.dart @@ -361,8 +361,10 @@ void main() { WebView( initialUrl: 'https://youtube.com', javascriptChannels: [ - JavascriptChannel(name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), - JavascriptChannel(name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), ].toSet(), ), ); @@ -396,8 +398,10 @@ void main() { WebView( initialUrl: 'https://youtube.com', javascriptChannels: [ - JavascriptChannel(name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), - JavascriptChannel(name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), ].toSet(), ), ); @@ -409,8 +413,10 @@ void main() { WebView( initialUrl: 'https://youtube.com', javascriptChannels: [ - JavascriptChannel(name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), - JavascriptChannel(name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), ].toSet(), ), ); @@ -419,9 +425,12 @@ void main() { WebView( initialUrl: 'https://youtube.com', javascriptChannels: [ - JavascriptChannel(name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), - JavascriptChannel(name: 'Alarm2', onMessageReceived: (JavascriptMessage msg) {}), - JavascriptChannel(name: 'Alarm3', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Alarm2', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Alarm3', onMessageReceived: (JavascriptMessage msg) {}), ].toSet(), ), ); From 2c423dd38e848f0324927b88c74fe64cdb589af2 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Mon, 28 Jan 2019 16:02:21 -0800 Subject: [PATCH 06/11] fix channel cache update cache bug --- .../webview_flutter/lib/webview_flutter.dart | 2 +- .../test/webview_flutter_test.dart | 37 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index c1afeabb2963..77c8f56c0546 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -429,10 +429,10 @@ class WebViewController { } void _updateJavascriptChannelsFromSet(Set channels) { + _javascriptChannels.clear(); if (channels == null) { return; } - _javascriptChannels.clear(); for (JavascriptChannel channel in channels) { _javascriptChannels[channel.name] = channel; } diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart index f7eb07b32370..05c5c497468d 100644 --- a/packages/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/test/webview_flutter_test.dart @@ -442,6 +442,42 @@ void main() { unorderedEquals(['Tts', 'Alarm2', 'Alarm3'])); }); + testWidgets('Remove all JavaScript channels and then add', (WidgetTester tester) async { + // This covers a specific bug we had where after updating javascriptChannels to null, + // updating it again with a subset of the previously registered channels fails as the + // widget's cache of current channel wasn't properly updated when updating javascriptChannels to + // null. + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + javascriptChannels: [ + JavascriptChannel(name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), + ].toSet(), + ), + ); + + await tester.pumpWidget( + const WebView( + initialUrl: 'https://youtube.com', + ), + ); + + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + javascriptChannels: [ + JavascriptChannel(name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), + ].toSet(), + ), + ); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + expect(platformWebView.javascriptChannelNames, + unorderedEquals(['Tts'])); + }); + testWidgets('JavaScript channel messages', (WidgetTester tester) async { final List ttsMessagesReceived = []; final List alarmMessagesReceived = []; @@ -474,6 +510,7 @@ void main() { expect(ttsMessagesReceived, ['Hello', 'World']); }); + } class FakePlatformWebView { From 57b3cd5c8da9d3923f6b8f98330753d443b6d49c Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Mon, 28 Jan 2019 16:32:15 -0800 Subject: [PATCH 07/11] format with stable flutter --- .../webview_flutter/test/webview_flutter_test.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart index 05c5c497468d..12b4831b5399 100644 --- a/packages/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/test/webview_flutter_test.dart @@ -442,7 +442,8 @@ void main() { unorderedEquals(['Tts', 'Alarm2', 'Alarm3'])); }); - testWidgets('Remove all JavaScript channels and then add', (WidgetTester tester) async { + testWidgets('Remove all JavaScript channels and then add', + (WidgetTester tester) async { // This covers a specific bug we had where after updating javascriptChannels to null, // updating it again with a subset of the previously registered channels fails as the // widget's cache of current channel wasn't properly updated when updating javascriptChannels to @@ -451,7 +452,8 @@ void main() { WebView( initialUrl: 'https://youtube.com', javascriptChannels: [ - JavascriptChannel(name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), ].toSet(), ), ); @@ -466,7 +468,8 @@ void main() { WebView( initialUrl: 'https://youtube.com', javascriptChannels: [ - JavascriptChannel(name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), ].toSet(), ), ); @@ -510,7 +513,6 @@ void main() { expect(ttsMessagesReceived, ['Hello', 'World']); }); - } class FakePlatformWebView { From b57117f6697fc995d8a195938adfc22664fa53d2 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Wed, 30 Jan 2019 16:23:21 -0800 Subject: [PATCH 08/11] Document when adding a channel takes affect. --- packages/webview_flutter/lib/webview_flutter.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index 77c8f56c0546..8d5a74686760 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -123,6 +123,8 @@ class WebView extends StatefulWidget { /// /// To asynchronously invoke the message handler which will print the message to standard output. /// + /// Adding a new JavaScript channel only takes affect after the next page is loaded. + /// /// Set values must not be null. A [JavascriptChannel.name] cannot be the same for multiple /// channels in the list. /// From 59e12482fedbef73b0a67dfdefb6a4652ee61a16 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Wed, 30 Jan 2019 16:23:42 -0800 Subject: [PATCH 09/11] update changelog and bump version --- packages/webview_flutter/CHANGELOG.md | 5 +++++ packages/webview_flutter/pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md index d90be52c5468..07a69c0f3501 100644 --- a/packages/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.3.1 + +* Added JavaScript channels to facilitate message passing from JavaScript code running inside + the WebView to the Flutter app's Dart code. + ## 0.3.0 * **Breaking change**. Migrate from the deprecated original Android Support diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml index 2ed52c97e23a..329159e0eeab 100644 --- a/packages/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: webview_flutter description: A Flutter plugin that provides a WebView widget on Android and iOS. -version: 0.3.0 +version: 0.3.1 author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter From 9a350dd179fe550ada3820cd47f13c64f912d4c0 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Wed, 30 Jan 2019 16:46:41 -0800 Subject: [PATCH 10/11] Use JavascriptChannels in the example app --- .../webview_flutter/example/lib/main.dart | 56 ++++++++++++------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/packages/webview_flutter/example/lib/main.dart b/packages/webview_flutter/example/lib/main.dart index 9befe4b67f41..fcb80bbfaf21 100644 --- a/packages/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/example/lib/main.dart @@ -23,17 +23,34 @@ class WebViewExample extends StatelessWidget { SampleMenu(_controller.future), ], ), - body: WebView( - initialUrl: 'https://flutter.io', - javascriptMode: JavascriptMode.unrestricted, - onWebViewCreated: (WebViewController webViewController) { - _controller.complete(webViewController); - }, - ), + // We're using a Builder here so we have a context that is below the Scaffold + // to allow calling Scaffold.maketoast + body: Builder(builder: (BuildContext context) { + return WebView( + initialUrl: 'https://flutter.io', + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController webViewController) { + _controller.complete(webViewController); + }, + javascriptChannels: [ + _toasterJavascriptChannel(context), + ].toSet(), + ); + }), floatingActionButton: favoriteButton(), ); } + JavascriptChannel _toasterJavascriptChannel(BuildContext context) { + return JavascriptChannel( + name: 'Toaster', + onMessageReceived: (JavascriptMessage message) { + Scaffold.of(context).showSnackBar( + SnackBar(content: Text(message.message)), + ); + }); + } + Widget favoriteButton() { return FutureBuilder( future: _controller.future, @@ -44,7 +61,7 @@ class WebViewExample extends StatelessWidget { onPressed: () async { final String url = await controller.data.currentUrl(); Scaffold.of(context).showSnackBar( - SnackBar(content: Text("Favorited $url")), + SnackBar(content: Text('Favorited $url')), ); }, child: const Icon(Icons.favorite), @@ -56,7 +73,7 @@ class WebViewExample extends StatelessWidget { } enum MenuOptions { - evaluateJavascript, + showUserAgent, toast, } @@ -73,8 +90,8 @@ class SampleMenu extends StatelessWidget { return PopupMenuButton( onSelected: (MenuOptions value) { switch (value) { - case MenuOptions.evaluateJavascript: - _onEvaluateJavascript(controller.data, context); + case MenuOptions.showUserAgent: + _onShowUserAgent(controller.data, context); break; case MenuOptions.toast: Scaffold.of(context).showSnackBar( @@ -87,8 +104,8 @@ class SampleMenu extends StatelessWidget { }, itemBuilder: (BuildContext context) => >[ PopupMenuItem( - value: MenuOptions.evaluateJavascript, - child: const Text('Evaluate JavaScript'), + value: MenuOptions.showUserAgent, + child: const Text('Show user agent'), enabled: controller.hasData, ), const PopupMenuItem( @@ -101,15 +118,12 @@ class SampleMenu extends StatelessWidget { ); } - void _onEvaluateJavascript( + void _onShowUserAgent( WebViewController controller, BuildContext context) async { - final String result = await controller - .evaluateJavascript("document.body.style.backgroundColor = 'red'"); - Scaffold.of(context).showSnackBar( - SnackBar( - content: Text('JavaScript evaluated, the result is: $result'), - ), - ); + // Send a message with the user agent string to the Toaster JavaScript channel we registered + // with the WebView. + controller.evaluateJavascript( + 'Toaster.postMessage("User Agent: " + navigator.userAgent);'); } } From 61807a170a7cd43e7de3ace01ba1804783cd5fdf Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Mon, 4 Feb 2019 11:35:53 -0800 Subject: [PATCH 11/11] fix example comment --- packages/webview_flutter/example/lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webview_flutter/example/lib/main.dart b/packages/webview_flutter/example/lib/main.dart index fcb80bbfaf21..d8a4e8c439d8 100644 --- a/packages/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/example/lib/main.dart @@ -24,7 +24,7 @@ class WebViewExample extends StatelessWidget { ], ), // We're using a Builder here so we have a context that is below the Scaffold - // to allow calling Scaffold.maketoast + // to allow calling Scaffold.of(context) so we can show a snackbar. body: Builder(builder: (BuildContext context) { return WebView( initialUrl: 'https://flutter.io',