Merge branch 'v4_webview' of github.com:flutter/plugins into v4_webview_android_navigation_delegate
diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart
index a9d8d12..8aa5f7d 100644
--- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart
+++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart
@@ -128,10 +128,9 @@
 class WebViewHostApiImpl extends WebViewHostApi {
   /// Constructs a [WebViewHostApiImpl].
   WebViewHostApiImpl({
-    BinaryMessenger? binaryMessenger,
+    super.binaryMessenger,
     InstanceManager? instanceManager,
-  })  : instanceManager = instanceManager ?? JavaObject.globalInstanceManager,
-        super(binaryMessenger: binaryMessenger);
+  }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager;
 
   /// Maintains instances stored to communicate with java objects.
   final InstanceManager instanceManager;
@@ -342,10 +341,9 @@
 class WebSettingsHostApiImpl extends WebSettingsHostApi {
   /// Constructs a [WebSettingsHostApiImpl].
   WebSettingsHostApiImpl({
-    BinaryMessenger? binaryMessenger,
+    super.binaryMessenger,
     InstanceManager? instanceManager,
-  })  : instanceManager = instanceManager ?? JavaObject.globalInstanceManager,
-        super(binaryMessenger: binaryMessenger);
+  }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager;
 
   /// Maintains instances stored to communicate with java objects.
   final InstanceManager instanceManager;
@@ -484,10 +482,9 @@
 class JavaScriptChannelHostApiImpl extends JavaScriptChannelHostApi {
   /// Constructs a [JavaScriptChannelHostApiImpl].
   JavaScriptChannelHostApiImpl({
-    BinaryMessenger? binaryMessenger,
+    super.binaryMessenger,
     InstanceManager? instanceManager,
-  })  : instanceManager = instanceManager ?? JavaObject.globalInstanceManager,
-        super(binaryMessenger: binaryMessenger);
+  }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager;
 
   /// Maintains instances stored to communicate with java objects.
   final InstanceManager instanceManager;
@@ -529,10 +526,9 @@
 class WebViewClientHostApiImpl extends WebViewClientHostApi {
   /// Constructs a [WebViewClientHostApiImpl].
   WebViewClientHostApiImpl({
-    BinaryMessenger? binaryMessenger,
+    super.binaryMessenger,
     InstanceManager? instanceManager,
-  })  : instanceManager = instanceManager ?? JavaObject.globalInstanceManager,
-        super(binaryMessenger: binaryMessenger);
+  }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager;
 
   /// Maintains instances stored to communicate with java objects.
   final InstanceManager instanceManager;
@@ -717,10 +713,9 @@
 class DownloadListenerHostApiImpl extends DownloadListenerHostApi {
   /// Constructs a [DownloadListenerHostApiImpl].
   DownloadListenerHostApiImpl({
-    BinaryMessenger? binaryMessenger,
+    super.binaryMessenger,
     InstanceManager? instanceManager,
-  })  : instanceManager = instanceManager ?? JavaObject.globalInstanceManager,
-        super(binaryMessenger: binaryMessenger);
+  }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager;
 
   /// Maintains instances stored to communicate with java objects.
   final InstanceManager instanceManager;
@@ -772,10 +767,9 @@
 class WebChromeClientHostApiImpl extends WebChromeClientHostApi {
   /// Constructs a [WebChromeClientHostApiImpl].
   WebChromeClientHostApiImpl({
-    BinaryMessenger? binaryMessenger,
+    super.binaryMessenger,
     InstanceManager? instanceManager,
-  })  : instanceManager = instanceManager ?? JavaObject.globalInstanceManager,
-        super(binaryMessenger: binaryMessenger);
+  }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager;
 
   /// Maintains instances stored to communicate with java objects.
   final InstanceManager instanceManager;
@@ -822,10 +816,9 @@
 class WebStorageHostApiImpl extends WebStorageHostApi {
   /// Constructs a [WebStorageHostApiImpl].
   WebStorageHostApiImpl({
-    BinaryMessenger? binaryMessenger,
+    super.binaryMessenger,
     InstanceManager? instanceManager,
-  })  : instanceManager = instanceManager ?? JavaObject.globalInstanceManager,
-        super(binaryMessenger: binaryMessenger);
+  }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager;
 
   /// Maintains instances stored to communicate with java objects.
   final InstanceManager instanceManager;
diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/v4/src/android_navigation_delegate.dart b/packages/webview_flutter/webview_flutter_android/lib/src/v4/src/android_navigation_delegate.dart
new file mode 100644
index 0000000..0d2630f
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/lib/src/v4/src/android_navigation_delegate.dart
@@ -0,0 +1,279 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart';
+
+import '../../android_webview.dart' as android_webview;
+import 'android_proxy.dart';
+
+/// Signature for the `loadUrl` callback responsible for loading the [url]
+/// after a navigation request has been approved.
+typedef LoadUrlCallback = Future<void> Function(
+    String url, Map<String, String>? headers);
+
+/// Error returned in `WebView.onWebResourceError` when a web resource loading error has occurred.
+class AndroidWebResourceError extends WebResourceError {
+  /// Creates a new [AndroidWebResourceError].
+  AndroidWebResourceError._({
+    required super.errorCode,
+    required super.description,
+    this.failingUrl,
+  }) : super(
+          errorType: _errorCodeToErrorType(errorCode),
+        );
+
+  /// Gets the URL for which the failing resource request was made.
+  final String? failingUrl;
+
+  static WebResourceErrorType? _errorCodeToErrorType(int errorCode) {
+    switch (errorCode) {
+      case android_webview.WebViewClient.errorAuthentication:
+        return WebResourceErrorType.authentication;
+      case android_webview.WebViewClient.errorBadUrl:
+        return WebResourceErrorType.badUrl;
+      case android_webview.WebViewClient.errorConnect:
+        return WebResourceErrorType.connect;
+      case android_webview.WebViewClient.errorFailedSslHandshake:
+        return WebResourceErrorType.failedSslHandshake;
+      case android_webview.WebViewClient.errorFile:
+        return WebResourceErrorType.file;
+      case android_webview.WebViewClient.errorFileNotFound:
+        return WebResourceErrorType.fileNotFound;
+      case android_webview.WebViewClient.errorHostLookup:
+        return WebResourceErrorType.hostLookup;
+      case android_webview.WebViewClient.errorIO:
+        return WebResourceErrorType.io;
+      case android_webview.WebViewClient.errorProxyAuthentication:
+        return WebResourceErrorType.proxyAuthentication;
+      case android_webview.WebViewClient.errorRedirectLoop:
+        return WebResourceErrorType.redirectLoop;
+      case android_webview.WebViewClient.errorTimeout:
+        return WebResourceErrorType.timeout;
+      case android_webview.WebViewClient.errorTooManyRequests:
+        return WebResourceErrorType.tooManyRequests;
+      case android_webview.WebViewClient.errorUnknown:
+        return WebResourceErrorType.unknown;
+      case android_webview.WebViewClient.errorUnsafeResource:
+        return WebResourceErrorType.unsafeResource;
+      case android_webview.WebViewClient.errorUnsupportedAuthScheme:
+        return WebResourceErrorType.unsupportedAuthScheme;
+      case android_webview.WebViewClient.errorUnsupportedScheme:
+        return WebResourceErrorType.unsupportedScheme;
+    }
+
+    throw ArgumentError(
+      'Could not find a WebResourceErrorType for errorCode: $errorCode',
+    );
+  }
+}
+
+/// Object specifying creation parameters for creating a [AndroidNavigationDelegate].
+///
+/// When adding additional fields make sure they can be null or have a default
+/// value to avoid breaking changes. See [PlatformNavigationDelegateCreationParams] for
+/// more information.
+@immutable
+class AndroidNavigationDelegateCreationParams
+    extends PlatformNavigationDelegateCreationParams {
+  /// Creates a new [AndroidNavigationDelegateCreationParams] instance.
+  const AndroidNavigationDelegateCreationParams._({
+    this.loadUrl,
+    @visibleForTesting this.androidWebViewProxy = const AndroidWebViewProxy(),
+  }) : super();
+
+  /// Creates a [AndroidNavigationDelegateCreationParams] instance based on [PlatformNavigationDelegateCreationParams].
+  factory AndroidNavigationDelegateCreationParams.fromPlatformNavigationDelegateCreationParams(
+    // Recommended placeholder to prevent being broken by platform interface.
+    // ignore: avoid_unused_constructor_parameters
+    PlatformNavigationDelegateCreationParams params, {
+    LoadUrlCallback? loadUrl,
+    @visibleForTesting
+        AndroidWebViewProxy androidWebViewProxy = const AndroidWebViewProxy(),
+  }) {
+    return AndroidNavigationDelegateCreationParams._(
+      loadUrl: loadUrl,
+      androidWebViewProxy: androidWebViewProxy,
+    );
+  }
+
+  /// Callback responsible for loading the [url] after a navigation request is approved.
+  final LoadUrlCallback? loadUrl;
+
+  /// Handles constructing objects and calling static methods for the Android WebView
+  /// native library.
+  @visibleForTesting
+  final AndroidWebViewProxy androidWebViewProxy;
+}
+
+/// A place to register callback methods responsible to handle navigation events
+/// triggered by the [android_webview.WebView].
+class AndroidNavigationDelegate extends PlatformNavigationDelegate {
+  /// Creates a new [AndroidNavigationkDelegate].
+  AndroidNavigationDelegate(PlatformNavigationDelegateCreationParams params)
+      : super.implementation(params is AndroidNavigationDelegateCreationParams
+            ? params
+            : AndroidNavigationDelegateCreationParams
+                .fromPlatformNavigationDelegateCreationParams(params)) {
+    final WeakReference<AndroidNavigationDelegate> weakThis =
+        WeakReference<AndroidNavigationDelegate>(this);
+
+    _webChromeClient = (params as AndroidNavigationDelegateCreationParams)
+        .androidWebViewProxy
+        .createAndroidWebChromeClient(
+            onProgressChanged: (android_webview.WebView webView, int progress) {
+      if (weakThis.target?._onProgress != null) {
+        weakThis.target!._onProgress!(progress);
+      }
+    });
+
+    _webViewClient = params.androidWebViewProxy.createAndroidWebViewClient(
+      onPageFinished: (android_webview.WebView webView, String url) {
+        if (weakThis.target?._onPageFinished != null) {
+          weakThis.target!._onPageFinished!(url);
+        }
+      },
+      onPageStarted: (android_webview.WebView webView, String url) {
+        if (weakThis.target?._onPageStarted != null) {
+          weakThis.target!._onPageStarted!(url);
+        }
+      },
+      onReceivedRequestError: (
+        android_webview.WebView webView,
+        android_webview.WebResourceRequest request,
+        android_webview.WebResourceError error,
+      ) {
+        if (weakThis.target?._onWebResourceError != null &&
+            request.isForMainFrame) {
+          weakThis.target!._onWebResourceError!(AndroidWebResourceError._(
+            errorCode: error.errorCode,
+            description: error.description,
+            failingUrl: request.url,
+          ));
+        }
+      },
+      onReceivedError: (
+        android_webview.WebView webView,
+        int errorCode,
+        String description,
+        String failingUrl,
+      ) {
+        if (weakThis.target?._onWebResourceError != null) {
+          weakThis.target!._onWebResourceError!(AndroidWebResourceError._(
+            errorCode: errorCode,
+            description: description,
+            failingUrl: failingUrl,
+          ));
+        }
+      },
+      requestLoading: (
+        android_webview.WebView webView,
+        android_webview.WebResourceRequest request,
+      ) {
+        if (weakThis.target != null) {
+          weakThis.target!._handleNavigation(
+            request.url,
+            request.requestHeaders,
+          );
+        }
+      },
+      urlLoading: (
+        android_webview.WebView webView,
+        String url,
+      ) {
+        if (weakThis.target != null) {
+          weakThis.target!._handleNavigation(url, <String, String>{});
+        }
+      },
+    );
+  }
+
+  late final android_webview.WebChromeClient _webChromeClient;
+
+  /// Gets the native [android_webview.WebChromeClient] that is bridged by this [AndroidNavigationDelegate].
+  ///
+  /// Used by the [AndroidWebViewController] to set the `android_webview.WebView.setWebChromeClient`.
+  android_webview.WebChromeClient get androidWebChromeClient =>
+      _webChromeClient;
+
+  late final android_webview.WebViewClient _webViewClient;
+
+  /// Gets the native [android_webview.WebViewClient] that is bridged by this [AndroidNavigationDelegate].
+  ///
+  /// Used by the [AndroidWebViewController] to set the `android_webview.WebView.setWebViewClient`.
+  android_webview.WebViewClient get androidWebViewClient => _webViewClient;
+
+  PageEventCallback? _onPageFinished;
+  PageEventCallback? _onPageStarted;
+  ProgressCallback? _onProgress;
+  WebResourceErrorCallback? _onWebResourceError;
+  NavigationRequestCallback? _onNavigationRequest;
+
+  AndroidNavigationDelegateCreationParams get _androidParams =>
+      params as AndroidNavigationDelegateCreationParams;
+
+  bool get _handlesNavigation =>
+      _onNavigationRequest != null && _androidParams.loadUrl != null;
+
+  void _handleNavigation(String url, Map<String, String> headers) {
+    if (!_handlesNavigation) {
+      return;
+    }
+
+    final FutureOr<NavigationDecision> returnValue = _onNavigationRequest!(
+      NavigationRequest(
+        isMainFrame: true,
+        url: url,
+      ),
+    );
+
+    if (returnValue is NavigationDecision &&
+        returnValue == NavigationDecision.navigate) {
+      _androidParams.loadUrl!(url, headers);
+    } else if (returnValue is Future<NavigationDecision>) {
+      returnValue.then((NavigationDecision shouldLoadUrl) {
+        if (shouldLoadUrl == NavigationDecision.navigate) {
+          _androidParams.loadUrl!(url, headers);
+        }
+      });
+    }
+  }
+
+  @override
+  Future<void> setOnNavigationRequest(
+    NavigationRequestCallback onNavigationRequest,
+  ) async {
+    _onNavigationRequest = onNavigationRequest;
+  }
+
+  @override
+  Future<void> setOnPageStarted(
+    PageEventCallback onPageStarted,
+  ) async {
+    _onPageStarted = onPageStarted;
+  }
+
+  @override
+  Future<void> setOnPageFinished(
+    PageEventCallback onPageFinished,
+  ) async {
+    _onPageFinished = onPageFinished;
+  }
+
+  @override
+  Future<void> setOnProgress(
+    ProgressCallback onProgress,
+  ) async {
+    _onProgress = onProgress;
+  }
+
+  @override
+  Future<void> setOnWebResourceError(
+    WebResourceErrorCallback onWebResourceError,
+  ) async {
+    _onWebResourceError = onWebResourceError;
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/v4/src/android_proxy.dart b/packages/webview_flutter/webview_flutter_android/lib/src/v4/src/android_proxy.dart
new file mode 100644
index 0000000..757397a
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/lib/src/v4/src/android_proxy.dart
@@ -0,0 +1,55 @@
+// 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.
+
+import '../../android_webview.dart' as android_webview;
+
+/// Handles constructing objects and calling static methods for the Android
+/// WebView native library.
+///
+/// This class provides dependency injection for the implementations of the
+/// platform interface classes. Improving the ease of unit testing and/or
+/// overriding the underlying Android WebView classes.
+///
+/// By default each function calls the default constructor of the WebView class
+/// it intends to return.
+class AndroidWebViewProxy {
+  /// Constructs a [AndroidWebViewProxy].
+  const AndroidWebViewProxy({
+    this.createAndroidWebChromeClient = android_webview.WebChromeClient.new,
+    this.createAndroidWebViewClient = android_webview.WebViewClient.new,
+  });
+
+  /// Constructs a [android_webview.WebChromeClient].
+  final android_webview.WebChromeClient Function({
+    void Function(android_webview.WebView webView, int progress)?
+        onProgressChanged,
+  }) createAndroidWebChromeClient;
+
+  /// Constructs a [android_webview.WebViewClient].
+  final android_webview.WebViewClient Function({
+    bool shouldOverrideUrlLoading,
+    void Function(android_webview.WebView webView, String url)? onPageStarted,
+    void Function(android_webview.WebView webView, String url)? onPageFinished,
+    void Function(
+      android_webview.WebView webView,
+      android_webview.WebResourceRequest request,
+      android_webview.WebResourceError error,
+    )?
+        onReceivedRequestError,
+    @Deprecated('Only called on Android version < 23.')
+        void Function(
+      android_webview.WebView webView,
+      int errorCode,
+      String description,
+      String failingUrl,
+    )?
+            onReceivedError,
+    void Function(
+      android_webview.WebView webView,
+      android_webview.WebResourceRequest request,
+    )?
+        requestLoading,
+    void Function(android_webview.WebView webView, String url)? urlLoading,
+  }) createAndroidWebViewClient;
+}
diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/v4/src/android_webview_cookie_manager.dart b/packages/webview_flutter/webview_flutter_android/lib/src/v4/src/android_webview_cookie_manager.dart
index 9a64a62..127b7e4 100644
--- a/packages/webview_flutter/webview_flutter_android/lib/src/v4/src/android_webview_cookie_manager.dart
+++ b/packages/webview_flutter/webview_flutter_android/lib/src/v4/src/android_webview_cookie_manager.dart
@@ -44,10 +44,10 @@
   /// with the default [AndroidWebViewCookieManager] constructor.
   @visibleForTesting
   AndroidWebViewCookieManager.fromNativeApi(
-    AndroidWebViewCookieManagerCreationParams params, {
+    AndroidWebViewCookieManagerCreationParams super.params, {
     required CookieManager cookieManager,
   })  : _cookieManager = cookieManager,
-        super.implementation(params);
+        super.implementation();
 
   final CookieManager _cookieManager;
 
diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart
index d1fa1b8..4cb99b5 100644
--- a/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart
+++ b/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart
@@ -16,7 +16,7 @@
 class WebViewAndroidWidget extends StatefulWidget {
   /// Constructs a [WebViewAndroidWidget].
   const WebViewAndroidWidget({
-    Key? key,
+    super.key,
     required this.creationParams,
     required this.useHybridComposition,
     required this.callbacksHandler,
@@ -26,7 +26,7 @@
     @visibleForTesting
         this.flutterAssetManager = const android_webview.FlutterAssetManager(),
     @visibleForTesting this.webStorage,
-  }) : super(key: key);
+  });
 
   /// Initial parameters used to setup the WebView.
   final CreationParams creationParams;
@@ -570,10 +570,9 @@
     extends android_webview.JavaScriptChannel {
   /// Creates a [WebViewAndroidJavaScriptChannel].
   WebViewAndroidJavaScriptChannel(
-    String channelName,
+    super.channelName,
     this.javascriptChannelRegistry,
   ) : super(
-          channelName,
           postMessage: withWeakRefenceTo(
             javascriptChannelRegistry,
             (WeakReference<JavascriptChannelRegistry> weakReference) {
diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml
index 8f7cd9b..8384b7c 100644
--- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml
+++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml
@@ -6,7 +6,7 @@
 publish_to: none
 
 environment:
-  sdk: ">=2.14.0 <3.0.0"
+  sdk: ">=2.17.0 <3.0.0"
   flutter: ">=3.0.0"
 
 flutter:
diff --git a/packages/webview_flutter/webview_flutter_android/test/v4/android_navigation_delegate_test.dart b/packages/webview_flutter/webview_flutter_android/test/v4/android_navigation_delegate_test.dart
new file mode 100644
index 0000000..dfde021
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/test/v4/android_navigation_delegate_test.dart
@@ -0,0 +1,396 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:webview_flutter_android/src/android_webview.dart'
+    as android_webview;
+import 'package:webview_flutter_android/src/v4/src/android_navigation_delegate.dart';
+import 'package:webview_flutter_android/src/v4/src/android_proxy.dart';
+import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart';
+
+void main() {
+  group('AndroidNavigationDelegate', () {
+    test('onPageFinished', () {
+      final AndroidNavigationDelegate webKitDelegate =
+          AndroidNavigationDelegate(_buildCreationParams());
+
+      late final String callbackUrl;
+      webKitDelegate.setOnPageFinished((String url) => callbackUrl = url);
+
+      CapturingWebViewClient.lastCreatedDelegate.onPageFinished!(
+        android_webview.WebView.detached(),
+        'https://www.google.com',
+      );
+
+      expect(callbackUrl, 'https://www.google.com');
+    });
+
+    test('onPageStarted', () {
+      final AndroidNavigationDelegate webKitDelegate =
+          AndroidNavigationDelegate(_buildCreationParams());
+
+      late final String callbackUrl;
+      webKitDelegate.setOnPageStarted((String url) => callbackUrl = url);
+
+      CapturingWebViewClient.lastCreatedDelegate.onPageStarted!(
+        android_webview.WebView.detached(),
+        'https://www.google.com',
+      );
+
+      expect(callbackUrl, 'https://www.google.com');
+    });
+
+    test('onWebResourceError from onReceivedRequestError', () {
+      final AndroidNavigationDelegate webKitDelegate =
+          AndroidNavigationDelegate(_buildCreationParams());
+
+      late final WebResourceError callbackError;
+      webKitDelegate.setOnWebResourceError(
+          (WebResourceError error) => callbackError = error);
+
+      CapturingWebViewClient.lastCreatedDelegate.onReceivedRequestError!(
+        android_webview.WebView.detached(),
+        android_webview.WebResourceRequest(
+          url: 'https://www.google.com',
+          isForMainFrame: true,
+          isRedirect: true,
+          hasGesture: true,
+          method: 'GET',
+          requestHeaders: <String, String>{'X-Mock': 'mocking'},
+        ),
+        android_webview.WebResourceError(
+          errorCode: android_webview.WebViewClient.errorFileNotFound,
+          description: 'Page not found.',
+        ),
+      );
+
+      expect(callbackError.errorCode,
+          android_webview.WebViewClient.errorFileNotFound);
+      expect(callbackError.description, 'Page not found.');
+      expect(callbackError.errorType, WebResourceErrorType.fileNotFound);
+    });
+
+    test('onWebResourceError from onRequestError', () {
+      final AndroidNavigationDelegate webKitDelegate =
+          AndroidNavigationDelegate(_buildCreationParams());
+
+      late final WebResourceError callbackError;
+      webKitDelegate.setOnWebResourceError(
+          (WebResourceError error) => callbackError = error);
+
+      CapturingWebViewClient.lastCreatedDelegate.onReceivedError!(
+        android_webview.WebView.detached(),
+        android_webview.WebViewClient.errorFileNotFound,
+        'Page not found.',
+        'https://www.google.com',
+      );
+
+      expect(callbackError.errorCode,
+          android_webview.WebViewClient.errorFileNotFound);
+      expect(callbackError.description, 'Page not found.');
+      expect(callbackError.errorType, WebResourceErrorType.fileNotFound);
+    });
+
+    test(
+        'onNavigationRequest from requestLoading should not be called when loadUrlCallback is not specified',
+        () {
+      final AndroidNavigationDelegate webKitDelegate =
+          AndroidNavigationDelegate(_buildCreationParams());
+
+      NavigationRequest? callbackNavigationRequest;
+      webKitDelegate
+          .setOnNavigationRequest((NavigationRequest navigationRequest) {
+        callbackNavigationRequest = navigationRequest;
+        return NavigationDecision.prevent;
+      });
+
+      CapturingWebViewClient.lastCreatedDelegate.requestLoading!(
+        android_webview.WebView.detached(),
+        android_webview.WebResourceRequest(
+          url: 'https://www.google.com',
+          isForMainFrame: true,
+          isRedirect: true,
+          hasGesture: true,
+          method: 'GET',
+          requestHeaders: <String, String>{'X-Mock': 'mocking'},
+        ),
+      );
+
+      expect(callbackNavigationRequest, isNull);
+    });
+
+    test(
+        'onLoadUrl from requestLoading should not be called when navigationRequestCallback is not specified',
+        () {
+      final Completer<void> completer = Completer<void>();
+      AndroidNavigationDelegate(_buildCreationParams(loadUrlCallback: (
+        String url,
+        Map<String, String>? headers,
+      ) {
+        completer.complete();
+        return completer.future;
+      }));
+
+      CapturingWebViewClient.lastCreatedDelegate.requestLoading!(
+        android_webview.WebView.detached(),
+        android_webview.WebResourceRequest(
+          url: 'https://www.google.com',
+          isForMainFrame: true,
+          isRedirect: true,
+          hasGesture: true,
+          method: 'GET',
+          requestHeaders: <String, String>{'X-Mock': 'mocking'},
+        ),
+      );
+
+      expect(completer.isCompleted, false);
+    });
+
+    test(
+        'onLoadUrl from requestLoading should not be called when onNavigationRequestCallback returns NavigationDecision.prevent',
+        () {
+      final Completer<void> completer = Completer<void>();
+      final AndroidNavigationDelegate webKitDelegate =
+          AndroidNavigationDelegate(_buildCreationParams(loadUrlCallback: (
+        String url,
+        Map<String, String>? headers,
+      ) {
+        completer.complete();
+        return completer.future;
+      }));
+
+      late final NavigationRequest callbackNavigationRequest;
+      webKitDelegate
+          .setOnNavigationRequest((NavigationRequest navigationRequest) {
+        callbackNavigationRequest = navigationRequest;
+        return NavigationDecision.prevent;
+      });
+
+      CapturingWebViewClient.lastCreatedDelegate.requestLoading!(
+        android_webview.WebView.detached(),
+        android_webview.WebResourceRequest(
+          url: 'https://www.google.com',
+          isForMainFrame: true,
+          isRedirect: true,
+          hasGesture: true,
+          method: 'GET',
+          requestHeaders: <String, String>{'X-Mock': 'mocking'},
+        ),
+      );
+
+      expect(callbackNavigationRequest.isMainFrame, true);
+      expect(callbackNavigationRequest.url, 'https://www.google.com');
+      expect(completer.isCompleted, false);
+    });
+
+    test(
+        'onLoadUrl from requestLoading should complete when onNavigationRequestCallback returns NavigationDecision.navigate',
+        () {
+      final Completer<void> completer = Completer<void>();
+      late final String loadUrlCallbackUrl;
+      late final Map<String, String>? loadUrlCallbackHeaders;
+      final AndroidNavigationDelegate webKitDelegate =
+          AndroidNavigationDelegate(_buildCreationParams(loadUrlCallback: (
+        String url,
+        Map<String, String>? headers,
+      ) {
+        loadUrlCallbackUrl = url;
+        loadUrlCallbackHeaders = headers;
+        completer.complete();
+        return completer.future;
+      }));
+
+      late final NavigationRequest callbackNavigationRequest;
+      webKitDelegate
+          .setOnNavigationRequest((NavigationRequest navigationRequest) {
+        callbackNavigationRequest = navigationRequest;
+        return NavigationDecision.navigate;
+      });
+
+      CapturingWebViewClient.lastCreatedDelegate.requestLoading!(
+        android_webview.WebView.detached(),
+        android_webview.WebResourceRequest(
+          url: 'https://www.google.com',
+          isForMainFrame: true,
+          isRedirect: true,
+          hasGesture: true,
+          method: 'GET',
+          requestHeaders: <String, String>{'X-Mock': 'mocking'},
+        ),
+      );
+
+      expect(loadUrlCallbackUrl, 'https://www.google.com');
+      expect(loadUrlCallbackHeaders, <String, String>{'X-Mock': 'mocking'});
+      expect(callbackNavigationRequest.isMainFrame, true);
+      expect(callbackNavigationRequest.url, 'https://www.google.com');
+      expect(completer.isCompleted, true);
+    });
+
+    test(
+        'onNavigationRequest from urlLoading should not be called when loadUrlCallback is not specified',
+        () {
+      final AndroidNavigationDelegate webKitDelegate =
+          AndroidNavigationDelegate(_buildCreationParams());
+
+      NavigationRequest? callbackNavigationRequest;
+      webKitDelegate
+          .setOnNavigationRequest((NavigationRequest navigationRequest) {
+        callbackNavigationRequest = navigationRequest;
+        return NavigationDecision.prevent;
+      });
+
+      CapturingWebViewClient.lastCreatedDelegate.urlLoading!(
+        android_webview.WebView.detached(),
+        'https://www.google.com',
+      );
+
+      expect(callbackNavigationRequest, isNull);
+    });
+
+    test(
+        'onLoadUrl from urlLoading should not be called when navigationRequestCallback is not specified',
+        () {
+      final Completer<void> completer = Completer<void>();
+      AndroidNavigationDelegate(_buildCreationParams(loadUrlCallback: (
+        String url,
+        Map<String, String>? headers,
+      ) {
+        completer.complete();
+        return completer.future;
+      }));
+
+      CapturingWebViewClient.lastCreatedDelegate.urlLoading!(
+        android_webview.WebView.detached(),
+        'https://www.google.com',
+      );
+
+      expect(completer.isCompleted, false);
+    });
+
+    test(
+        'onLoadUrl from urlLoading should not be called when onNavigationRequestCallback returns NavigationDecision.prevent',
+        () {
+      final Completer<void> completer = Completer<void>();
+      final AndroidNavigationDelegate webKitDelegate =
+          AndroidNavigationDelegate(_buildCreationParams(loadUrlCallback: (
+        String url,
+        Map<String, String>? headers,
+      ) {
+        completer.complete();
+        return completer.future;
+      }));
+
+      late final NavigationRequest callbackNavigationRequest;
+      webKitDelegate
+          .setOnNavigationRequest((NavigationRequest navigationRequest) {
+        callbackNavigationRequest = navigationRequest;
+        return NavigationDecision.prevent;
+      });
+
+      CapturingWebViewClient.lastCreatedDelegate.urlLoading!(
+        android_webview.WebView.detached(),
+        'https://www.google.com',
+      );
+
+      expect(callbackNavigationRequest.isMainFrame, true);
+      expect(callbackNavigationRequest.url, 'https://www.google.com');
+      expect(completer.isCompleted, false);
+    });
+
+    test(
+        'onLoadUrl from urlLoading should complete when onNavigationRequestCallback returns NavigationDecision.navigate',
+        () {
+      final Completer<void> completer = Completer<void>();
+      late final String loadUrlCallbackUrl;
+      late final Map<String, String>? loadUrlCallbackHeaders;
+      final AndroidNavigationDelegate webKitDelegate =
+          AndroidNavigationDelegate(_buildCreationParams(loadUrlCallback: (
+        String url,
+        Map<String, String>? headers,
+      ) {
+        loadUrlCallbackUrl = url;
+        loadUrlCallbackHeaders = headers;
+        completer.complete();
+        return completer.future;
+      }));
+
+      late final NavigationRequest callbackNavigationRequest;
+      webKitDelegate
+          .setOnNavigationRequest((NavigationRequest navigationRequest) {
+        callbackNavigationRequest = navigationRequest;
+        return NavigationDecision.navigate;
+      });
+
+      CapturingWebViewClient.lastCreatedDelegate.urlLoading!(
+        android_webview.WebView.detached(),
+        'https://www.google.com',
+      );
+
+      expect(loadUrlCallbackUrl, 'https://www.google.com');
+      expect(loadUrlCallbackHeaders, <String, String>{});
+      expect(callbackNavigationRequest.isMainFrame, true);
+      expect(callbackNavigationRequest.url, 'https://www.google.com');
+      expect(completer.isCompleted, true);
+    });
+
+    test('setOnProgress', () {
+      final AndroidNavigationDelegate webKitDelegate =
+          AndroidNavigationDelegate(_buildCreationParams());
+
+      late final int callbackProgress;
+      webKitDelegate
+          .setOnProgress((int progress) => callbackProgress = progress);
+
+      CapturingWebChromeClient.lastCreatedDelegate.onProgressChanged!(
+        android_webview.WebView.detached(),
+        42,
+      );
+
+      expect(callbackProgress, 42);
+    });
+  });
+}
+
+AndroidNavigationDelegateCreationParams _buildCreationParams({
+  LoadUrlCallback? loadUrlCallback,
+}) {
+  return AndroidNavigationDelegateCreationParams
+      .fromPlatformNavigationDelegateCreationParams(
+    const PlatformNavigationDelegateCreationParams(),
+    loadUrl: loadUrlCallback,
+    androidWebViewProxy: const AndroidWebViewProxy(
+      createAndroidWebChromeClient: CapturingWebChromeClient.new,
+      createAndroidWebViewClient: CapturingWebViewClient.new,
+    ),
+  );
+}
+
+// Records the last created instance of itself.
+class CapturingWebViewClient extends android_webview.WebViewClient {
+  CapturingWebViewClient({
+    super.onPageFinished,
+    super.onPageStarted,
+    super.onReceivedError,
+    super.onReceivedRequestError,
+    super.requestLoading,
+    super.shouldOverrideUrlLoading,
+    super.urlLoading,
+  }) : super.detached() {
+    lastCreatedDelegate = this;
+  }
+  static CapturingWebViewClient lastCreatedDelegate = CapturingWebViewClient();
+}
+
+// Records the last created instance of itself.
+class CapturingWebChromeClient extends android_webview.WebChromeClient {
+  CapturingWebChromeClient({
+    super.onProgressChanged,
+  }) : super.detached() {
+    lastCreatedDelegate = this;
+  }
+  static CapturingWebChromeClient lastCreatedDelegate =
+      CapturingWebChromeClient();
+}