From 76f294c556baef50b163f1198d6c9b85f4f67288 Mon Sep 17 00:00:00 2001 From: Maurice Parrish <10687576+bparrishMines@users.noreply.github.com> Date: Fri, 20 Jan 2023 01:29:52 -0500 Subject: [PATCH 01/13] fix and test non disposing webview --- .../webview_flutter_test.dart | 46 ++++++++++++++++++- .../lib/src/webkit_proxy.dart | 8 +++- .../lib/src/webkit_webview_controller.dart | 34 +++++++++----- .../test/webkit_webview_controller_test.dart | 23 ++++++++-- .../test/webkit_webview_widget_test.dart | 13 ++++-- 5 files changed, 100 insertions(+), 24 deletions(-) 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..eae5ae6c4e04 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,6 +69,40 @@ 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) { + if (!webViewGCCompleter.isCompleted) { + webViewGCCompleter.complete(); + } + } + }); + + // ignore: unused_local_variable + WebKitWebViewController? controller = WebKitWebViewController( + WebKitWebViewControllerCreationParams(instanceManager: instanceManager), + ); + controller = null; + + // 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 PlatformWebViewController controller = PlatformWebViewController( const PlatformWebViewControllerCreationParams(), @@ -113,6 +148,8 @@ Future main() async { ), ); + await tester.pumpAndSettle(); + await pageLoads.stream.firstWhere((String url) => url == headersUrl); final String content = await controller.runJavaScriptReturningResult( @@ -147,6 +184,8 @@ Future main() async { 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', ); + await tester.pumpAndSettle(); + await pageFinished.future; await controller.runJavaScript('Echo.postMessage("hello");'); @@ -250,6 +289,8 @@ Future main() async { ), ); + await tester.pumpAndSettle(); + await pageLoaded.future; bool isPaused = @@ -509,6 +550,8 @@ Future main() async { ), ); + await tester.pumpAndSettle(); + await pageLoaded.future; // On at least iOS, it does not appear to be guaranteed that the native @@ -824,6 +867,7 @@ Future main() async { )..setOnPageFinished((_) => pageLoaded.complete())); await controller.runJavaScript('window.open("$primaryUrl", "_blank")'); + await tester.pumpAndSettle(); await pageLoaded.future; final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); 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/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, ), ); From 7c26cf600dba70fd20f10103e1f90769baa3e08e Mon Sep 17 00:00:00 2001 From: Maurice Parrish <10687576+bparrishMines@users.noreply.github.com> Date: Fri, 20 Jan 2023 01:32:03 -0500 Subject: [PATCH 02/13] combine boolean --- .../example/integration_test/webview_flutter_test.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 eae5ae6c4e04..b07da863b368 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 @@ -79,10 +79,8 @@ Future main() async { InstanceManager(onWeakReferenceRemoved: (int identifier) { final Copyable instance = instanceManager.getInstanceWithWeakReference(identifier)!; - if (instance is WKWebView) { - if (!webViewGCCompleter.isCompleted) { - webViewGCCompleter.complete(); - } + if (instance is WKWebView && !webViewGCCompleter.isCompleted) { + webViewGCCompleter.complete(); } }); From 2e374a4f1851249574df71115b1cb5aa25f879b4 Mon Sep 17 00:00:00 2001 From: Maurice Parrish <10687576+bparrishMines@users.noreply.github.com> Date: Fri, 20 Jan 2023 01:36:06 -0500 Subject: [PATCH 03/13] version bump --- .../webview_flutter/webview_flutter_wkwebview/CHANGELOG.md | 4 ++++ .../webview_flutter/webview_flutter_wkwebview/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index b4ad2a16b425..4f2ea9dd241e 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.3 + +* Fixes bug where `WKWebView` could not be released. + ## 3.0.2 * Updates code for stricter lint checks. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index 85440f6e3dfc..c41bce18cae6 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.2 +version: 3.0.3 environment: sdk: ">=2.17.0 <3.0.0" From 26f90c3bf3ebe87c7da705f73bd07475b5fa286f Mon Sep 17 00:00:00 2001 From: Maurice Parrish <10687576+bparrishMines@users.noreply.github.com> Date: Fri, 20 Jan 2023 02:06:15 -0500 Subject: [PATCH 04/13] more pumpAndSettles --- .../integration_test/webview_flutter_test.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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 b07da863b368..8b017e7da54d 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 @@ -510,6 +510,8 @@ Future main() async { ), ); + await tester.pumpAndSettle(); + await pageLoaded.future; isPaused = @@ -606,6 +608,8 @@ Future main() async { ), ); + await tester.pumpAndSettle(); + await pageLoaded.future; await tester.pumpAndSettle(const Duration(seconds: 3)); @@ -662,6 +666,8 @@ Future main() async { LoadRequestParams(uri: Uri.parse(blankPageEncoded)), ); + await tester.pumpAndSettle(); + await pageLoads.stream.first; // Wait for initial page load. await controller.runJavaScript('location.href = "$secondaryUrl"'); @@ -689,6 +695,8 @@ Future main() async { LoadRequestParams(uri: Uri.parse('https://www.notawebsite..com')), ); + await tester.pumpAndSettle(); + final WebResourceError error = await errorCompleter.future; expect(error, isNotNull); @@ -722,6 +730,8 @@ Future main() async { ), ); + await tester.pumpAndSettle(); + expect(errorCompleter.future, doesNotComplete); await pageFinishCompleter.future; }); @@ -768,6 +778,8 @@ Future main() async { ), ); + await tester.pumpAndSettle(); + expect(errorCompleter.future, doesNotComplete); await pageFinishCompleter.future; }, @@ -794,6 +806,8 @@ Future main() async { ) ..loadRequest(LoadRequestParams(uri: Uri.parse(blankPageEncoded))); + await tester.pumpAndSettle(); + await pageLoads.stream.first; // Wait for initial page load. await controller .runJavaScript('location.href = "https://www.youtube.com/"'); @@ -831,6 +845,8 @@ Future main() async { ) ..loadRequest(LoadRequestParams(uri: Uri.parse(blankPageEncoded))); + await tester.pumpAndSettle(); + await pageLoads.stream.first; // Wait for initial page load. await controller.runJavaScript('location.href = "$secondaryUrl"'); From abbd6374d1f55e8113bca6c8cbca9e3a6134e220 Mon Sep 17 00:00:00 2001 From: Maurice Parrish <10687576+bparrishMines@users.noreply.github.com> Date: Fri, 20 Jan 2023 10:07:42 -0500 Subject: [PATCH 05/13] add more pumpAndSettles --- .../example/integration_test/webview_flutter_test.dart | 8 ++++++++ .../example/ios/Runner.xcodeproj/project.pbxproj | 5 ++++- .../example/ios/Runner/Info.plist | 2 ++ 3 files changed, 14 insertions(+), 1 deletion(-) 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 8b017e7da54d..0968e01b30c0 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 @@ -221,6 +221,8 @@ Future main() async { ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setUserAgent('Custom_User_Agent1'); + await tester.pumpAndSettle(); + final String customUserAgent2 = await _getUserAgent(controller); expect(customUserAgent2, 'Custom_User_Agent1'); }); @@ -486,6 +488,8 @@ Future main() async { ), ); + await tester.pumpAndSettle(); + await pageLoaded.future; bool isPaused = @@ -864,6 +868,8 @@ Future main() async { ..setAllowsBackForwardNavigationGestures(true) ..loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); + await tester.pumpAndSettle(); + final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); }); @@ -901,6 +907,8 @@ Future main() async { )..setOnPageFinished((_) => pageLoaded.complete())) ..loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); + await tester.pumpAndSettle(); + expect(controller.currentUrl(), completion(primaryUrl)); await pageLoaded.future; pageLoaded = Completer(); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj index 1efee8f844ef..a1040b16e95a 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -379,10 +379,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -415,6 +417,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist index bea41604e8aa..6ee44fd0e2fd 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist @@ -43,5 +43,7 @@ CADisableMinimumFrameDurationOnPhone + UIApplicationSupportsIndirectInputEvents + From 88a660a65b7b234793e8fbc933dfbec84acb69ed Mon Sep 17 00:00:00 2001 From: Maurice Parrish <10687576+bparrishMines@users.noreply.github.com> Date: Sat, 28 Jan 2023 02:39:32 -0500 Subject: [PATCH 06/13] update int tests --- .../webview_flutter_test.dart | 162 +++++----- .../lib/src/android_webview_controller.dart | 23 +- .../webview_flutter_test.dart | 301 +++++++++++++----- 3 files changed, 308 insertions(+), 178 deletions(-) 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..7023e6be0f4c 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; @@ -110,15 +110,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 +149,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 +191,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 +252,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 +327,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 +359,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 +479,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 +511,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 +557,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..46ae77bcf112 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) { + _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); } - }, - 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 = @@ -128,7 +130,6 @@ class AndroidWebViewController extends PlatformWebViewController { // The keeps a reference to the current NavigationDelegate so that the // callback methods remain reachable. - // ignore: unused_field late AndroidNavigationDelegate _currentNavigationDelegate; Future> Function(FileSelectorParams)? 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 0968e01b30c0..9f3d55b9eee5 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 @@ -47,75 +47,110 @@ Future main() async { final String secondaryUrl = '$prefixUrl/secondary.txt'; final String headersUrl = '$prefixUrl/headers'; - testWidgets( - 'withWeakReferenceTo allows encapsulating class to be garbage collected', - (WidgetTester tester) async { - final Completer gcCompleter = Completer(); - final InstanceManager instanceManager = InstanceManager( - onWeakReferenceRemoved: gcCompleter.complete, - ); - - ClassWithCallbackClass? instance = ClassWithCallbackClass(); - instanceManager.addHostCreatedInstance(instance.callbackClass, 0); - instance = null; - - // Force garbage collection. - await IntegrationTestWidgetsFlutterBinding.instance - .watchPerformance(() async { - await tester.pumpAndSettle(); - }); - - final int gcIdentifier = await gcCompleter.future; - 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(); - } - }); - - // ignore: unused_local_variable - WebKitWebViewController? controller = WebKitWebViewController( - WebKitWebViewControllerCreationParams(instanceManager: instanceManager), - ); - controller = null; - - // Force garbage collection. - await IntegrationTestWidgetsFlutterBinding.instance - .watchPerformance(() async { - await tester.pumpAndSettle(); - }); - - await expectLater(webViewGCCompleter.future, completes); - }, - timeout: const Timeout(Duration(seconds: 10)), - ); + // testWidgets( + // 'withWeakReferenceTo allows encapsulating class to be garbage collected', + // (WidgetTester tester) async { + // final Completer gcCompleter = Completer(); + // final InstanceManager instanceManager = InstanceManager( + // onWeakReferenceRemoved: gcCompleter.complete, + // ); + // + // ClassWithCallbackClass? instance = ClassWithCallbackClass(); + // instanceManager.addHostCreatedInstance(instance.callbackClass, 0); + // instance = null; + // + // // Force garbage collection. + // await IntegrationTestWidgetsFlutterBinding.instance + // .watchPerformance(() async { + // await tester.pumpAndSettle(); + // }); + // + // final int gcIdentifier = await gcCompleter.future; + // 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(); + // } + // }); + // + // // ignore: unused_local_variable + // WebKitWebViewController? controller = WebKitWebViewController( + // WebKitWebViewControllerCreationParams(instanceManager: instanceManager), + // ); + // controller = null; + // + // // 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'), @@ -146,7 +181,13 @@ Future main() async { ), ); - await tester.pumpAndSettle(); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageLoads.stream.firstWhere((String url) => url == headersUrl); @@ -182,7 +223,13 @@ Future main() async { 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', ); - await tester.pumpAndSettle(); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageFinished.future; @@ -221,7 +268,13 @@ Future main() async { ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setUserAgent('Custom_User_Agent1'); - await tester.pumpAndSettle(); + 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'); @@ -289,7 +342,13 @@ Future main() async { ), ); - await tester.pumpAndSettle(); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageLoaded.future; @@ -315,6 +374,14 @@ Future main() async { ), ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + await pageLoaded.future; isPaused = @@ -488,7 +555,13 @@ Future main() async { ), ); - await tester.pumpAndSettle(); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageLoaded.future; @@ -514,7 +587,13 @@ Future main() async { ), ); - await tester.pumpAndSettle(); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageLoaded.future; @@ -554,7 +633,13 @@ Future main() async { ), ); - await tester.pumpAndSettle(); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageLoaded.future; @@ -612,7 +697,13 @@ Future main() async { ), ); - await tester.pumpAndSettle(); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageLoaded.future; @@ -670,7 +761,13 @@ Future main() async { LoadRequestParams(uri: Uri.parse(blankPageEncoded)), ); - await tester.pumpAndSettle(); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageLoads.stream.first; // Wait for initial page load. await controller.runJavaScript('location.href = "$secondaryUrl"'); @@ -684,7 +781,7 @@ Future main() async { final Completer errorCompleter = Completer(); - PlatformWebViewController( + final PlatformWebViewController controller = PlatformWebViewController( WebKitWebViewControllerCreationParams(), ) ..setJavaScriptMode(JavaScriptMode.unrestricted) @@ -699,7 +796,13 @@ Future main() async { LoadRequestParams(uri: Uri.parse('https://www.notawebsite..com')), ); - await tester.pumpAndSettle(); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); final WebResourceError error = await errorCompleter.future; expect(error, isNotNull); @@ -713,7 +816,7 @@ Future main() async { Completer(); final Completer pageFinishCompleter = Completer(); - PlatformWebViewController( + final PlatformWebViewController controller = PlatformWebViewController( WebKitWebViewControllerCreationParams(), ) ..setJavaScriptMode(JavaScriptMode.unrestricted) @@ -734,7 +837,13 @@ Future main() async { ), ); - await tester.pumpAndSettle(); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); expect(errorCompleter.future, doesNotComplete); await pageFinishCompleter.future; @@ -761,7 +870,7 @@ Future main() async { Completer(); final Completer pageFinishCompleter = Completer(); - PlatformWebViewController( + final PlatformWebViewController controller = PlatformWebViewController( WebKitWebViewControllerCreationParams(), ) ..setJavaScriptMode(JavaScriptMode.unrestricted) @@ -782,7 +891,13 @@ Future main() async { ), ); - await tester.pumpAndSettle(); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); expect(errorCompleter.future, doesNotComplete); await pageFinishCompleter.future; @@ -810,7 +925,13 @@ Future main() async { ) ..loadRequest(LoadRequestParams(uri: Uri.parse(blankPageEncoded))); - await tester.pumpAndSettle(); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageLoads.stream.first; // Wait for initial page load. await controller @@ -849,7 +970,13 @@ Future main() async { ) ..loadRequest(LoadRequestParams(uri: Uri.parse(blankPageEncoded))); - await tester.pumpAndSettle(); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageLoads.stream.first; // Wait for initial page load. await controller.runJavaScript('location.href = "$secondaryUrl"'); @@ -868,7 +995,13 @@ Future main() async { ..setAllowsBackForwardNavigationGestures(true) ..loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); - await tester.pumpAndSettle(); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); @@ -887,7 +1020,15 @@ Future main() async { )..setOnPageFinished((_) => pageLoaded.complete())); await controller.runJavaScript('window.open("$primaryUrl", "_blank")'); - await tester.pumpAndSettle(); + + 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); @@ -907,7 +1048,13 @@ Future main() async { )..setOnPageFinished((_) => pageLoaded.complete())) ..loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); - await tester.pumpAndSettle(); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); expect(controller.currentUrl(), completion(primaryUrl)); await pageLoaded.future; From 2037a4be48bd1469dc042137e733e5ba72a37922 Mon Sep 17 00:00:00 2001 From: Maurice Parrish <10687576+bparrishMines@users.noreply.github.com> Date: Sun, 29 Jan 2023 15:58:26 -0500 Subject: [PATCH 07/13] android test --- .../webview_flutter_android/CHANGELOG.md | 4 + .../webview_flutter_test.dart | 73 +++++++++++ .../webview_flutter_android/pubspec.yaml | 2 +- .../webview_flutter_wkwebview/CHANGELOG.md | 4 + .../webview_flutter_test.dart | 120 ++++++++++-------- .../webview_flutter_wkwebview/pubspec.yaml | 2 +- 6 files changed, 150 insertions(+), 55 deletions(-) diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index e1786d6cd7d0..88b1f08d8d61 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.2.3 + +* Fixes bug that prevented the web view from being garbage collected. + ## 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 7023e6be0f4c..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 @@ -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(); 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_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index 13e0c0558689..dd7106d8418f 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.5 + +* Fixes bug that prevented the web view from being garbage collected. + ## 3.0.4 * Fixes bug where `WKWebView` could not be released. 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 9f3d55b9eee5..3ba2aeec0075 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 @@ -47,59 +47,73 @@ Future main() async { final String secondaryUrl = '$prefixUrl/secondary.txt'; final String headersUrl = '$prefixUrl/headers'; - // testWidgets( - // 'withWeakReferenceTo allows encapsulating class to be garbage collected', - // (WidgetTester tester) async { - // final Completer gcCompleter = Completer(); - // final InstanceManager instanceManager = InstanceManager( - // onWeakReferenceRemoved: gcCompleter.complete, - // ); - // - // ClassWithCallbackClass? instance = ClassWithCallbackClass(); - // instanceManager.addHostCreatedInstance(instance.callbackClass, 0); - // instance = null; - // - // // Force garbage collection. - // await IntegrationTestWidgetsFlutterBinding.instance - // .watchPerformance(() async { - // await tester.pumpAndSettle(); - // }); - // - // final int gcIdentifier = await gcCompleter.future; - // 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(); - // } - // }); - // - // // ignore: unused_local_variable - // WebKitWebViewController? controller = WebKitWebViewController( - // WebKitWebViewControllerCreationParams(instanceManager: instanceManager), - // ); - // controller = null; - // - // // Force garbage collection. - // await IntegrationTestWidgetsFlutterBinding.instance - // .watchPerformance(() async { - // await tester.pumpAndSettle(); - // }); - // - // await expectLater(webViewGCCompleter.future, completes); - // }, - // timeout: const Timeout(Duration(seconds: 10)), - // ); + testWidgets( + 'withWeakReferenceTo allows encapsulating class to be garbage collected', + (WidgetTester tester) async { + final Completer gcCompleter = Completer(); + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: gcCompleter.complete, + ); + + ClassWithCallbackClass? instance = ClassWithCallbackClass(); + instanceManager.addHostCreatedInstance(instance.callbackClass, 0); + instance = null; + + // Force garbage collection. + await IntegrationTestWidgetsFlutterBinding.instance + .watchPerformance(() async { + await tester.pumpAndSettle(); + }); + + final int gcIdentifier = await gcCompleter.future; + 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(); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index 15e8ac5678f8..5c4df9922840 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.4 +version: 3.0.5 environment: sdk: ">=2.17.0 <3.0.0" From befe0417a8810fec20d5a11f72157c775d436c3c Mon Sep 17 00:00:00 2001 From: Maurice Parrish <10687576+bparrishMines@users.noreply.github.com> Date: Sun, 29 Jan 2023 16:08:34 -0500 Subject: [PATCH 08/13] fix version bump and unchange compile files --- .../lib/src/android_webview_controller.dart | 2 -- .../webview_flutter/webview_flutter_wkwebview/CHANGELOG.md | 6 +----- .../example/ios/Runner.xcodeproj/project.pbxproj | 5 +---- .../webview_flutter_wkwebview/example/ios/Runner/Info.plist | 2 -- .../webview_flutter/webview_flutter_wkwebview/pubspec.yaml | 2 +- 5 files changed, 3 insertions(+), 14 deletions(-) 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 46ae77bcf112..0b7654751f4c 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 @@ -128,8 +128,6 @@ class AndroidWebViewController extends PlatformWebViewController { final Map _javaScriptChannelParams = {}; - // The keeps a reference to the current NavigationDelegate so that the - // callback methods remain reachable. late AndroidNavigationDelegate _currentNavigationDelegate; Future> Function(FileSelectorParams)? diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index dd7106d8418f..4576b416b20b 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,10 +1,6 @@ -## 3.0.5 - -* Fixes bug that prevented the web view from being garbage collected. - ## 3.0.4 -* Fixes bug where `WKWebView` could not be released. +* Fixes bug that prevented the web view from being garbage collected. # 3.0.3 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj index a1040b16e95a..1efee8f844ef 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ @@ -379,12 +379,10 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -417,7 +415,6 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist index 6ee44fd0e2fd..bea41604e8aa 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist @@ -43,7 +43,5 @@ CADisableMinimumFrameDurationOnPhone - UIApplicationSupportsIndirectInputEvents - diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index 5c4df9922840..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.5 +version: 3.0.4 environment: sdk: ">=2.17.0 <3.0.0" From 15d8b768452978e161f1ebe03907e6cee2eaa536 Mon Sep 17 00:00:00 2001 From: Maurice Parrish <10687576+bparrishMines@users.noreply.github.com> Date: Sun, 29 Jan 2023 21:25:36 -0500 Subject: [PATCH 09/13] make _currentNavigationDelegate nullable --- .../webview_flutter_android/example/lib/main.dart | 2 +- .../lib/src/android_webview_controller.dart | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart index 75f01b457b3a..ac54c7ba1e89 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -83,7 +83,7 @@ class WebViewExample extends StatefulWidget { } class _WebViewExampleState extends State { - late final PlatformWebViewController _controller; + late final PlatformWebViewController _controller;z @override void initState() { 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 0b7654751f4c..6fd47c2f6cb6 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 @@ -100,10 +100,10 @@ class AndroidWebViewController extends PlatformWebViewController { onProgressChanged: withWeakReferenceTo(this, (WeakReference weakReference) { return (android_webview.WebView webView, int progress) { - if (weakReference.target?._currentNavigationDelegate._onProgress != + if (weakReference.target?._currentNavigationDelegate?._onProgress != null) { weakReference - .target!._currentNavigationDelegate._onProgress!(progress); + .target!._currentNavigationDelegate?._onProgress!(progress); } }; }), @@ -128,7 +128,7 @@ class AndroidWebViewController extends PlatformWebViewController { final Map _javaScriptChannelParams = {}; - late AndroidNavigationDelegate _currentNavigationDelegate; + AndroidNavigationDelegate? _currentNavigationDelegate; Future> Function(FileSelectorParams)? _onShowFileSelectorCallback; From 23b820dd16cc1c7be14a9614da3e9041ac2a2b01 Mon Sep 17 00:00:00 2001 From: Maurice Parrish <10687576+bparrishMines@users.noreply.github.com> Date: Sun, 29 Jan 2023 21:37:42 -0500 Subject: [PATCH 10/13] quick fix --- .../webview_flutter_android/CHANGELOG.md | 1 + .../lib/src/android_webview_controller.dart | 2 +- .../test/android_webview_controller_test.dart | 13 +++++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index 88b1f08d8d61..1a490b5a579d 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,6 +1,7 @@ ## 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 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 6fd47c2f6cb6..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 @@ -103,7 +103,7 @@ class AndroidWebViewController extends PlatformWebViewController { if (weakReference.target?._currentNavigationDelegate?._onProgress != null) { weakReference - .target!._currentNavigationDelegate?._onProgress!(progress); + .target!._currentNavigationDelegate!._onProgress!(progress); } }; }), 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, From 043f89c1694e26ed077036b35cf17c78df8a1a01 Mon Sep 17 00:00:00 2001 From: Maurice Parrish <10687576+bparrishMines@users.noreply.github.com> Date: Sun, 29 Jan 2023 21:44:33 -0500 Subject: [PATCH 11/13] accidental letter --- .../webview_flutter_android/example/lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart index ac54c7ba1e89..75f01b457b3a 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -83,7 +83,7 @@ class WebViewExample extends StatefulWidget { } class _WebViewExampleState extends State { - late final PlatformWebViewController _controller;z + late final PlatformWebViewController _controller; @override void initState() { From 19f7fef6fd49bf33f2227c4e38d89990cd82c2b8 Mon Sep 17 00:00:00 2001 From: Maurice Parrish <10687576+bparrishMines@users.noreply.github.com> Date: Mon, 30 Jan 2023 14:40:06 -0500 Subject: [PATCH 12/13] small tests fixes --- .../webview_flutter_test.dart | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) 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 3ba2aeec0075..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 @@ -753,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(), @@ -764,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 @@ -783,10 +782,12 @@ Future main() async { }, )); - await pageLoads.stream.first; // Wait for initial page load. + 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); }); @@ -919,8 +920,7 @@ Future main() async { ); testWidgets('can block requests', (WidgetTester tester) async { - final StreamController pageLoads = - StreamController.broadcast(); + Completer pageLoaded = Completer(); final PlatformWebViewController controller = PlatformWebViewController( WebKitWebViewControllerCreationParams(), @@ -930,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 @@ -947,22 +947,23 @@ Future main() async { }, )); - await pageLoads.stream.first; // Wait for initial page load. + 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(), @@ -972,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; @@ -992,10 +993,12 @@ Future main() async { }, )); - await pageLoads.stream.first; // Wait for initial page load. + 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); }); From 6078f506f3718a915e73924ec14246c9ad049922 Mon Sep 17 00:00:00 2001 From: Maurice Parrish <10687576+bparrishMines@users.noreply.github.com> Date: Mon, 30 Jan 2023 20:05:28 -0500 Subject: [PATCH 13/13] missing hashtag --- packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index 4576b416b20b..f79058c80216 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -2,7 +2,7 @@ * Fixes bug that prevented the web view from being garbage collected. -# 3.0.3 +## 3.0.3 * Updates example code for `use_build_context_synchronously` lint.