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 9687c7d56cbe..1119f7457bc9 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 @@ -19,6 +19,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; 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_example/navigation_decision.dart'; import 'package:webview_flutter_wkwebview_example/navigation_request.dart'; import 'package:webview_flutter_wkwebview_example/web_view.dart'; @@ -66,6 +68,27 @@ Future main() async { expect(currentUrl, primaryUrl); }); + testWidgets( + 'withWeakRefenceTo 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(); + }); + + expect(gcCompleter.future, completion(0)); + }, timeout: const Timeout(Duration(seconds: 10))); + testWidgets('loadUrl', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); @@ -1253,3 +1276,33 @@ class ResizableWebViewState extends State { ); } } + +class CopyableObjectWithCallback with Copyable { + CopyableObjectWithCallback(this.callback); + + final VoidCallback callback; + + @override + CopyableObjectWithCallback copy() { + return CopyableObjectWithCallback(callback); + } +} + +class ClassWithCallbackClass { + ClassWithCallbackClass() { + callbackClass = CopyableObjectWithCallback( + withWeakRefenceTo( + this, + (WeakReference weakReference) { + return () { + // Weak reference to `this` in callback. + // ignore: unnecessary_statements + weakReference; + }; + }, + ), + ); + } + + late final CopyableObjectWithCallback callbackClass; +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/weak_reference_utils.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/weak_reference_utils.dart new file mode 100644 index 000000000000..ad0c9ebf4f5c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/weak_reference_utils.dart @@ -0,0 +1,34 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Helper method for creating callbacks methods with a weak reference. +/// +/// Example: +/// ``` +/// final JavascriptChannelRegistry javascriptChannelRegistry = ... +/// +/// final WKScriptMessageHandler handler = WKScriptMessageHandler( +/// didReceiveScriptMessage: withWeakRefenceTo( +/// javascriptChannelRegistry, +/// (WeakReference weakReference) { +/// return ( +/// WKUserContentController userContentController, +/// WKScriptMessage message, +/// ) { +/// weakReference.target?.onJavascriptChannelMessage( +/// message.name, +/// message.body!.toString(), +/// ); +/// }; +/// }, +/// ), +/// ); +/// ``` +S withWeakRefenceTo( + T reference, + S Function(WeakReference weakReference) onCreate, +) { + final WeakReference weakReference = WeakReference(reference); + return onCreate(weakReference); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation.dart index f537a4454c4f..2059aa544207 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation.dart @@ -8,6 +8,7 @@ import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:webview_flutter_wkwebview/src/common/weak_reference_utils.dart'; import '../common/instance_manager.dart'; import 'foundation_api_impls.dart'; @@ -267,7 +268,19 @@ class NSObject with Copyable { final NSObjectHostApiImpl _api; - /// Informs the observing object when the value at the specified key path has changed. + /// Informs the observing object when the value at the specified key path has + /// changed. + /// + /// {@template webview_flutter_wkwebview.foundation.callbacks} + /// For the associated Objective-C object to be automatically garbage + /// collected, it is required that this Function doesn't contain a strong + /// reference to the encapsulating class instance. Consider using + /// `WeakReference` when referencing an object not received as a parameter. + /// Otherwise, use [NSObject.dispose] to release the associated Objective-C + /// object manually. + /// + /// See [withWeakRefenceTo]. + /// {@endtemplate} final void Function( String keyPath, NSObject object, diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart index ffc6eb8c23bf..a671064ddc0f 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart @@ -442,6 +442,8 @@ class WKScriptMessageHandler extends NSObject { /// Use this method to respond to a message sent from the webpage’s /// JavaScript code. Use the [message] parameter to get the message contents and /// to determine the originating web view. + /// + /// {@macro webview_flutter_wkwebview.foundation.callbacks} final void Function( WKUserContentController userContentController, WKScriptMessage message, @@ -733,6 +735,8 @@ class WKUIDelegate extends NSObject { final WKUIDelegateHostApiImpl _uiDelegateApi; /// Indicates a new [WKWebView] was requested to be created with [configuration]. + /// + /// {@macro webview_flutter_wkwebview.foundation.callbacks} final void Function( WKWebView webView, WKWebViewConfiguration configuration, @@ -803,26 +807,38 @@ class WKNavigationDelegate extends NSObject { final WKNavigationDelegateHostApiImpl _navigationDelegateApi; /// Called when navigation is complete. + /// + /// {@macro webview_flutter_wkwebview.foundation.callbacks} final void Function(WKWebView webView, String? url)? didFinishNavigation; /// Called when navigation from the main frame has started. + /// + /// {@macro webview_flutter_wkwebview.foundation.callbacks} final void Function(WKWebView webView, String? url)? didStartProvisionalNavigation; /// Called when permission is needed to navigate to new content. + /// + /// {@macro webview_flutter_wkwebview.foundation.callbacks} final Future Function( WKWebView webView, WKNavigationAction navigationAction, )? decidePolicyForNavigationAction; /// Called when an error occurred during navigation. + /// + /// {@macro webview_flutter_wkwebview.foundation.callbacks} final void Function(WKWebView webView, NSError error)? didFailNavigation; /// Called when an error occurred during the early navigation process. + /// + /// {@macro webview_flutter_wkwebview.foundation.callbacks} final void Function(WKWebView webView, NSError error)? didFailProvisionalNavigation; /// Called when the web view’s content process was terminated. + /// + /// {@macro webview_flutter_wkwebview.foundation.callbacks} final void Function(WKWebView webView)? webViewWebContentProcessDidTerminate; @override diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit_webview_widget.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit_webview_widget.dart index ed1912ee7898..852f9caa0c49 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit_webview_widget.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit_webview_widget.dart @@ -11,6 +11,7 @@ import 'package:flutter/services.dart'; import 'package:path/path.dart' as path; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'common/weak_reference_utils.dart'; import 'foundation/foundation.dart'; import 'web_kit/web_kit.dart'; @@ -114,58 +115,74 @@ class WebKitWebViewPlatformController extends WebViewPlatformController { /// Used to integrate custom user interface elements into web view interactions. @visibleForTesting - late final WKUIDelegate uiDelegate = - webViewProxy.createUIDelgate(onCreateWebView: ( - WKWebView webView, - WKWebViewConfiguration configuration, - WKNavigationAction navigationAction, - ) { - if (!navigationAction.targetFrame.isMainFrame) { - webView.loadRequest(navigationAction.request); - } - }); + late final WKUIDelegate uiDelegate = webViewProxy.createUIDelgate( + onCreateWebView: ( + WKWebView webView, + WKWebViewConfiguration configuration, + WKNavigationAction navigationAction, + ) { + if (!navigationAction.targetFrame.isMainFrame) { + webView.loadRequest(navigationAction.request); + } + }, + ); /// Methods for handling navigation changes and tracking navigation requests. @visibleForTesting - late final WKNavigationDelegate navigationDelegate = - webViewProxy.createNavigationDelegate( - didFinishNavigation: (WKWebView webView, String? url) { - callbacksHandler.onPageFinished(url ?? ''); - }, - didStartProvisionalNavigation: (WKWebView webView, String? url) { - callbacksHandler.onPageStarted(url ?? ''); - }, - decidePolicyForNavigationAction: ( - WKWebView webView, - WKNavigationAction action, - ) async { - if (!_hasNavigationDelegate) { - return WKNavigationActionPolicy.allow; - } + late final WKNavigationDelegate navigationDelegate = withWeakRefenceTo( + this, + (WeakReference weakReference) { + return webViewProxy.createNavigationDelegate( + didFinishNavigation: (WKWebView webView, String? url) { + weakReference.target?.callbacksHandler.onPageFinished(url ?? ''); + }, + didStartProvisionalNavigation: (WKWebView webView, String? url) { + weakReference.target?.callbacksHandler.onPageStarted(url ?? ''); + }, + decidePolicyForNavigationAction: ( + WKWebView webView, + WKNavigationAction action, + ) async { + if (weakReference.target == null) { + return WKNavigationActionPolicy.allow; + } + + if (!weakReference.target!._hasNavigationDelegate) { + return WKNavigationActionPolicy.allow; + } + + final bool allow = + await weakReference.target!.callbacksHandler.onNavigationRequest( + url: action.request.url, + isForMainFrame: action.targetFrame.isMainFrame, + ); - final bool allow = await callbacksHandler.onNavigationRequest( - url: action.request.url, - isForMainFrame: action.targetFrame.isMainFrame, + return allow + ? WKNavigationActionPolicy.allow + : WKNavigationActionPolicy.cancel; + }, + didFailNavigation: (WKWebView webView, NSError error) { + weakReference.target?.callbacksHandler.onWebResourceError( + _toWebResourceError(error), + ); + }, + didFailProvisionalNavigation: (WKWebView webView, NSError error) { + weakReference.target?.callbacksHandler.onWebResourceError( + _toWebResourceError(error), + ); + }, + webViewWebContentProcessDidTerminate: (WKWebView webView) { + weakReference.target?.callbacksHandler.onWebResourceError( + WebResourceError( + errorCode: WKErrorCode.webContentProcessTerminated, + // Value from https://developer.apple.com/documentation/webkit/wkerrordomain?language=objc. + domain: 'WKErrorDomain', + description: '', + errorType: WebResourceErrorType.webContentProcessTerminated, + ), + ); + }, ); - - return allow - ? WKNavigationActionPolicy.allow - : WKNavigationActionPolicy.cancel; - }, - didFailNavigation: (WKWebView webView, NSError error) { - callbacksHandler.onWebResourceError(_toWebResourceError(error)); - }, - didFailProvisionalNavigation: (WKWebView webView, NSError error) { - callbacksHandler.onWebResourceError(_toWebResourceError(error)); - }, - webViewWebContentProcessDidTerminate: (WKWebView webView) { - callbacksHandler.onWebResourceError(WebResourceError( - errorCode: WKErrorCode.webContentProcessTerminated, - // Value from https://developer.apple.com/documentation/webkit/wkerrordomain?language=objc. - domain: 'WKErrorDomain', - description: '', - errorType: WebResourceErrorType.webContentProcessTerminated, - )); }, ); @@ -179,14 +196,23 @@ class WebKitWebViewPlatformController extends WebViewPlatformController { autoMediaPlaybackPolicy: params.autoMediaPlaybackPolicy, ); - webView = webViewProxy.createWebView(configuration, observeValue: ( - String keyPath, - NSObject object, - Map change, - ) { - final double progress = change[NSKeyValueChangeKey.newValue]! as double; - callbacksHandler.onProgress((progress * 100).round()); - }); + webView = webViewProxy.createWebView( + configuration, + observeValue: withWeakRefenceTo( + callbacksHandler, + (WeakReference weakReference) { + return ( + String keyPath, + NSObject object, + Map change, + ) { + final double progress = + change[NSKeyValueChangeKey.newValue]! as double; + weakReference.target?.onProgress((progress * 100).round()); + }; + }, + ), + ); webView.setUIDelegate(uiDelegate); @@ -413,15 +439,22 @@ class WebKitWebViewPlatformController extends WebViewPlatformController { ).map>( (String channelName) { final WKScriptMessageHandler handler = - webViewProxy.createScriptMessageHandler(didReceiveScriptMessage: ( - WKUserContentController userContentController, - WKScriptMessage message, - ) { - javascriptChannelRegistry.onJavascriptChannelMessage( - message.name, - message.body!.toString(), - ); - }); + webViewProxy.createScriptMessageHandler( + didReceiveScriptMessage: withWeakRefenceTo( + javascriptChannelRegistry, + (WeakReference weakReference) { + return ( + WKUserContentController userContentController, + WKScriptMessage message, + ) { + weakReference.target?.onJavascriptChannelMessage( + message.name, + message.body!.toString(), + ); + }; + }, + ), + ); _scriptMessageHandlers[channelName] = handler; final String wrapperSource = @@ -652,11 +685,7 @@ class WebViewWidgetProxy { /// Constructs a [WKNavigationDelegate]. WKNavigationDelegate createNavigationDelegate({ - void Function( - WKWebView webView, - String? url, - )? - didFinishNavigation, + void Function(WKWebView webView, String? url)? didFinishNavigation, void Function(WKWebView webView, String? url)? didStartProvisionalNavigation, Future Function(