diff --git a/packages/webview_flutter/webview_flutter_android/example/integration_test/legacy/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_android/example/integration_test/legacy/webview_flutter_test.dart index 2fd0c1b6cdf..84ff9e70eb1 100644 --- a/packages/webview_flutter/webview_flutter_android/example/integration_test/legacy/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter_android/example/integration_test/legacy/webview_flutter_test.dart @@ -18,6 +18,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/src/webview_flutter_android_legacy.dart'; @@ -126,6 +128,76 @@ 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, + ); + android.WebViewClient.api = WebViewClientHostApiImpl( + instanceManager: instanceManager, + ); + android.DownloadListener.api = DownloadListenerHostApiImpl( + instanceManager: instanceManager, + ); + + // Continually recreate web views until one is disposed through garbage + // collection. + while (!webViewGCCompleter.isCompleted) { + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + return AndroidWebView(instanceManager: instanceManager).build( + context: context, + creationParams: CreationParams( + webSettings: WebSettings( + hasNavigationDelegate: false, + userAgent: const WebSetting.of('woeifj'), + ), + ), + javascriptChannelRegistry: JavascriptChannelRegistry( + {}, + ), + webViewPlatformCallbacksHandler: TestPlatformCallbacksHandler(), + ); + }, + ), + ); + await tester.pumpAndSettle(); + + await tester.pumpWidget(Container()); + await tester.pumpAndSettle(); + } + + android.WebView.api = WebViewHostApiImpl(); + android.WebSettings.api = WebSettingsHostApiImpl(); + android.WebChromeClient.api = WebChromeClientHostApiImpl(); + android.WebViewClient.api = WebViewClientHostApiImpl(); + android.DownloadListener.api = DownloadListenerHostApiImpl(); + + // Create a new `WebStorage` with the default InstanceManager. + android.WebStorage.instance = android.WebStorage(); + }, + ); + testWidgets('evaluateJavascript', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); @@ -1572,3 +1644,25 @@ class ClassWithCallbackClass { late final CopyableObjectWithCallback callbackClass; } + +class TestPlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { + @override + FutureOr onNavigationRequest({ + required String url, + required bool isForMainFrame, + }) async { + return true; + } + + @override + void onPageStarted(String url) {} + + @override + void onPageFinished(String url) {} + + @override + void onProgress(int progress) {} + + @override + void onWebResourceError(WebResourceError error) {} +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android.dart b/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android.dart index cfda749fa4a..382e40a9bff 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android.dart @@ -12,6 +12,7 @@ import 'package:flutter/widgets.dart'; import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; import '../android_webview.dart'; +import '../instance_manager.dart'; import 'webview_android_widget.dart'; /// Builds an Android webview. @@ -20,6 +21,15 @@ import 'webview_android_widget.dart'; /// an [AndroidView] to embed the webview in the widget hierarchy, and uses a method channel to /// communicate with the platform code. class AndroidWebView implements WebViewPlatform { + /// Constructs an [AndroidWebView]. + AndroidWebView({@visibleForTesting InstanceManager? instanceManager}) + : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + + /// Maintains instances used to communicate with the native objects they + /// represent. + @protected + final InstanceManager instanceManager; + @override Widget build({ required BuildContext context, @@ -55,8 +65,7 @@ class AndroidWebView implements WebViewPlatform { gestureRecognizers: gestureRecognizers, layoutDirection: Directionality.maybeOf(context) ?? TextDirection.rtl, - creationParams: JavaObject.globalInstanceManager - .getIdentifier(controller.webView), + creationParams: instanceManager.getIdentifier(controller.webView), creationParamsCodec: const StandardMessageCodec(), ), ); diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android_widget.dart b/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android_widget.dart index ec1c3cd6204..8504253cd92 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android_widget.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android_widget.dart @@ -138,16 +138,26 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { final Map _javaScriptChannels = {}; - late final android_webview.WebViewClient _webViewClient = withWeakReferenceTo( - this, (WeakReference weakReference) { - return webViewProxy.createWebViewClient( - onPageStarted: (_, String url) { + late final android_webview.WebViewClient _webViewClient = + webViewProxy.createWebViewClient( + onPageStarted: withWeakReferenceTo(this, ( + WeakReference weakReference, + ) { + return (_, String url) { weakReference.target?.callbacksHandler.onPageStarted(url); - }, - onPageFinished: (_, String url) { + }; + }), + onPageFinished: withWeakReferenceTo(this, ( + WeakReference weakReference, + ) { + return (_, String url) { weakReference.target?.callbacksHandler.onPageFinished(url); - }, - onReceivedError: ( + }; + }), + onReceivedError: withWeakReferenceTo(this, ( + WeakReference weakReference, + ) { + return ( _, int errorCode, String description, @@ -160,8 +170,12 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { failingUrl: failingUrl, errorType: _errorCodeToErrorType(errorCode), )); - }, - onReceivedRequestError: ( + }; + }), + onReceivedRequestError: withWeakReferenceTo(this, ( + WeakReference weakReference, + ) { + return ( _, android_webview.WebResourceRequest request, android_webview.WebResourceError error, @@ -175,21 +189,29 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { errorType: _errorCodeToErrorType(error.errorCode), )); } - }, - urlLoading: (_, String url) { + }; + }), + urlLoading: withWeakReferenceTo(this, ( + WeakReference weakReference, + ) { + return (_, String url) { weakReference.target?._handleNavigationRequest( url: url, isForMainFrame: true, ); - }, - requestLoading: (_, android_webview.WebResourceRequest request) { + }; + }), + requestLoading: withWeakReferenceTo(this, ( + WeakReference weakReference, + ) { + return (_, android_webview.WebResourceRequest request) { weakReference.target?._handleNavigationRequest( url: request.url, isForMainFrame: request.isForMainFrame, ); - }, - ); - }); + }; + }), + ); bool _hasNavigationDelegate = false; bool _hasProgressTracking = false; diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_surface_android.dart b/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_surface_android.dart index 8db2fe08835..81a965545c5 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_surface_android.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_surface_android.dart @@ -30,6 +30,9 @@ import 'webview_android_widget.dart'; /// https://github.com/flutter/flutter/wiki/Hybrid-Composition for more /// information. class SurfaceAndroidWebView extends AndroidWebView { + /// Constructs a [SurfaceAndroidWebView]. + SurfaceAndroidWebView({@visibleForTesting super.instanceManager}); + @override Widget build({ required BuildContext context, @@ -74,8 +77,8 @@ class SurfaceAndroidWebView extends AndroidWebView { // directionality. layoutDirection: Directionality.maybeOf(context) ?? TextDirection.ltr, - webViewIdentifier: JavaObject.globalInstanceManager - .getIdentifier(controller.webView)!, + webViewIdentifier: + instanceManager.getIdentifier(controller.webView)!, ) ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) ..addOnPlatformViewCreatedListener((int id) {