diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md index c90205d42c11..1d4ce005146f 100644 --- a/packages/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.18 + +* Add support for onPageStarted event. + ## 0.3.17 * Fix pedantic lint errors. Added missing documentation and awaited some futures diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java index 37ec1c992e26..b660a721d0db 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java @@ -5,6 +5,7 @@ package io.flutter.plugins.webviewflutter; import android.annotation.TargetApi; +import android.graphics.Bitmap; import android.os.Build; import android.util.Log; import android.view.KeyEvent; @@ -66,6 +67,12 @@ private boolean shouldOverrideUrlLoading(WebView view, String url) { return true; } + private void onPageStarted(WebView view, String url) { + Map args = new HashMap<>(); + args.put("url", url); + methodChannel.invokeMethod("onPageStarted", args); + } + private void onPageFinished(WebView view, String url) { Map args = new HashMap<>(); args.put("url", url); @@ -106,6 +113,11 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, request); } + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + FlutterWebViewClient.this.onPageStarted(view, url); + } + @Override public void onPageFinished(WebView view, String url) { FlutterWebViewClient.this.onPageFinished(view, url); @@ -132,6 +144,11 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) { return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, url); } + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + FlutterWebViewClient.this.onPageStarted(view, url); + } + @Override public void onPageFinished(WebView view, String url) { FlutterWebViewClient.this.onPageFinished(view, url); diff --git a/packages/webview_flutter/example/lib/main.dart b/packages/webview_flutter/example/lib/main.dart index 4016e1063dad..20520d1532a4 100644 --- a/packages/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/example/lib/main.dart @@ -68,6 +68,9 @@ class _WebViewExampleState extends State { print('allowing navigation to $request'); return NavigationDecision.navigate; }, + onPageStarted: (String url) { + print('Page started loading: $url'); + }, onPageFinished: (String url) { print('Page finished loading: $url'); }, diff --git a/packages/webview_flutter/example/test_driver/webview_flutter_e2e.dart b/packages/webview_flutter/example/test_driver/webview_flutter_e2e.dart index 324ab6140de5..373e65c6cbdd 100644 --- a/packages/webview_flutter/example/test_driver/webview_flutter_e2e.dart +++ b/packages/webview_flutter/example/test_driver/webview_flutter_e2e.dart @@ -62,6 +62,7 @@ void main() { testWidgets('loadUrl with headers', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); + final StreamController pageStarts = StreamController(); final StreamController pageLoads = StreamController(); await tester.pumpWidget( Directionality( @@ -73,6 +74,9 @@ void main() { controllerCompleter.complete(controller); }, javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarts.add(url); + }, onPageFinished: (String url) { pageLoads.add(url); }, @@ -88,7 +92,9 @@ void main() { final String currentUrl = await controller.currentUrl(); expect(currentUrl, 'https://flutter-header-echo.herokuapp.com/'); + await pageStarts.stream.firstWhere((String url) => url == currentUrl); await pageLoads.stream.firstWhere((String url) => url == currentUrl); + final String content = await controller .evaluateJavascript('document.documentElement.innerText'); expect(content.contains('flutter_test_header'), isTrue); @@ -97,6 +103,7 @@ void main() { testWidgets('JavaScriptChannel', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); + final Completer pageStarted = Completer(); final Completer pageLoaded = Completer(); final List messagesReceived = []; await tester.pumpWidget( @@ -121,6 +128,9 @@ void main() { }, ), ].toSet(), + onPageStarted: (String url) { + pageStarted.complete(null); + }, onPageFinished: (String url) { pageLoaded.complete(null); }, @@ -128,6 +138,7 @@ void main() { ), ); final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; await pageLoaded.future; expect(messagesReceived, isEmpty); @@ -155,6 +166,7 @@ void main() { final String resizeTestBase64 = base64Encode(const Utf8Encoder().convert(resizeTest)); final Completer resizeCompleter = Completer(); + final Completer pageStarted = Completer(); final Completer pageLoaded = Completer(); final Completer controllerCompleter = Completer(); @@ -176,6 +188,9 @@ void main() { }, ), ].toSet(), + onPageStarted: (String url) { + pageStarted.complete(null); + }, onPageFinished: (String url) { pageLoaded.complete(null); }, @@ -198,6 +213,7 @@ void main() { ); await controllerCompleter.future; + await pageStarted.future; await pageLoaded.future; expect(resizeCompleter.isCompleted, false); @@ -343,6 +359,7 @@ void main() { testWidgets('Auto media playback', (WidgetTester tester) async { Completer controllerCompleter = Completer(); + Completer pageStarted = Completer(); Completer pageLoaded = Completer(); await tester.pumpWidget( @@ -355,6 +372,9 @@ void main() { controllerCompleter.complete(controller); }, javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, onPageFinished: (String url) { pageLoaded.complete(null); }, @@ -363,12 +383,14 @@ void main() { ), ); WebViewController controller = await controllerCompleter.future; + await pageStarted.future; await pageLoaded.future; String isPaused = await controller.evaluateJavascript('isPaused();'); expect(isPaused, _webviewBool(false)); controllerCompleter = Completer(); + pageStarted = Completer(); pageLoaded = Completer(); // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy @@ -382,6 +404,9 @@ void main() { controllerCompleter.complete(controller); }, javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, onPageFinished: (String url) { pageLoaded.complete(null); }, @@ -392,6 +417,7 @@ void main() { ); controller = await controllerCompleter.future; + await pageStarted.future; await pageLoaded.future; isPaused = await controller.evaluateJavascript('isPaused();'); @@ -402,6 +428,7 @@ void main() { (WidgetTester tester) async { final Completer controllerCompleter = Completer(); + Completer pageStarted = Completer(); Completer pageLoaded = Completer(); final GlobalKey key = GlobalKey(); @@ -415,6 +442,9 @@ void main() { controllerCompleter.complete(controller); }, javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, onPageFinished: (String url) { pageLoaded.complete(null); }, @@ -423,11 +453,13 @@ void main() { ), ); final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; await pageLoaded.future; String isPaused = await controller.evaluateJavascript('isPaused();'); expect(isPaused, _webviewBool(false)); + pageStarted = Completer(); pageLoaded = Completer(); await tester.pumpWidget( @@ -440,6 +472,9 @@ void main() { controllerCompleter.complete(controller); }, javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, onPageFinished: (String url) { pageLoaded.complete(null); }, @@ -451,6 +486,7 @@ void main() { await controller.reload(); + await pageStarted.future; await pageLoaded.future; isPaused = await controller.evaluateJavascript('isPaused();'); @@ -469,6 +505,7 @@ void main() { '''; final String getTitleTestBase64 = base64Encode(const Utf8Encoder().convert(getTitleTest)); + final Completer pageStarted = Completer(); final Completer pageLoaded = Completer(); final Completer controllerCompleter = Completer(); @@ -481,6 +518,9 @@ void main() { onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, onPageFinished: (String url) { pageLoaded.complete(null); }, @@ -489,6 +529,7 @@ void main() { ); final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; await pageLoaded.future; final String title = await controller.getTitle(); diff --git a/packages/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m b/packages/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m index abcca0a5e8a9..3e9d2762c7fa 100644 --- a/packages/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m +++ b/packages/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m @@ -16,6 +16,12 @@ - (instancetype)initWithChannel:(FlutterMethodChannel*)channel { return self; } +#pragma mark - WKNavigationDelegate conformance + +- (void)webView:(WKWebView*)webView didStartProvisionalNavigation:(WKNavigation*)navigation { + [_methodChannel invokeMethod:@"onPageStarted" arguments:@{@"url" : webView.URL.absoluteString}]; +} + - (void)webView:(WKWebView*)webView decidePolicyForNavigationAction:(WKNavigationAction*)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { diff --git a/packages/webview_flutter/lib/platform_interface.dart b/packages/webview_flutter/lib/platform_interface.dart index 3646869b84ab..a3af47a5c714 100644 --- a/packages/webview_flutter/lib/platform_interface.dart +++ b/packages/webview_flutter/lib/platform_interface.dart @@ -23,6 +23,9 @@ abstract class WebViewPlatformCallbacksHandler { /// If true is returned the navigation is allowed, otherwise it is blocked. FutureOr onNavigationRequest({String url, bool isForMainFrame}); + /// Invoked by [WebViewPlatformController] when a page has started loading. + void onPageStarted(String url); + /// Invoked by [WebViewPlatformController] when a page has finished loading. void onPageFinished(String url); } diff --git a/packages/webview_flutter/lib/src/webview_method_channel.dart b/packages/webview_flutter/lib/src/webview_method_channel.dart index ba5b9583db4a..ad5a81e98ef5 100644 --- a/packages/webview_flutter/lib/src/webview_method_channel.dart +++ b/packages/webview_flutter/lib/src/webview_method_channel.dart @@ -40,6 +40,9 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { case 'onPageFinished': _platformCallbacksHandler.onPageFinished(call.arguments['url']); return null; + case 'onPageStarted': + _platformCallbacksHandler.onPageStarted(call.arguments['url']); + return null; } throw MissingPluginException( '${call.method} was invoked but has no handler'); diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index c17f8b9a25b0..a57e2e13bdba 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -73,6 +73,9 @@ enum NavigationDecision { typedef FutureOr NavigationDelegate( NavigationRequest navigation); +/// Signature for when a [WebView] has started loading a page. +typedef void PageStartedCallback(String url); + /// Signature for when a [WebView] has finished loading a page. typedef void PageFinishedCallback(String url); @@ -142,6 +145,7 @@ class WebView extends StatefulWidget { this.javascriptChannels, this.navigationDelegate, this.gestureRecognizers, + this.onPageStarted, this.onPageFinished, this.debuggingEnabled = false, this.userAgent, @@ -257,6 +261,9 @@ class WebView extends StatefulWidget { /// * When a navigationDelegate is set HTTP requests do not include the HTTP referer header. final NavigationDelegate navigationDelegate; + /// Invoked when a page starts loading. + final PageStartedCallback onPageStarted; + /// Invoked when a page has finished loading. /// /// This is invoked only for the main frame. @@ -452,6 +459,13 @@ class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { return allowNavigation; } + @override + void onPageStarted(String url) { + if (_widget.onPageStarted != null) { + _widget.onPageStarted(url); + } + } + @override void onPageFinished(String url) { if (_widget.onPageFinished != null) { diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart index 728686f45e5b..1772df3fa815 100644 --- a/packages/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/test/webview_flutter_test.dart @@ -601,6 +601,63 @@ void main() { expect(ttsMessagesReceived, ['Hello', 'World']); }); + group('$PageStartedCallback', () { + testWidgets('onPageStarted is not null', (WidgetTester tester) async { + String returnedUrl; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageStarted: (String url) { + returnedUrl = url; + }, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + platformWebView.fakeOnPageStartedCallback(); + + expect(platformWebView.currentUrl, returnedUrl); + }); + + testWidgets('onPageStarted is null', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + onPageStarted: null, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + // The platform side will always invoke a call for onPageStarted. This is + // to test that it does not crash on a null callback. + platformWebView.fakeOnPageStartedCallback(); + }); + + testWidgets('onPageStarted changed', (WidgetTester tester) async { + String returnedUrl; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageStarted: (String url) {}, + )); + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageStarted: (String url) { + returnedUrl = url; + }, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + platformWebView.fakeOnPageStartedCallback(); + + expect(platformWebView.currentUrl, returnedUrl); + }); + }); + group('$PageFinishedCallback', () { testWidgets('onPageFinished is not null', (WidgetTester tester) async { String returnedUrl; @@ -968,6 +1025,24 @@ class FakePlatformWebView { }); } + void fakeOnPageStartedCallback() { + final StandardMethodCodec codec = const StandardMethodCodec(); + + final ByteData data = codec.encodeMethodCall(MethodCall( + 'onPageStarted', + {'url': currentUrl}, + )); + + // TODO(hterkelsen): Remove this when defaultBinaryMessages is in stable. + // https://github.com/flutter/flutter/issues/33446 + // ignore: deprecated_member_use + BinaryMessages.handlePlatformMessage( + channel.name, + data, + (ByteData data) {}, + ); + } + void fakeOnPageFinishedCallback() { final StandardMethodCodec codec = const StandardMethodCodec();