[webview_flutter] Add support for onPageStarted event (#2295)

* Added support for webView's 'onPageStarted' event
diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md
index c90205d..1d4ce00 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 37ec1c9..b660a72 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 @@
     return true;
   }
 
+  private void onPageStarted(WebView view, String url) {
+    Map<String, Object> args = new HashMap<>();
+    args.put("url", url);
+    methodChannel.invokeMethod("onPageStarted", args);
+  }
+
   private void onPageFinished(WebView view, String url) {
     Map<String, Object> args = new HashMap<>();
     args.put("url", url);
@@ -107,6 +114,11 @@
       }
 
       @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);
       }
@@ -133,6 +145,11 @@
       }
 
       @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 4016e10..20520d1 100644
--- a/packages/webview_flutter/example/lib/main.dart
+++ b/packages/webview_flutter/example/lib/main.dart
@@ -68,6 +68,9 @@
             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 324ab61..373e65c 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 @@
   testWidgets('loadUrl with headers', (WidgetTester tester) async {
     final Completer<WebViewController> controllerCompleter =
         Completer<WebViewController>();
+    final StreamController<String> pageStarts = StreamController<String>();
     final StreamController<String> pageLoads = StreamController<String>();
     await tester.pumpWidget(
       Directionality(
@@ -73,6 +74,9 @@
             controllerCompleter.complete(controller);
           },
           javascriptMode: JavascriptMode.unrestricted,
+          onPageStarted: (String url) {
+            pageStarts.add(url);
+          },
           onPageFinished: (String url) {
             pageLoads.add(url);
           },
@@ -88,7 +92,9 @@
     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 @@
   testWidgets('JavaScriptChannel', (WidgetTester tester) async {
     final Completer<WebViewController> controllerCompleter =
         Completer<WebViewController>();
+    final Completer<void> pageStarted = Completer<void>();
     final Completer<void> pageLoaded = Completer<void>();
     final List<String> messagesReceived = <String>[];
     await tester.pumpWidget(
@@ -121,6 +128,9 @@
               },
             ),
           ].toSet(),
+          onPageStarted: (String url) {
+            pageStarted.complete(null);
+          },
           onPageFinished: (String url) {
             pageLoaded.complete(null);
           },
@@ -128,6 +138,7 @@
       ),
     );
     final WebViewController controller = await controllerCompleter.future;
+    await pageStarted.future;
     await pageLoaded.future;
 
     expect(messagesReceived, isEmpty);
@@ -155,6 +166,7 @@
     final String resizeTestBase64 =
         base64Encode(const Utf8Encoder().convert(resizeTest));
     final Completer<void> resizeCompleter = Completer<void>();
+    final Completer<void> pageStarted = Completer<void>();
     final Completer<void> pageLoaded = Completer<void>();
     final Completer<WebViewController> controllerCompleter =
         Completer<WebViewController>();
@@ -176,6 +188,9 @@
           },
         ),
       ].toSet(),
+      onPageStarted: (String url) {
+        pageStarted.complete(null);
+      },
       onPageFinished: (String url) {
         pageLoaded.complete(null);
       },
@@ -198,6 +213,7 @@
     );
 
     await controllerCompleter.future;
+    await pageStarted.future;
     await pageLoaded.future;
 
     expect(resizeCompleter.isCompleted, false);
@@ -343,6 +359,7 @@
     testWidgets('Auto media playback', (WidgetTester tester) async {
       Completer<WebViewController> controllerCompleter =
           Completer<WebViewController>();
+      Completer<void> pageStarted = Completer<void>();
       Completer<void> pageLoaded = Completer<void>();
 
       await tester.pumpWidget(
@@ -355,6 +372,9 @@
               controllerCompleter.complete(controller);
             },
             javascriptMode: JavascriptMode.unrestricted,
+            onPageStarted: (String url) {
+              pageStarted.complete(null);
+            },
             onPageFinished: (String url) {
               pageLoaded.complete(null);
             },
@@ -363,12 +383,14 @@
         ),
       );
       WebViewController controller = await controllerCompleter.future;
+      await pageStarted.future;
       await pageLoaded.future;
 
       String isPaused = await controller.evaluateJavascript('isPaused();');
       expect(isPaused, _webviewBool(false));
 
       controllerCompleter = Completer<WebViewController>();
+      pageStarted = Completer<void>();
       pageLoaded = Completer<void>();
 
       // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy
@@ -382,6 +404,9 @@
               controllerCompleter.complete(controller);
             },
             javascriptMode: JavascriptMode.unrestricted,
