diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index e1786d6cd7d0..1a490b5a579d 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.2.3 + +* Fixes bug that prevented the web view from being garbage collected. +* Fixes bug causing a `LateInitializationError` when a `PlatformNavigationDelegate` is not provided. + ## 3.2.2 * Updates example code for `use_build_context_synchronously` lint. diff --git a/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart index 3f62053d0ac3..af144e55efba 100644 --- a/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart @@ -17,6 +17,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:webview_flutter_android/src/android_webview.dart' as android; +import 'package:webview_flutter_android/src/android_webview_api_impls.dart'; import 'package:webview_flutter_android/src/instance_manager.dart'; import 'package:webview_flutter_android/src/weak_reference_utils.dart'; import 'package:webview_flutter_android/webview_flutter_android.dart'; @@ -58,15 +60,13 @@ Future main() async { ) ..loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - return PlatformWebViewWidget( - PlatformWebViewWidgetCreationParams(controller: controller), - ).build(context); - }, - ), - ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageFinished.future; @@ -96,6 +96,79 @@ Future main() async { expect(gcIdentifier, 0); }, timeout: const Timeout(Duration(seconds: 10))); + testWidgets( + 'WebView is released by garbage collection', + (WidgetTester tester) async { + final Completer webViewGCCompleter = Completer(); + + late final InstanceManager instanceManager; + instanceManager = + InstanceManager(onWeakReferenceRemoved: (int identifier) { + final Copyable instance = + instanceManager.getInstanceWithWeakReference(identifier)!; + if (instance is android.WebView && !webViewGCCompleter.isCompleted) { + webViewGCCompleter.complete(); + } + }); + + android.WebView.api = WebViewHostApiImpl( + instanceManager: instanceManager, + ); + android.WebSettings.api = WebSettingsHostApiImpl( + instanceManager: instanceManager, + ); + android.WebChromeClient.api = WebChromeClientHostApiImpl( + instanceManager: instanceManager, + ); + + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + AndroidWebViewWidgetCreationParams( + instanceManager: instanceManager, + controller: PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ), + ), + ).build(context); + }, + ), + ); + await tester.pumpAndSettle(); + + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + AndroidWebViewWidgetCreationParams( + instanceManager: instanceManager, + controller: PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ), + ), + ).build(context); + }, + ), + ); + await tester.pumpAndSettle(); + + // Force garbage collection. + await IntegrationTestWidgetsFlutterBinding.instance + .watchPerformance(() async { + await tester.pumpAndSettle(); + }); + + await tester.pumpAndSettle(); + await expectLater(webViewGCCompleter.future, completes); + + android.WebView.api = WebViewHostApiImpl(); + android.WebSettings.api = WebSettingsHostApiImpl(); + android.WebChromeClient.api = WebChromeClientHostApiImpl(); + }, + timeout: const Timeout(Duration(seconds: 10)), + ); + testWidgets('runJavaScriptReturningResult', (WidgetTester tester) async { final Completer pageFinished = Completer(); @@ -110,15 +183,13 @@ Future main() async { ) ..loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - return PlatformWebViewWidget( - PlatformWebViewWidgetCreationParams(controller: controller), - ).build(context); - }, - ), - ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageFinished.future; @@ -151,15 +222,13 @@ Future main() async { ), ); - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - return PlatformWebViewWidget( - PlatformWebViewWidgetCreationParams(controller: controller), - ).build(context); - }, - ), - ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageLoads.stream.firstWhere((String url) => url == headersUrl); @@ -195,15 +264,13 @@ Future main() async { 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', ); - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - return PlatformWebViewWidget( - PlatformWebViewWidgetCreationParams(controller: controller), - ).build(context); - }, - ), - ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageFinished.future; @@ -258,15 +325,13 @@ Future main() async { ..setUserAgent('Custom_User_Agent1') ..loadRequest(LoadRequestParams(uri: Uri.parse('about:blank'))); - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - return PlatformWebViewWidget( - PlatformWebViewWidgetCreationParams(controller: controller), - ).build(context); - }, - ), - ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageFinished.future; @@ -335,15 +400,13 @@ Future main() async { ), ); - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - return PlatformWebViewWidget( - PlatformWebViewWidgetCreationParams(controller: controller), - ).build(context); - }, - ), - ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageLoaded.future; @@ -369,15 +432,13 @@ Future main() async { ), ); - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - return PlatformWebViewWidget( - PlatformWebViewWidgetCreationParams(controller: controller), - ).build(context); - }, - ), - ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageLoaded.future; @@ -491,15 +552,13 @@ Future main() async { ), ); - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - return PlatformWebViewWidget( - PlatformWebViewWidgetCreationParams(controller: controller), - ).build(context); - }, - ), - ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageLoaded.future; @@ -525,15 +584,13 @@ Future main() async { ), ); - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - return PlatformWebViewWidget( - PlatformWebViewWidgetCreationParams(controller: controller), - ).build(context); - }, - ), - ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageLoaded.future; @@ -573,15 +630,13 @@ Future main() async { ), ); - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - return PlatformWebViewWidget( - PlatformWebViewWidgetCreationParams(controller: controller), - ).build(context); - }, - ), - ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageLoaded.future; diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart index 6f4af3ee7476..fd287a515c65 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart @@ -96,18 +96,20 @@ class AndroidWebViewController extends PlatformWebViewController { ); late final android_webview.WebChromeClient _webChromeClient = - withWeakReferenceTo(this, - (WeakReference weakReference) { - return _androidWebViewParams.androidWebViewProxy - .createAndroidWebChromeClient( - onProgressChanged: (android_webview.WebView webView, int progress) { - if (weakReference.target?._currentNavigationDelegate._onProgress != + _androidWebViewParams.androidWebViewProxy.createAndroidWebChromeClient( + onProgressChanged: withWeakReferenceTo(this, + (WeakReference weakReference) { + return (android_webview.WebView webView, int progress) { + if (weakReference.target?._currentNavigationDelegate?._onProgress != null) { weakReference - .target!._currentNavigationDelegate._onProgress!(progress); + .target!._currentNavigationDelegate!._onProgress!(progress); } - }, - onShowFileChooser: (android_webview.WebView webView, + }; + }), + onShowFileChooser: withWeakReferenceTo(this, + (WeakReference weakReference) { + return (android_webview.WebView webView, android_webview.FileChooserParams params) async { if (weakReference.target?._onShowFileSelectorCallback != null) { return weakReference.target!._onShowFileSelectorCallback!( @@ -115,9 +117,9 @@ class AndroidWebViewController extends PlatformWebViewController { ); } return []; - }, - ); - }); + }; + }), + ); /// The native [android_webview.FlutterAssetManager] allows managing assets. late final android_webview.FlutterAssetManager _flutterAssetManager = @@ -126,10 +128,7 @@ class AndroidWebViewController extends PlatformWebViewController { final Map _javaScriptChannelParams = {}; - // The keeps a reference to the current NavigationDelegate so that the - // callback methods remain reachable. - // ignore: unused_field - late AndroidNavigationDelegate _currentNavigationDelegate; + AndroidNavigationDelegate? _currentNavigationDelegate; Future> Function(FileSelectorParams)? _onShowFileSelectorCallback; diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 81255dfa0f93..30ea5d7823e9 100644 --- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_android description: A Flutter plugin that provides a WebView widget on Android. repository: https://github.com/flutter/plugins/tree/main/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 3.2.2 +version: 3.2.3 environment: sdk: ">=2.17.0 <3.0.0" diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart index 80fa1924487c..03e71ec5d987 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart @@ -539,6 +539,19 @@ void main() { expect(callbackProgress, 42); }); + test('onProgress does not cause LateInitializationError', () { + // ignore: unused_local_variable + final AndroidWebViewController controller = createControllerWithMocks( + createWebChromeClient: CapturingWebChromeClient.new, + ); + + // Should not cause LateInitializationError + CapturingWebChromeClient.lastCreatedDelegate.onProgressChanged!( + android_webview.WebView.detached(), + 42, + ); + }); + test('setOnShowFileSelector', () async { late final Future> Function( android_webview.WebView webView, diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index a9cb87f57c65..f79058c80216 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.0.4 + +* Fixes bug that prevented the web view from being garbage collected. + ## 3.0.3 * Updates example code for `use_build_context_synchronously` lint. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart index 946f27b5df83..16411b8140a5 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart @@ -21,6 +21,7 @@ import 'package:integration_test/integration_test.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'package:webview_flutter_wkwebview/src/common/instance_manager.dart'; import 'package:webview_flutter_wkwebview/src/common/weak_reference_utils.dart'; +import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; Future main() async { @@ -47,7 +48,7 @@ Future main() async { final String headersUrl = '$prefixUrl/headers'; testWidgets( - 'withWeakRefenceTo allows encapsulating class to be garbage collected', + 'withWeakReferenceTo allows encapsulating class to be garbage collected', (WidgetTester tester) async { final Completer gcCompleter = Completer(); final InstanceManager instanceManager = InstanceManager( @@ -68,21 +69,102 @@ Future main() async { expect(gcIdentifier, 0); }, timeout: const Timeout(Duration(seconds: 10))); + testWidgets( + 'WKWebView is released by garbage collection', + (WidgetTester tester) async { + final Completer webViewGCCompleter = Completer(); + + late final InstanceManager instanceManager; + instanceManager = + InstanceManager(onWeakReferenceRemoved: (int identifier) { + final Copyable instance = + instanceManager.getInstanceWithWeakReference(identifier)!; + if (instance is WKWebView && !webViewGCCompleter.isCompleted) { + webViewGCCompleter.complete(); + } + }); + + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + WebKitWebViewWidgetCreationParams( + instanceManager: instanceManager, + controller: PlatformWebViewController( + WebKitWebViewControllerCreationParams( + instanceManager: instanceManager, + ), + ), + ), + ).build(context); + }, + ), + ); + await tester.pumpAndSettle(); + + await tester.pumpWidget(Container()); + + // Force garbage collection. + await IntegrationTestWidgetsFlutterBinding.instance + .watchPerformance(() async { + await tester.pumpAndSettle(); + }); + + await expectLater(webViewGCCompleter.future, completes); + }, + timeout: const Timeout(Duration(seconds: 10)), + ); + testWidgets('loadRequest', (WidgetTester tester) async { + final Completer pageFinished = Completer(); + final PlatformWebViewController controller = PlatformWebViewController( const PlatformWebViewControllerCreationParams(), - ); - controller.loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); + ) + ..setPlatformNavigationDelegate( + PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageFinished.complete()), + ) + ..loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + + await pageFinished.future; final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); }); testWidgets('runJavaScriptReturningResult', (WidgetTester tester) async { + final Completer pageFinished = Completer(); + final PlatformWebViewController controller = PlatformWebViewController( const PlatformWebViewControllerCreationParams(), - ); - controller.loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageFinished.complete()), + ) + ..loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + + await pageFinished.future; await expectLater( controller.runJavaScriptReturningResult('1 + 1'), @@ -113,6 +195,14 @@ Future main() async { ), ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + await pageLoads.stream.firstWhere((String url) => url == headersUrl); final String content = await controller.runJavaScriptReturningResult( @@ -147,6 +237,14 @@ Future main() async { 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + await pageFinished.future; await controller.runJavaScript('Echo.postMessage("hello");'); @@ -184,6 +282,14 @@ Future main() async { ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setUserAgent('Custom_User_Agent1'); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + final String customUserAgent2 = await _getUserAgent(controller); expect(customUserAgent2, 'Custom_User_Agent1'); }); @@ -250,6 +356,14 @@ Future main() async { ), ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + await pageLoaded.future; bool isPaused = @@ -274,6 +388,14 @@ Future main() async { ), ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + await pageLoaded.future; isPaused = @@ -447,6 +569,14 @@ Future main() async { ), ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + await pageLoaded.future; bool isPaused = @@ -471,6 +601,14 @@ Future main() async { ), ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + await pageLoaded.future; isPaused = @@ -509,6 +647,14 @@ Future main() async { ), ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + await pageLoaded.future; // On at least iOS, it does not appear to be guaranteed that the native @@ -565,6 +711,14 @@ Future main() async { ), ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + await pageLoaded.future; await tester.pumpAndSettle(const Duration(seconds: 3)); @@ -599,8 +753,7 @@ Future main() async { '${base64Encode(const Utf8Encoder().convert(blankPage))}'; testWidgets('can allow requests', (WidgetTester tester) async { - final StreamController pageLoads = - StreamController.broadcast(); + Completer pageLoaded = Completer(); final PlatformWebViewController controller = PlatformWebViewController( WebKitWebViewControllerCreationParams(), @@ -610,7 +763,7 @@ Future main() async { WebKitNavigationDelegate( const WebKitNavigationDelegateCreationParams(), ) - ..setOnPageFinished((String url) => pageLoads.add(url)) + ..setOnPageFinished((_) => pageLoaded.complete()) ..setOnNavigationRequest((NavigationRequest navigationRequest) { return (navigationRequest.url.contains('youtube.com')) ? NavigationDecision.prevent @@ -621,10 +774,20 @@ Future main() async { LoadRequestParams(uri: Uri.parse(blankPageEncoded)), ); - await pageLoads.stream.first; // Wait for initial page load. + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + + await pageLoaded.future; // Wait for initial page load. + + pageLoaded = Completer(); await controller.runJavaScript('location.href = "$secondaryUrl"'); + await pageLoaded.future; - await pageLoads.stream.first; // Wait for the next page load. final String? currentUrl = await controller.currentUrl(); expect(currentUrl, secondaryUrl); }); @@ -633,7 +796,7 @@ Future main() async { final Completer errorCompleter = Completer(); - PlatformWebViewController( + final PlatformWebViewController controller = PlatformWebViewController( WebKitWebViewControllerCreationParams(), ) ..setJavaScriptMode(JavaScriptMode.unrestricted) @@ -648,6 +811,14 @@ Future main() async { LoadRequestParams(uri: Uri.parse('https://www.notawebsite..com')), ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + final WebResourceError error = await errorCompleter.future; expect(error, isNotNull); @@ -660,7 +831,7 @@ Future main() async { Completer(); final Completer pageFinishCompleter = Completer(); - PlatformWebViewController( + final PlatformWebViewController controller = PlatformWebViewController( WebKitWebViewControllerCreationParams(), ) ..setJavaScriptMode(JavaScriptMode.unrestricted) @@ -681,6 +852,14 @@ Future main() async { ), ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + expect(errorCompleter.future, doesNotComplete); await pageFinishCompleter.future; }); @@ -706,7 +885,7 @@ Future main() async { Completer(); final Completer pageFinishCompleter = Completer(); - PlatformWebViewController( + final PlatformWebViewController controller = PlatformWebViewController( WebKitWebViewControllerCreationParams(), ) ..setJavaScriptMode(JavaScriptMode.unrestricted) @@ -727,14 +906,21 @@ Future main() async { ), ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + expect(errorCompleter.future, doesNotComplete); await pageFinishCompleter.future; }, ); testWidgets('can block requests', (WidgetTester tester) async { - final StreamController pageLoads = - StreamController.broadcast(); + Completer pageLoaded = Completer(); final PlatformWebViewController controller = PlatformWebViewController( WebKitWebViewControllerCreationParams(), @@ -744,7 +930,7 @@ Future main() async { WebKitNavigationDelegate( const WebKitNavigationDelegateCreationParams(), ) - ..setOnPageFinished((String url) => pageLoads.add(url)) + ..setOnPageFinished((_) => pageLoaded.complete()) ..setOnNavigationRequest((NavigationRequest navigationRequest) { return (navigationRequest.url.contains('youtube.com')) ? NavigationDecision.prevent @@ -753,22 +939,31 @@ Future main() async { ) ..loadRequest(LoadRequestParams(uri: Uri.parse(blankPageEncoded))); - await pageLoads.stream.first; // Wait for initial page load. + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + + await pageLoaded.future; // Wait for initial page load. + + pageLoaded = Completer(); await controller .runJavaScript('location.href = "https://www.youtube.com/"'); // There should never be any second page load, since our new URL is // blocked. Still wait for a potential page change for some time in order // to give the test a chance to fail. - await pageLoads.stream.first + await pageLoaded.future .timeout(const Duration(milliseconds: 500), onTimeout: () => ''); final String? currentUrl = await controller.currentUrl(); expect(currentUrl, isNot(contains('youtube.com'))); }); testWidgets('supports asynchronous decisions', (WidgetTester tester) async { - final StreamController pageLoads = - StreamController.broadcast(); + Completer pageLoaded = Completer(); final PlatformWebViewController controller = PlatformWebViewController( WebKitWebViewControllerCreationParams(), @@ -778,7 +973,7 @@ Future main() async { WebKitNavigationDelegate( const WebKitNavigationDelegateCreationParams(), ) - ..setOnPageFinished((String url) => pageLoads.add(url)) + ..setOnPageFinished((_) => pageLoaded.complete()) ..setOnNavigationRequest( (NavigationRequest navigationRequest) async { NavigationDecision decision = NavigationDecision.prevent; @@ -790,10 +985,20 @@ Future main() async { ) ..loadRequest(LoadRequestParams(uri: Uri.parse(blankPageEncoded))); - await pageLoads.stream.first; // Wait for initial page load. + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + + await pageLoaded.future; // Wait for initial page load. + + pageLoaded = Completer(); await controller.runJavaScript('location.href = "$secondaryUrl"'); - await pageLoads.stream.first; // Wait for second page to load. + await pageLoaded.future; // Wait for second page to load. final String? currentUrl = await controller.currentUrl(); expect(currentUrl, secondaryUrl); }); @@ -807,6 +1012,14 @@ Future main() async { ..setAllowsBackForwardNavigationGestures(true) ..loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); }); @@ -824,6 +1037,15 @@ Future main() async { )..setOnPageFinished((_) => pageLoaded.complete())); await controller.runJavaScript('window.open("$primaryUrl", "_blank")'); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + await pageLoaded.future; final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); @@ -843,6 +1065,14 @@ Future main() async { )..setOnPageFinished((_) => pageLoaded.complete())) ..loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + expect(controller.currentUrl(), completion(primaryUrl)); await pageLoaded.future; pageLoaded = Completer(); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart index 2cdc7e269454..3e8d6796069b 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'common/instance_manager.dart'; import 'foundation/foundation.dart'; import 'web_kit/web_kit.dart'; @@ -39,10 +40,13 @@ class WebKitProxy { Map change, )? observeValue, + InstanceManager? instanceManager, }) createWebView; /// Constructs a [WKWebViewConfiguration]. - final WKWebViewConfiguration Function() createWebViewConfiguration; + final WKWebViewConfiguration Function({ + InstanceManager? instanceManager, + }) createWebViewConfiguration; /// Constructs a [WKScriptMessageHandler]. final WKScriptMessageHandler Function({ @@ -72,7 +76,7 @@ class WebKitProxy { void Function(WKWebView webView)? webViewWebContentProcessDidTerminate, }) createNavigationDelegate; - /// Contructs a [WKUIDelegate]. + /// Constructs a [WKUIDelegate]. final WKUIDelegate Function({ void Function( WKWebView webView, diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart index dc90906d78f4..02b5b73b5971 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart @@ -49,7 +49,12 @@ class WebKitWebViewControllerCreationParams PlaybackMediaTypes.video, }, this.allowsInlineMediaPlayback = false, - }) : _configuration = webKitProxy.createWebViewConfiguration() { + @visibleForTesting InstanceManager? instanceManager, + }) : _instanceManager = instanceManager ?? NSObject.globalInstanceManager { + _configuration = webKitProxy.createWebViewConfiguration( + instanceManager: _instanceManager, + ); + if (mediaTypesRequiringUserAction.isEmpty) { _configuration.setMediaTypesRequiringUserActionForPlayback( {WKAudiovisualMediaType.none}, @@ -79,13 +84,15 @@ class WebKitWebViewControllerCreationParams PlaybackMediaTypes.video, }, bool allowsInlineMediaPlayback = false, + @visibleForTesting InstanceManager? instanceManager, }) : this( webKitProxy: webKitProxy, mediaTypesRequiringUserAction: mediaTypesRequiringUserAction, allowsInlineMediaPlayback: allowsInlineMediaPlayback, + instanceManager: instanceManager, ); - final WKWebViewConfiguration _configuration; + late final WKWebViewConfiguration _configuration; /// Media types that require a user gesture to begin playing. /// @@ -102,6 +109,10 @@ class WebKitWebViewControllerCreationParams /// native library. @visibleForTesting final WebKitProxy webKitProxy; + + // Maintains instances used to communicate with the native objects they + // represent. + final InstanceManager _instanceManager; } /// An implementation of [PlatformWebViewController] with the WebKit api. @@ -122,12 +133,12 @@ class WebKitWebViewController extends PlatformWebViewController { } /// The WebKit WebView being controlled. - late final WKWebView _webView = withWeakRefenceTo(this, ( - WeakReference weakReference, - ) { - return _webKitParams.webKitProxy.createWebView( - _webKitParams._configuration, - observeValue: ( + late final WKWebView _webView = _webKitParams.webKitProxy.createWebView( + _webKitParams._configuration, + observeValue: withWeakRefenceTo(this, ( + WeakReference weakReference, + ) { + return ( String keyPath, NSObject object, Map change, @@ -139,9 +150,10 @@ class WebKitWebViewController extends PlatformWebViewController { change[NSKeyValueChangeKey.newValue]! as double; progressCallback((progress * 100).round()); } - }, - ); - }); + }; + }), + instanceManager: _webKitParams._instanceManager, + ); final Map _javaScriptChannelParams = {}; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index c41bce18cae6..15e8ac5678f8 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_wkwebview description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control. repository: https://github.com/flutter/plugins/tree/main/packages/webview_flutter/webview_flutter_wkwebview issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 3.0.3 +version: 3.0.4 environment: sdk: ">=2.17.0 <3.0.0" diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart index fc06db24f055..0360c13b052a 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart @@ -13,6 +13,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_wkwebview/src/common/instance_manager.dart'; import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'; import 'package:webview_flutter_wkwebview/src/ui_kit/ui_kit.dart'; import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; @@ -49,6 +50,7 @@ void main() { })? createMockWebView, MockWKWebViewConfiguration? mockWebViewConfiguration, + InstanceManager? instanceManager, }) { final MockWKWebViewConfiguration nonNullMockWebViewConfiguration = mockWebViewConfiguration ?? MockWKWebViewConfiguration(); @@ -57,7 +59,9 @@ void main() { final PlatformWebViewControllerCreationParams controllerCreationParams = WebKitWebViewControllerCreationParams( webKitProxy: WebKitProxy( - createWebViewConfiguration: () => nonNullMockWebViewConfiguration, + createWebViewConfiguration: ({InstanceManager? instanceManager}) { + return nonNullMockWebViewConfiguration; + }, createWebView: ( _, { void Function( @@ -66,6 +70,7 @@ void main() { Map change, )? observeValue, + InstanceManager? instanceManager, }) { nonNullMockWebView = createMockWebView == null ? MockWKWebView() @@ -104,7 +109,9 @@ void main() { WebKitWebViewControllerCreationParams( webKitProxy: WebKitProxy( - createWebViewConfiguration: () => mockConfiguration, + createWebViewConfiguration: ({InstanceManager? instanceManager}) { + return mockConfiguration; + }, ), allowsInlineMediaPlayback: true, ); @@ -120,7 +127,9 @@ void main() { WebKitWebViewControllerCreationParams( webKitProxy: WebKitProxy( - createWebViewConfiguration: () => mockConfiguration, + createWebViewConfiguration: ({InstanceManager? instanceManager}) { + return mockConfiguration; + }, ), mediaTypesRequiringUserAction: const { PlaybackMediaTypes.video, @@ -143,7 +152,9 @@ void main() { WebKitWebViewControllerCreationParams( webKitProxy: WebKitProxy( - createWebViewConfiguration: () => mockConfiguration, + createWebViewConfiguration: ({InstanceManager? instanceManager}) { + return mockConfiguration; + }, ), ); @@ -164,7 +175,9 @@ void main() { WebKitWebViewControllerCreationParams( webKitProxy: WebKitProxy( - createWebViewConfiguration: () => mockConfiguration, + createWebViewConfiguration: ({InstanceManager? instanceManager}) { + return mockConfiguration; + }, ), mediaTypesRequiringUserAction: const {}, ); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.dart index 2e0d6e3e9af3..2a6434be4f03 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.dart @@ -19,7 +19,7 @@ void main() { group('WebKitWebViewWidget', () { testWidgets('build', (WidgetTester tester) async { - final InstanceManager instanceManager = InstanceManager( + final InstanceManager testInstanceManager = InstanceManager( onWeakReferenceRemoved: (_) {}, ); @@ -34,14 +34,17 @@ void main() { Map change, )? observeValue, + InstanceManager? instanceManager, }) { final WKWebView webView = WKWebView.detached( - instanceManager: instanceManager, + instanceManager: testInstanceManager, ); - instanceManager.addDartCreatedInstance(webView); + testInstanceManager.addDartCreatedInstance(webView); return webView; }, - createWebViewConfiguration: () => MockWKWebViewConfiguration(), + createWebViewConfiguration: ({InstanceManager? instanceManager}) { + return MockWKWebViewConfiguration(); + }, ), ), ); @@ -50,7 +53,7 @@ void main() { WebKitWebViewWidgetCreationParams( key: const Key('keyValue'), controller: controller, - instanceManager: instanceManager, + instanceManager: testInstanceManager, ), );