diff --git a/packages/webview_flutter/lib/platform_interface.dart b/packages/webview_flutter/lib/platform_interface.dart new file mode 100644 index 000000000000..f5918d80aef6 --- /dev/null +++ b/packages/webview_flutter/lib/platform_interface.dart @@ -0,0 +1,74 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; + +/// 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 { + /// Loads the specified URL. + /// + /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will + /// be added as key value pairs of HTTP headers for the request. + /// + /// `url` must not be null. + /// + /// Throws an ArgumentError if `url` is not a valid URL string. + Future loadUrl( + String url, + Map headers, + ) { + throw UnimplementedError( + "WebView loadUrl 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; +} + +typedef WebViewPlatformCreatedCallback = void Function( + WebViewPlatform webViewPlatform); + +/// Interface building a platform WebView implementation. +/// +/// [WebView#platformBuilder] controls the builder that is used by [WebView]. +/// [AndroidWebViewPlatform] and [CupertinoWebViewPlatform] are the default implementations +/// for Android and iOS respectively. +abstract class WebViewBuilder { + /// Builds a new WebView. + /// + /// Returns a Widget tree that embeds the created webview. + /// + /// `creationParams` are the initial parameters used to setup the webview. + /// + /// `onWebViewPlatformCreated` will be invoked after the platform specific [WebViewPlatform] + /// implementation is created with the [WebViewPlatform] instance as a parameter. + /// + /// `gestureRecognizers` specifies which gestures should be consumed by the web view. + /// It is possible for other gesture recognizers to be competing with the web view on pointer + /// events, e.g if the web view is inside a [ListView] the [ListView] will want to handle + /// vertical drags. The web view will claim gestures that are recognized by any of the + /// 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. + 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. + Map creationParams, + WebViewPlatformCreatedCallback onWebViewPlatformCreated, + Set> gestureRecognizers, + }); +} diff --git a/packages/webview_flutter/lib/src/webview_android.dart b/packages/webview_flutter/lib/src/webview_android.dart new file mode 100644 index 000000000000..86a20b27dcb2 --- /dev/null +++ b/packages/webview_flutter/lib/src/webview_android.dart @@ -0,0 +1,54 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +import '../platform_interface.dart'; +import 'webview_method_channel.dart'; + +/// Builds an Android webview. +/// +/// This is used as the default implementation for [WebView.platformBuilder] on Android. It uses +/// an [AndroidView] to embed the webview in the widget hierarchy, and uses a method channel to +/// communicate with the platform code. +class AndroidWebViewBuilder implements WebViewBuilder { + @override + Widget build({ + BuildContext context, + Map creationParams, + WebViewPlatformCreatedCallback onWebViewPlatformCreated, + Set> gestureRecognizers, + }) { + 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: + // https://github.com/flutter/flutter/issues/24585 - the text selection + // dialog is not responding to touch events. + // https://github.com/flutter/flutter/issues/24584 - the text selection + // handles are not showing. + // TODO(amirh): remove this when the issues above are fixed. + onLongPress: () {}, + excludeFromSemantics: true, + child: AndroidView( + viewType: 'plugins.flutter.io/webview', + onPlatformViewCreated: (int id) { + if (onWebViewPlatformCreated == null) { + return; + } + onWebViewPlatformCreated(MethodChannelWebViewPlatform(id)); + }, + gestureRecognizers: gestureRecognizers, + // WebView content is not affected by the Android view's layout direction, + // we explicitly set it here so that the widget doesn't require an ambient + // directionality. + layoutDirection: TextDirection.rtl, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + ), + ); + } +} diff --git a/packages/webview_flutter/lib/src/webview_cupertino.dart b/packages/webview_flutter/lib/src/webview_cupertino.dart new file mode 100644 index 000000000000..e5487f435230 --- /dev/null +++ b/packages/webview_flutter/lib/src/webview_cupertino.dart @@ -0,0 +1,39 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +import '../platform_interface.dart'; +import 'webview_method_channel.dart'; + +/// Builds an iOS webview. +/// +/// This is used as the default implementation for [WebView.platformBuilder] on iOS. It uses +/// a [UiKitView] to embed the webview in the widget hierarchy, and uses a method channel to +/// communicate with the platform code. +class CupertinoWebViewBuilder implements WebViewBuilder { + @override + Widget build({ + BuildContext context, + Map creationParams, + WebViewPlatformCreatedCallback onWebViewPlatformCreated, + Set> gestureRecognizers, + }) { + return UiKitView( + viewType: 'plugins.flutter.io/webview', + onPlatformViewCreated: (int id) { + if (onWebViewPlatformCreated == null) { + return; + } + onWebViewPlatformCreated(MethodChannelWebViewPlatform(id)); + }, + gestureRecognizers: gestureRecognizers, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + ); + } +} diff --git a/packages/webview_flutter/lib/src/webview_method_channel.dart b/packages/webview_flutter/lib/src/webview_method_channel.dart new file mode 100644 index 000000000000..e13fe6f4e5e5 --- /dev/null +++ b/packages/webview_flutter/lib/src/webview_method_channel.dart @@ -0,0 +1,37 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/services.dart'; + +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'); + + final int _id; + + final MethodChannel _channel; + + @override + Future loadUrl( + String url, + Map headers, + ) async { + assert(url != 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 + return _channel.invokeMethod('loadUrl', { + 'url': url, + 'headers': headers, + }); + } + + @override + int get id => _id; +} diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index 1dabba1a0acf..432de6e95d5a 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -9,6 +9,10 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import 'platform_interface.dart'; +import 'src/webview_android.dart'; +import 'src/webview_cupertino.dart'; + typedef void WebViewCreatedCallback(WebViewController controller); enum JavascriptMode { @@ -121,6 +125,39 @@ class WebView extends StatefulWidget { }) : assert(javascriptMode != null), super(key: key); + static WebViewBuilder _platformBuilder; + + /// Sets a custom [WebViewBuilder]. + /// + /// This property can be set to use a custom platform implementation for WebViews. + /// + /// Setting `platformBuilder` doesn't affect [WebView]s that were already created. + /// + /// The default value is [AndroidWebViewBuilder] on Android and [CupertinoWebViewBuilder] on iOs. + static set platformBuilder(WebViewBuilder platformBuilder) { + _platformBuilder = platformBuilder; + } + + /// The [WebViewBuilder] that's used to create new [WebView]s. + /// + /// The default value is [AndroidWebViewBuilder] on Android and [CupertinoWebViewBuilder] on iOs. + static WebViewBuilder get platformBuilder { + if (_platformBuilder == null) { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + _platformBuilder = AndroidWebViewBuilder(); + break; + case TargetPlatform.iOS: + _platformBuilder = CupertinoWebViewBuilder(); + break; + default: + throw UnsupportedError( + "Trying to use the default webview implementation for $defaultTargetPlatform but there isn't a default one"); + } + } + return _platformBuilder; + } + /// If not null invoked once the web view is created. final WebViewCreatedCallback onWebViewCreated; @@ -229,40 +266,12 @@ class _WebViewState extends State { @override Widget build(BuildContext context) { - if (defaultTargetPlatform == TargetPlatform.android) { - 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: - // https://github.com/flutter/flutter/issues/24585 - the text selection - // dialog is not responding to touch events. - // https://github.com/flutter/flutter/issues/24584 - the text selection - // handles are not showing. - // TODO(amirh): remove this when the issues above are fixed. - onLongPress: () {}, - excludeFromSemantics: true, - child: AndroidView( - viewType: 'plugins.flutter.io/webview', - onPlatformViewCreated: _onPlatformViewCreated, - gestureRecognizers: widget.gestureRecognizers, - // WebView content is not affected by the Android view's layout direction, - // we explicitly set it here so that the widget doesn't require an ambient - // directionality. - layoutDirection: TextDirection.rtl, - creationParams: _CreationParams.fromWidget(widget).toMap(), - creationParamsCodec: const StandardMessageCodec(), - ), - ); - } else if (defaultTargetPlatform == TargetPlatform.iOS) { - return UiKitView( - viewType: 'plugins.flutter.io/webview', - onPlatformViewCreated: _onPlatformViewCreated, - gestureRecognizers: widget.gestureRecognizers, - creationParams: _CreationParams.fromWidget(widget).toMap(), - creationParamsCodec: const StandardMessageCodec(), - ); - } - return Text( - '$defaultTargetPlatform is not yet supported by the webview_flutter plugin'); + return WebView.platformBuilder.build( + context: context, + creationParams: _CreationParams.fromWidget(widget).toMap(), + onWebViewPlatformCreated: _onWebViewPlatformCreated, + gestureRecognizers: widget.gestureRecognizers, + ); } @override @@ -279,8 +288,9 @@ class _WebViewState extends State { (WebViewController controller) => controller._updateWidget(widget)); } - void _onPlatformViewCreated(int id) { - final WebViewController controller = WebViewController._(id, widget); + void _onWebViewPlatformCreated(WebViewPlatform platformController) { + final WebViewController controller = + WebViewController._(platformController.id, platformController, widget); _controller.complete(controller); if (widget.onWebViewCreated != null) { widget.onWebViewCreated(controller); @@ -385,6 +395,7 @@ class _WebSettings { class WebViewController { WebViewController._( int id, + this._platformInterface, this._widget, ) : _channel = MethodChannel('plugins.flutter.io/webview_$id') { _settings = _WebSettings.fromWidget(_widget); @@ -394,6 +405,8 @@ class WebViewController { final MethodChannel _channel; + final WebViewPlatform _platformInterface; + _WebSettings _settings; WebView _widget; @@ -434,6 +447,9 @@ class WebViewController { /// Loads the specified URL. /// + /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will + /// be added as key value pairs of HTTP headers for the request. + /// /// `url` must not be null. /// /// Throws an ArgumentError if `url` is not a valid URL string. @@ -443,13 +459,7 @@ class WebViewController { }) async { assert(url != null); _validateUrlString(url); - // 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('loadUrl', { - 'url': url, - 'headers': headers, - }); + return _platformInterface.loadUrl(url, headers); } /// Accessor to the current URL that the WebView is displaying. diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart index 699b8918e6f2..a973e43e6081 100644 --- a/packages/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/test/webview_flutter_test.dart @@ -6,8 +6,11 @@ import 'dart:math'; import 'dart:typed_data'; import 'package:flutter/services.dart'; +import 'package:flutter/src/foundation/basic_types.dart'; +import 'package:flutter/src/gestures/recognizer.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter/platform_interface.dart'; import 'package:webview_flutter/webview_flutter.dart'; typedef void VoidCallback(); @@ -747,6 +750,60 @@ void main() { expect(platformWebView.debuggingEnabled, false); }); }); + + group('Custom platform implementation', () { + setUpAll(() { + WebView.platformBuilder = MyWebViewBuilder(); + }); + tearDownAll(() { + WebView.platformBuilder = null; + }); + + testWidgets('creation', (WidgetTester tester) async { + await tester.pumpWidget( + const WebView( + initialUrl: 'https://youtube.com', + ), + ); + + final MyWebViewBuilder builder = WebView.platformBuilder; + final MyWebViewPlatform platform = builder.lastPlatformBuilt; + + expect(platform.creationParams, { + 'initialUrl': 'https://youtube.com', + 'settings': { + 'jsMode': 0, + 'hasNavigationDelegate': false, + 'debuggingEnabled': false + }, + 'javascriptChannelNames': [], + }); + }); + + testWidgets('loadUrl', (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + final MyWebViewBuilder builder = WebView.platformBuilder; + final MyWebViewPlatform platform = builder.lastPlatformBuilt; + + final Map headers = { + 'header': 'value', + }; + + await controller.loadUrl('https://google.com', headers: headers); + + expect(platform.lastUrlLoaded, 'https://google.com'); + expect(platform.lastRequestHeaders, headers); + }); + }); } class FakePlatformWebView { @@ -963,3 +1020,41 @@ class _FakeCookieManager { hasCookies = true; } } + +class MyWebViewBuilder implements WebViewBuilder { + MyWebViewPlatform lastPlatformBuilt; + + @override + Widget build({ + BuildContext context, + Map creationParams, + @required WebViewPlatformCreatedCallback onWebViewPlatformCreated, + Set> gestureRecognizers, + }) { + assert(onWebViewPlatformCreated != null); + lastPlatformBuilt = MyWebViewPlatform(creationParams, gestureRecognizers); + onWebViewPlatformCreated(lastPlatformBuilt); + return Container(); + } +} + +class MyWebViewPlatform extends WebViewPlatform { + MyWebViewPlatform(this.creationParams, this.gestureRecognizers); + + Map creationParams; + Set> gestureRecognizers; + + String lastUrlLoaded; + Map lastRequestHeaders; + + @override + Future loadUrl(String url, Map headers) { + lastUrlLoaded = url; + lastRequestHeaders = headers; + return null; + } + + @override + // TODO: implement id + int get id => 1; +}