From 26889ce846ad739968e942482a64ade7134b7495 Mon Sep 17 00:00:00 2001 From: Maurice Parrish <10687576+bparrishMines@users.noreply.github.com> Date: Tue, 25 Jul 2023 10:33:22 -0400 Subject: [PATCH] [webview_flutter_android][webview_flutter_wkwebview] Fixes bug where `PlatformWebViewWidget` doesn't rebuild when the controller changes (#4533) In a scenario where a `WebViewWidget` was updated with a new `WebViewController`, the native `WebView` from the new controller would not be shown. e.g. ```dart class WebViewExample extends StatefulWidget { const WebViewExample({super.key}); @override State createState() => _WebViewExampleState(); } class _WebViewExampleState extends State { late WebViewController controller; @override void initState() { super.initState(); controller = WebViewController() ..loadRequest(Uri.parse('https://flutter.dev')); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Flutter Simple Example')), body: WebViewWidget(controller: controller), floatingActionButton: FloatingActionButton( onPressed: () { setState(() { controller = WebViewController() ..loadRequest(Uri.parse('https://google.com')); }); }, child: const Icon(Icons.add), ), ); } } ``` From testing, the `WebViewWidget` would continue showing the original `WebViewController` and eventually freeze the PlatformView preventing button presses. This is because the PlatformView widget only creates the native PlatformView once and doesn't recreate it when the creation parameters change (as expected). This adds a default `key` to the `PlatformWebViewWidget` that is used to indicate when the underlying widget needs to be removed and recreated. See https://api.flutter.dev/flutter/widgets/Widget/key.html --- .../webview_flutter_android/CHANGELOG.md | 5 + .../lib/src/android_webview_controller.dart | 23 +++- .../webview_flutter_android/pubspec.yaml | 2 +- .../test/android_webview_controller_test.dart | 63 ++++++++++ .../webview_flutter_wkwebview/CHANGELOG.md | 4 + .../lib/src/webkit_webview_controller.dart | 19 ++- .../webview_flutter_wkwebview/pubspec.yaml | 2 +- .../test/webkit_webview_widget_test.dart | 114 +++++++++++++----- 8 files changed, 197 insertions(+), 35 deletions(-) diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index 7a02dcf024e6..03f1539a069b 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.9.2 + +* Fixes bug where `PlatformWebViewWidget` doesn't rebuild when the controller or PlatformView + implementation flag changes. + ## 3.9.1 * Adjusts SDK checks for better testability. 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 7c5f0929def8..3761dd2b99df 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 @@ -744,6 +744,25 @@ class AndroidWebViewWidgetCreationParams /// /// Defaults to false. final bool displayWithHybridComposition; + + @override + int get hashCode => Object.hash( + controller, + layoutDirection, + displayWithHybridComposition, + platformViewsServiceProxy, + instanceManager, + ); + + @override + bool operator ==(Object other) { + return other is AndroidWebViewWidgetCreationParams && + controller == other.controller && + layoutDirection == other.layoutDirection && + displayWithHybridComposition == other.displayWithHybridComposition && + platformViewsServiceProxy == other.platformViewsServiceProxy && + instanceManager == other.instanceManager; + } } /// An implementation of [PlatformWebViewWidget] with the Android WebView API. @@ -763,7 +782,9 @@ class AndroidWebViewWidget extends PlatformWebViewWidget { @override Widget build(BuildContext context) { return PlatformViewLink( - key: _androidParams.key, + // Setting a default key using `params` ensures the `PlatformViewLink` + // recreates the PlatformView when changes are made. + key: _androidParams.key ?? ObjectKey(params), viewType: 'plugins.flutter.io/webview', surfaceFactory: ( BuildContext context, diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index eb79daf79e1d..f56b5a6a9e7e 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/packages/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.9.1 +version: 3.9.2 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart index a6905c7ee981..f3bdb63322e9 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 @@ -1216,5 +1216,68 @@ void main() { ), ); }); + + testWidgets('PlatformView is recreated when the controller changes', + (WidgetTester tester) async { + final MockPlatformViewsServiceProxy mockPlatformViewsService = + MockPlatformViewsServiceProxy(); + + when( + mockPlatformViewsService.initSurfaceAndroidView( + id: anyNamed('id'), + viewType: anyNamed('viewType'), + layoutDirection: anyNamed('layoutDirection'), + creationParams: anyNamed('creationParams'), + creationParamsCodec: anyNamed('creationParamsCodec'), + onFocus: anyNamed('onFocus'), + ), + ).thenReturn(MockSurfaceAndroidViewController()); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return AndroidWebViewWidget( + AndroidWebViewWidgetCreationParams( + controller: createControllerWithMocks(), + platformViewsServiceProxy: mockPlatformViewsService, + ), + ).build(context); + }, + )); + await tester.pumpAndSettle(); + + verify( + mockPlatformViewsService.initSurfaceAndroidView( + id: anyNamed('id'), + viewType: anyNamed('viewType'), + layoutDirection: anyNamed('layoutDirection'), + creationParams: anyNamed('creationParams'), + creationParamsCodec: anyNamed('creationParamsCodec'), + onFocus: anyNamed('onFocus'), + ), + ); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return AndroidWebViewWidget( + AndroidWebViewWidgetCreationParams( + controller: createControllerWithMocks(), + platformViewsServiceProxy: mockPlatformViewsService, + ), + ).build(context); + }, + )); + await tester.pumpAndSettle(); + + verify( + mockPlatformViewsService.initSurfaceAndroidView( + id: anyNamed('id'), + viewType: anyNamed('viewType'), + layoutDirection: anyNamed('layoutDirection'), + creationParams: anyNamed('creationParams'), + creationParamsCodec: anyNamed('creationParamsCodec'), + onFocus: anyNamed('onFocus'), + ), + ); + }); }); } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index f21422537e68..35b2b8fb0afa 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.7.2 + +* Fixes bug where `PlatformWebViewWidget` doesn't rebuild when the controller changes. + ## 3.7.1 * Updates pigeon version to `10.1.4`. 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 14cba330a747..5b87a39fe0bf 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 @@ -632,6 +632,21 @@ class WebKitWebViewWidgetCreationParams // Maintains instances used to communicate with the native objects they // represent. final InstanceManager _instanceManager; + + @override + int get hashCode => Object.hash( + controller, + layoutDirection, + _instanceManager, + ); + + @override + bool operator ==(Object other) { + return other is WebKitWebViewWidgetCreationParams && + controller == other.controller && + layoutDirection == other.layoutDirection && + _instanceManager == other._instanceManager; + } } /// An implementation of [PlatformWebViewWidget] with the WebKit api. @@ -651,7 +666,9 @@ class WebKitWebViewWidget extends PlatformWebViewWidget { @override Widget build(BuildContext context) { return UiKitView( - key: _webKitParams.key, + // Setting a default key using `params` ensures the `UIKitView` recreates + // the PlatformView when changes are made. + key: _webKitParams.key ?? ObjectKey(params), viewType: 'plugins.flutter.io/webview', onPlatformViewCreated: (_) {}, layoutDirection: params.layoutDirection, diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index f213fbc9a513..6e80a3964971 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/packages/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.7.1 +version: 3.7.2 environment: sdk: ">=2.18.0 <4.0.0" 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 443e3a7163a9..ae9ab6bd1d23 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 @@ -24,37 +24,8 @@ void main() { onWeakReferenceRemoved: (_) {}, ); - final WebKitWebViewController controller = WebKitWebViewController( - WebKitWebViewControllerCreationParams( - webKitProxy: WebKitProxy(createWebView: ( - WKWebViewConfiguration configuration, { - void Function( - String keyPath, - NSObject object, - Map change, - )? observeValue, - InstanceManager? instanceManager, - }) { - final WKWebView webView = WKWebView.detached( - instanceManager: testInstanceManager, - ); - testInstanceManager.addDartCreatedInstance(webView); - return webView; - }, createWebViewConfiguration: ({InstanceManager? instanceManager}) { - return MockWKWebViewConfiguration(); - }, createUIDelegate: ({ - dynamic onCreateWebView, - dynamic requestMediaCapturePermission, - InstanceManager? instanceManager, - }) { - final MockWKUIDelegate mockWKUIDelegate = MockWKUIDelegate(); - when(mockWKUIDelegate.copy()).thenReturn(MockWKUIDelegate()); - - testInstanceManager.addDartCreatedInstance(mockWKUIDelegate); - return mockWKUIDelegate; - }), - ), - ); + final WebKitWebViewController controller = + createTestWebViewController(testInstanceManager); final WebKitWebViewWidget widget = WebKitWebViewWidget( WebKitWebViewWidgetCreationParams( @@ -71,5 +42,86 @@ void main() { expect(find.byType(UiKitView), findsOneWidget); expect(find.byKey(const Key('keyValue')), findsOneWidget); }); + + testWidgets('Key of the PlatformView changes when the controller changes', + (WidgetTester tester) async { + final InstanceManager testInstanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + // Pump WebViewWidget with first controller. + final WebKitWebViewController controller1 = + createTestWebViewController(testInstanceManager); + final WebKitWebViewWidget webViewWidget = WebKitWebViewWidget( + WebKitWebViewWidgetCreationParams( + controller: controller1, + instanceManager: testInstanceManager, + ), + ); + + await tester.pumpWidget( + Builder( + builder: (BuildContext context) => webViewWidget.build(context), + ), + ); + await tester.pumpAndSettle(); + + expect(find.byKey(ObjectKey(webViewWidget.params)), findsOneWidget); + + // Pump WebViewWidget with second controller. + final WebKitWebViewController controller2 = + createTestWebViewController(testInstanceManager); + final WebKitWebViewWidget webViewWidget2 = WebKitWebViewWidget( + WebKitWebViewWidgetCreationParams( + controller: controller2, + instanceManager: testInstanceManager, + ), + ); + + await tester.pumpWidget( + Builder( + builder: (BuildContext context) => webViewWidget2.build(context), + ), + ); + await tester.pumpAndSettle(); + + expect(find.byKey(ObjectKey(webViewWidget2.params)), findsOneWidget); + }); }); } + +WebKitWebViewController createTestWebViewController( + InstanceManager testInstanceManager, +) { + return WebKitWebViewController( + WebKitWebViewControllerCreationParams( + webKitProxy: WebKitProxy(createWebView: ( + WKWebViewConfiguration configuration, { + void Function( + String keyPath, + NSObject object, + Map change, + )? observeValue, + InstanceManager? instanceManager, + }) { + final WKWebView webView = WKWebView.detached( + instanceManager: testInstanceManager, + ); + testInstanceManager.addDartCreatedInstance(webView); + return webView; + }, createWebViewConfiguration: ({InstanceManager? instanceManager}) { + return MockWKWebViewConfiguration(); + }, createUIDelegate: ({ + dynamic onCreateWebView, + dynamic requestMediaCapturePermission, + InstanceManager? instanceManager, + }) { + final MockWKUIDelegate mockWKUIDelegate = MockWKUIDelegate(); + when(mockWKUIDelegate.copy()).thenReturn(MockWKUIDelegate()); + + testInstanceManager.addDartCreatedInstance(mockWKUIDelegate); + return mockWKUIDelegate; + }), + ), + ); +}