+            onPageStarted: (String url) {
+              pageStarted.complete(null);
+            },
             onPageFinished: (String url) {
               pageLoaded.complete(null);
             },
@@ -392,6 +417,7 @@
       );
 
       controller = await controllerCompleter.future;
+      await pageStarted.future;
       await pageLoaded.future;
 
       isPaused = await controller.evaluateJavascript('isPaused();');
@@ -402,6 +428,7 @@
         (WidgetTester tester) async {
       final Completer<WebViewController> controllerCompleter =
           Completer<WebViewController>();
+      Completer<void> pageStarted = Completer<void>();
       Completer<void> pageLoaded = Completer<void>();
 
       final GlobalKey key = GlobalKey();
@@ -415,6 +442,9 @@
               controllerCompleter.complete(controller);
             },
             javascriptMode: JavascriptMode.unrestricted,
+            onPageStarted: (String url) {
+              pageStarted.complete(null);
+            },
             onPageFinished: (String url) {
               pageLoaded.complete(null);
             },
@@ -423,11 +453,13 @@
         ),
       );
       final WebViewController controller = await controllerCompleter.future;
+      await pageStarted.future;
       await pageLoaded.future;
 
       String isPaused = await controller.evaluateJavascript('isPaused();');
       expect(isPaused, _webviewBool(false));
 
+      pageStarted = Completer<void>();
       pageLoaded = Completer<void>();
 
       await tester.pumpWidget(
@@ -440,6 +472,9 @@
               controllerCompleter.complete(controller);
             },
             javascriptMode: JavascriptMode.unrestricted,
+            onPageStarted: (String url) {
+              pageStarted.complete(null);
+            },
             onPageFinished: (String url) {
               pageLoaded.complete(null);
             },
@@ -451,6 +486,7 @@
 
       await controller.reload();
 
+      await pageStarted.future;
       await pageLoaded.future;
 
       isPaused = await controller.evaluateJavascript('isPaused();');
@@ -469,6 +505,7 @@
       ''';
     final String getTitleTestBase64 =
         base64Encode(const Utf8Encoder().convert(getTitleTest));
+    final Completer<void> pageStarted = Completer<void>();
     final Completer<void> pageLoaded = Completer<void>();
     final Completer<WebViewController> controllerCompleter =
         Completer<WebViewController>();
@@ -481,6 +518,9 @@
           onWebViewCreated: (WebViewController controller) {
             controllerCompleter.complete(controller);
           },
+          onPageStarted: (String url) {
+            pageStarted.complete(null);
+          },
           onPageFinished: (String url) {
             pageLoaded.complete(null);
           },
@@ -489,6 +529,7 @@
     );
 
     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 abcca0a..3e9d276 100644
--- a/packages/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m
+++ b/packages/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m
@@ -16,6 +16,12 @@
   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 3646869..a3af47a 100644
--- a/packages/webview_flutter/lib/platform_interface.dart
+++ b/packages/webview_flutter/lib/platform_interface.dart
@@ -23,6 +23,9 @@
   /// If true is returned the navigation is allowed, otherwise it is blocked.
   FutureOr<bool> 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 ba5b958..ad5a81e 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 @@
       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 c17f8b9..a57e2e1 100644
--- a/packages/webview_flutter/lib/webview_flutter.dart
+++ b/packages/webview_flutter/lib/webview_flutter.dart
@@ -73,6 +73,9 @@
 typedef FutureOr<NavigationDecision> 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 @@
     this.javascriptChannels,
     this.navigationDelegate,
     this.gestureRecognizers,
+    this.onPageStarted,
     this.onPageFinished,
     this.debuggingEnabled = false,
     this.userAgent,
@@ -257,6 +261,9 @@
   ///     * 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.
@@ -453,6 +460,13 @@
   }
 
   @override
+  void onPageStarted(String url) {
+    if (_widget.onPageStarted != null) {
+      _widget.onPageStarted(url);
+    }
+  }
+
+  @override
   void onPageFinished(String url) {
     if (_widget.onPageFinished != null) {
       _widget.onPageFinished(url);
diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart
index 728686f..1772df3 100644
--- a/packages/webview_flutter/test/webview_flutter_test.dart
+++ b/packages/webview_flutter/test/webview_flutter_test.dart
@@ -601,6 +601,63 @@
     expect(ttsMessagesReceived, <String>['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 @@
     });
   }
 
+  void fakeOnPageStartedCallback() {
+    final StandardMethodCodec codec = const StandardMethodCodec();
+
+    final ByteData data = codec.encodeMethodCall(MethodCall(
+      'onPageStarted',
+      <dynamic, dynamic>{'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();