diff --git a/packages/webview_flutter/lib/platform_interface.dart b/packages/webview_flutter/lib/platform_interface.dart index df4e3f7d7cca..5d8899ae9e3a 100644 --- a/packages/webview_flutter/lib/platform_interface.dart +++ b/packages/webview_flutter/lib/platform_interface.dart @@ -10,11 +10,35 @@ import 'package:flutter/widgets.dart'; import 'webview_flutter.dart'; +/// Interface for callbacks made by [WebViewPlatform]. +/// +/// The webview plugin implements this class, and passes an instance to the [WebViewPlatform]. +/// [WebViewPlatform] is notifying this handler on events that happened on the platform's webview. +abstract class WebViewPlatformCallbacksHandler { + /// Invoked by [WebViewPlatform] when a JavaScript channel message is received. + void onJavaScriptChannelMessage(String channel, String message); + + /// Invoked by [WebViewPlatform] when a navigation request is pending. + /// + /// If true is returned the navigation is allowed, otherwise it is blocked. + bool onNavigationRequest({String url, bool isForMainFrame}); + + /// Invoked by [WebViewPlatform] when a page has finished loading. + void onPageFinished(String url); +} + /// Interface for talking to the webview's platform implementation. /// /// An instance implementing this interface is passed to the `onWebViewPlatformCreated` callback that is /// passed to [WebViewPlatformBuilder#onWebViewPlatformCreated]. abstract class WebViewPlatform { + /// Creates a new WebViewPlatform. + /// + /// Callbacks made by the WebView will be delegated to `handler`. + /// + /// The `handler` parameter must not be null. + WebViewPlatform(WebViewPlatformCallbacksHandler handler); + /// Loads the specified URL. /// /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will @@ -40,12 +64,94 @@ abstract class WebViewPlatform { "WebView updateSettings is not implemented on the current platform"); } - // As the PR currently focus about the wiring I've only moved loadUrl to the new way, so - // the discussion is more focused. - // In this temporary state WebViewController still uses a method channel directly for all other - // method calls so we need to expose the webview ID. - // TODO(amirh): remove this before publishing this package. - int get id; + /// Accessor to the current URL that the WebView is displaying. + /// + /// If no URL was ever loaded, returns `null`. + Future currentUrl() { + throw UnimplementedError( + "WebView currentUrl is not implemented on the current platform"); + } + + /// Checks whether there's a back history item. + Future canGoBack() { + throw UnimplementedError( + "WebView canGoBack is not implemented on the current platform"); + } + + /// Checks whether there's a forward history item. + Future canGoForward() { + throw UnimplementedError( + "WebView canGoForward is not implemented on the current platform"); + } + + /// Goes back in the history of this WebView. + /// + /// If there is no back history item this is a no-op. + Future goBack() { + throw UnimplementedError( + "WebView goBack is not implemented on the current platform"); + } + + /// Goes forward in the history of this WebView. + /// + /// If there is no forward history item this is a no-op. + Future goForward() { + throw UnimplementedError( + "WebView goForward is not implemented on the current platform"); + } + + /// Reloads the current URL. + Future reload() { + throw UnimplementedError( + "WebView reload is not implemented on the current platform"); + } + + /// Clears all caches used by the [WebView]. + /// + /// The following caches are cleared: + /// 1. Browser HTTP Cache. + /// 2. [Cache API](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api) caches. + /// These are not yet supported in iOS WkWebView. Service workers tend to use this cache. + /// 3. Application cache. + /// 4. Local Storage. + Future clearCache() { + throw UnimplementedError( + "WebView clearCache is not implemented on the current platform"); + } + + /// Evaluates a JavaScript expression in the context of the current page. + /// + /// The Future completes with an error if a JavaScript error occurred, or if the type of the + /// evaluated expression is not supported(e.g on iOS not all non primitive type can be evaluated). + Future evaluateJavascript(String javascriptString) { + throw UnimplementedError( + "WebView evaluateJavascript is not implemented on the current platform"); + } + + /// Adds new JavaScript channels to the set of enabled channels. + /// + /// For each value in this list the platform's webview should make sure that a corresponding + /// property with a postMessage method is set on `window`. For example for a JavaScript channel + /// named `Foo` it should be possible for JavaScript code executing in the webview to do + /// + /// ```javascript + /// Foo.postMessage('hello'); + /// ``` + /// + /// See also: [CreationParams.javascriptChannelNames]. + Future addJavascriptChannels(Set javascriptChannelNames) { + throw UnimplementedError( + "WebView addJavascriptChannels is not implemented on the current platform"); + } + + /// Removes JavaScript channel names from the set of enabled channels. + /// + /// This disables channels that were previously enabled by [addJavaScriptChannels] or through + /// [CreationParams.javascriptChannelNames]. + Future removeJavascriptChannels(Set javascriptChannelNames) { + throw UnimplementedError( + "WebView removeJavascriptChannels is not implemented on the current platform"); + } } /// Settings for configuring a WebViewPlatform. @@ -125,6 +231,9 @@ abstract class WebViewBuilder { /// /// `creationParams` are the initial parameters used to setup the webview. /// + /// `webViewPlatformHandler` will be used for handling callbacks that are made by the created + /// [WebViewPlatform]. + /// /// `onWebViewPlatformCreated` will be invoked after the platform specific [WebViewPlatform] /// implementation is created with the [WebViewPlatform] instance as a parameter. /// @@ -135,12 +244,15 @@ abstract class WebViewBuilder { /// recognizers on this list. /// When `gestureRecognizers` is empty or null, the web view will only handle pointer events for gestures that /// were not claimed by any other gesture recognizer. + /// + /// `webViewPlatformHandler` must not be null. Widget build({ BuildContext context, // TODO(amirh): convert this to be the actual parameters. // I'm starting without it as the PR is starting to become pretty big. // I'll followup with the conversion PR. CreationParams creationParams, + @required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, WebViewPlatformCreatedCallback onWebViewPlatformCreated, Set> gestureRecognizers, }); diff --git a/packages/webview_flutter/lib/src/webview_android.dart b/packages/webview_flutter/lib/src/webview_android.dart index 7304e9bce22c..8d5f98a61512 100644 --- a/packages/webview_flutter/lib/src/webview_android.dart +++ b/packages/webview_flutter/lib/src/webview_android.dart @@ -20,9 +20,11 @@ class AndroidWebViewBuilder implements WebViewBuilder { Widget build({ BuildContext context, CreationParams creationParams, + @required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, WebViewPlatformCreatedCallback onWebViewPlatformCreated, Set> gestureRecognizers, }) { + assert(webViewPlatformCallbacksHandler != null); return GestureDetector( // We prevent text selection by intercepting the long press event. // This is a temporary stop gap due to issues with text selection on Android: @@ -39,7 +41,8 @@ class AndroidWebViewBuilder implements WebViewBuilder { if (onWebViewPlatformCreated == null) { return; } - onWebViewPlatformCreated(MethodChannelWebViewPlatform(id)); + onWebViewPlatformCreated(MethodChannelWebViewPlatform( + id, webViewPlatformCallbacksHandler)); }, gestureRecognizers: gestureRecognizers, // WebView content is not affected by the Android view's layout direction, diff --git a/packages/webview_flutter/lib/src/webview_cupertino.dart b/packages/webview_flutter/lib/src/webview_cupertino.dart index aba0a8d264d5..04ca9d78eabc 100644 --- a/packages/webview_flutter/lib/src/webview_cupertino.dart +++ b/packages/webview_flutter/lib/src/webview_cupertino.dart @@ -20,6 +20,7 @@ class CupertinoWebViewBuilder implements WebViewBuilder { Widget build({ BuildContext context, CreationParams creationParams, + @required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, WebViewPlatformCreatedCallback onWebViewPlatformCreated, Set> gestureRecognizers, }) { @@ -29,7 +30,8 @@ class CupertinoWebViewBuilder implements WebViewBuilder { if (onWebViewPlatformCreated == null) { return; } - onWebViewPlatformCreated(MethodChannelWebViewPlatform(id)); + onWebViewPlatformCreated( + MethodChannelWebViewPlatform(id, webViewPlatformCallbacksHandler)); }, gestureRecognizers: gestureRecognizers, creationParams: diff --git a/packages/webview_flutter/lib/src/webview_method_channel.dart b/packages/webview_flutter/lib/src/webview_method_channel.dart index 4166f9078399..d86a8b4cd2e7 100644 --- a/packages/webview_flutter/lib/src/webview_method_channel.dart +++ b/packages/webview_flutter/lib/src/webview_method_channel.dart @@ -10,13 +10,36 @@ import '../platform_interface.dart'; /// A [WebViewPlatform] that uses a method channel to control the webview. class MethodChannelWebViewPlatform implements WebViewPlatform { - MethodChannelWebViewPlatform(this._id) - : _channel = MethodChannel('plugins.flutter.io/webview_$_id'); + MethodChannelWebViewPlatform(int id, this._platformCallbacksHandler) + : assert(_platformCallbacksHandler != null), + _channel = MethodChannel('plugins.flutter.io/webview_$id') { + _channel.setMethodCallHandler(_onMethodCall); + } - final int _id; + final WebViewPlatformCallbacksHandler _platformCallbacksHandler; final MethodChannel _channel; + Future _onMethodCall(MethodCall call) async { + switch (call.method) { + case 'javascriptChannelMessage': + final String channel = call.arguments['channel']; + final String message = call.arguments['message']; + _platformCallbacksHandler.onJavaScriptChannelMessage(channel, message); + return true; + case 'navigationRequest': + return _platformCallbacksHandler.onNavigationRequest( + url: call.arguments['url'], + isForMainFrame: call.arguments['isForMainFrame'], + ); + case 'onPageFinished': + _platformCallbacksHandler.onPageFinished(call.arguments['url']); + return null; + } + throw MissingPluginException( + '${call.method} was invoked but has no handler'); + } + @override Future loadUrl( String url, @@ -32,6 +55,27 @@ class MethodChannelWebViewPlatform implements WebViewPlatform { }); } + @override + Future currentUrl() => _channel.invokeMethod('currentUrl'); + + @override + Future canGoBack() => _channel.invokeMethod("canGoBack"); + + @override + Future canGoForward() => _channel.invokeMethod("canGoForward"); + + @override + Future goBack() => _channel.invokeMethod("goBack"); + + @override + Future goForward() => _channel.invokeMethod("goForward"); + + @override + Future reload() => _channel.invokeMethod("reload"); + + @override + Future clearCache() => _channel.invokeMethod("clearCache"); + @override Future updateSettings(WebSettings settings) { // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. @@ -44,6 +88,23 @@ class MethodChannelWebViewPlatform implements WebViewPlatform { return _channel.invokeMethod('updateSettings', updatesMap); } + @override + Future evaluateJavascript(String javascriptString) { + return _channel.invokeMethod('evaluateJavascript', javascriptString); + } + + @override + Future addJavascriptChannels(Set javascriptChannelNames) { + return _channel.invokeMethod( + 'addJavascriptChannels', javascriptChannelNames.toList()); + } + + @override + Future removeJavascriptChannels(Set javascriptChannelNames) { + return _channel.invokeMethod( + 'removeJavascriptChannels', javascriptChannelNames.toList()); + } + static Map _webSettingsToMap(WebSettings settings) { final Map map = {}; void _addIfNonNull(String key, dynamic value) { @@ -71,7 +132,4 @@ class MethodChannelWebViewPlatform implements WebViewPlatform { 'javascriptChannelNames': creationParams.javascriptChannelNames.toList(), }; } - - @override - int get id => _id; } diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index a6126d709531..dd7e9203a667 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -264,11 +264,14 @@ class _WebViewState extends State { final Completer _controller = Completer(); + _PlatformCallbacksHandler _platformCallbacksHandler; + @override Widget build(BuildContext context) { return WebView.platformBuilder.build( context: context, onWebViewPlatformCreated: _onWebViewPlatformCreated, + webViewPlatformCallbacksHandler: _platformCallbacksHandler, gestureRecognizers: widget.gestureRecognizers, creationParams: _creationParamsfromWidget(widget), ); @@ -278,19 +281,22 @@ class _WebViewState extends State { void initState() { super.initState(); _assertJavascriptChannelNamesAreUnique(); + _platformCallbacksHandler = _PlatformCallbacksHandler(widget); } @override void didUpdateWidget(WebView oldWidget) { super.didUpdateWidget(oldWidget); _assertJavascriptChannelNamesAreUnique(); - _controller.future.then( - (WebViewController controller) => controller._updateWidget(widget)); + _controller.future.then((WebViewController controller) { + _platformCallbacksHandler._widget = widget; + controller._updateWidget(widget); + }); } - void _onWebViewPlatformCreated(WebViewPlatform platformController) { + void _onWebViewPlatformCreated(WebViewPlatform webViewPlatform) { final WebViewController controller = - WebViewController._(platformController.id, platformController, widget); + WebViewController._(widget, webViewPlatform, _platformCallbacksHandler); _controller.complete(controller); if (widget.onWebViewCreated != null) { widget.onWebViewCreated(controller); @@ -360,63 +366,70 @@ Set _extractChannelNames(Set channels) { return channelNames; } +class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { + _PlatformCallbacksHandler(this._widget) { + _updateJavascriptChannelsFromSet(_widget.javascriptChannels); + } + + WebView _widget; + + // Maps a channel name to a channel. + final Map _javascriptChannels = + {}; + + @override + void onJavaScriptChannelMessage(String channel, String message) { + _javascriptChannels[channel].onMessageReceived(JavascriptMessage(message)); + } + + @override + bool onNavigationRequest({String url, bool isForMainFrame}) { + final NavigationRequest request = + NavigationRequest._(url: url, isForMainFrame: isForMainFrame); + final bool allowNavigation = _widget.navigationDelegate == null || + _widget.navigationDelegate(request) == NavigationDecision.navigate; + return allowNavigation; + } + + @override + void onPageFinished(String url) { + if (_widget.onPageFinished != null) { + _widget.onPageFinished(url); + } + } + + void _updateJavascriptChannelsFromSet(Set channels) { + _javascriptChannels.clear(); + if (channels == null) { + return; + } + for (JavascriptChannel channel in channels) { + _javascriptChannels[channel.name] = channel; + } + } +} + /// Controls a [WebView]. /// /// A [WebViewController] instance can be obtained by setting the [WebView.onWebViewCreated] /// callback for a [WebView] widget. class WebViewController { WebViewController._( - int id, - this._platformInterface, this._widget, - ) : _channel = MethodChannel('plugins.flutter.io/webview_$id') { + this._webViewPlatform, + this._platformCallbacksHandler, + ) : assert(_webViewPlatform != null) { _settings = _webSettingsFromWidget(_widget); - _updateJavascriptChannelsFromSet(_widget.javascriptChannels); - _channel.setMethodCallHandler(_onMethodCall); } - final MethodChannel _channel; + final WebViewPlatform _webViewPlatform; - final WebViewPlatform _platformInterface; + final _PlatformCallbacksHandler _platformCallbacksHandler; WebSettings _settings; WebView _widget; - // 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(JavascriptMessage(message)); - return true; - case 'navigationRequest': - final NavigationRequest request = NavigationRequest._( - url: call.arguments['url'], - isForMainFrame: call.arguments['isForMainFrame'], - ); - // _navigationDelegate can be null if the widget was rebuilt with no - // navigation delegate after a navigation happened and just before we - // got the navigationRequest message. - final bool allowNavigation = _widget.navigationDelegate == null || - _widget.navigationDelegate(request) == NavigationDecision.navigate; - return allowNavigation; - case 'onPageFinished': - if (_widget.onPageFinished != null) { - _widget.onPageFinished(call.arguments['url']); - } - - return null; - } - throw MissingPluginException( - '${call.method} was invoked but has no handler'); - } - /// Loads the specified URL. /// /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will @@ -431,7 +444,7 @@ class WebViewController { }) async { assert(url != null); _validateUrlString(url); - return _platformInterface.loadUrl(url, headers); + return _webViewPlatform.loadUrl(url, headers); } /// Accessor to the current URL that the WebView is displaying. @@ -441,64 +454,43 @@ class WebViewController { /// current URL changes again by the time this function returns (in other /// words, by the time this future completes, the WebView may be displaying a /// different URL). - Future currentUrl() async { - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - final String url = await _channel.invokeMethod('currentUrl'); - return url; + Future currentUrl() { + return _webViewPlatform.currentUrl(); } /// Checks whether there's a back history item. /// /// Note that this operation is asynchronous, and it is possible that the "canGoBack" state has /// changed by the time the future completed. - Future canGoBack() async { - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - final bool canGoBack = await _channel.invokeMethod("canGoBack"); - return canGoBack; + Future canGoBack() { + return _webViewPlatform.canGoBack(); } /// Checks whether there's a forward history item. /// /// Note that this operation is asynchronous, and it is possible that the "canGoForward" state has /// changed by the time the future completed. - Future canGoForward() async { - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - final bool canGoForward = await _channel.invokeMethod("canGoForward"); - return canGoForward; + Future canGoForward() { + return _webViewPlatform.canGoForward(); } /// Goes back in the history of this WebView. /// /// If there is no back history item this is a no-op. - Future goBack() async { - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - return _channel.invokeMethod("goBack"); + Future goBack() { + return _webViewPlatform.goBack(); } /// Goes forward in the history of this WebView. /// /// If there is no forward history item this is a no-op. - Future goForward() async { - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - return _channel.invokeMethod("goForward"); + Future goForward() { + return _webViewPlatform.goForward(); } /// Reloads the current URL. - Future reload() async { - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - return _channel.invokeMethod("reload"); + Future reload() { + return _webViewPlatform.reload(); } /// Clears all caches used by the [WebView]. @@ -512,10 +504,7 @@ class WebViewController { /// /// Note: Calling this method also triggers a reload. Future clearCache() async { - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - await _channel.invokeMethod("clearCache"); + await _webViewPlatform.clearCache(); return reload(); } @@ -529,41 +518,25 @@ class WebViewController { final WebSettings update = _clearUnchangedWebSettings(_settings, newSettings); _settings = newSettings; - return _platformInterface.updateSettings(update); + return _webViewPlatform.updateSettings(update); } Future _updateJavascriptChannels( Set newChannels) async { - final Set currentChannels = _javascriptChannels.keys.toSet(); + final Set currentChannels = + _platformCallbacksHandler._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()); + _webViewPlatform.removeJavascriptChannels(channelsToRemove); } 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) { - _javascriptChannels.clear(); - if (channels == null) { - return; - } - for (JavascriptChannel channel in channels) { - _javascriptChannels[channel.name] = channel; + _webViewPlatform.addJavascriptChannels(channelsToAdd); } + _platformCallbacksHandler._updateJavascriptChannelsFromSet(newChannels); } /// Evaluates a JavaScript expression in the context of the current page. @@ -582,20 +555,19 @@ class WebViewController { /// When evaluating Javascript in a [WebView], it is best practice to wait for /// the [WebView.onPageFinished] callback. This guarantees all the Javascript /// embedded in the main frame HTML has been loaded. - Future evaluateJavascript(String javascriptString) async { + Future evaluateJavascript(String javascriptString) { if (_settings.javascriptMode == JavascriptMode.disabled) { - throw FlutterError( - 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.'); + return Future.error(FlutterError( + 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); } if (javascriptString == null) { - throw ArgumentError('The argument javascriptString must not be null. '); + return Future.error( + ArgumentError('The argument javascriptString must not be null.')); } // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. // https://github.com/flutter/flutter/issues/26431 // ignore: strong_mode_implicit_dynamic_method - final String result = - await _channel.invokeMethod('evaluateJavascript', javascriptString); - return result; + return _webViewPlatform.evaluateJavascript(javascriptString); } } diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart index 7176756bb79b..2a458ad434b7 100644 --- a/packages/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/test/webview_flutter_test.dart @@ -1025,7 +1025,7 @@ class _FakeCookieManager { }); break; } - return Future.sync(() {}); + return Future.sync(() => null); } void reset() { @@ -1040,18 +1040,22 @@ class MyWebViewBuilder implements WebViewBuilder { Widget build({ BuildContext context, CreationParams creationParams, + @required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, @required WebViewPlatformCreatedCallback onWebViewPlatformCreated, Set> gestureRecognizers, }) { assert(onWebViewPlatformCreated != null); - lastPlatformBuilt = MyWebViewPlatform(creationParams, gestureRecognizers); + lastPlatformBuilt = MyWebViewPlatform( + creationParams, gestureRecognizers, webViewPlatformCallbacksHandler); onWebViewPlatformCreated(lastPlatformBuilt); return Container(); } } class MyWebViewPlatform extends WebViewPlatform { - MyWebViewPlatform(this.creationParams, this.gestureRecognizers); + MyWebViewPlatform(this.creationParams, this.gestureRecognizers, + WebViewPlatformCallbacksHandler platformHandler) + : super(platformHandler); CreationParams creationParams; Set> gestureRecognizers; @@ -1066,10 +1070,6 @@ class MyWebViewPlatform extends WebViewPlatform { lastRequestHeaders = headers; return null; } - - @override - // TODO: implement id - int get id => 1; } class MatchesWebSettings extends Matcher {