@@ -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.
2368class 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
128230class _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.
180289class 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.
0 commit comments