[webview_flutter_android] Fix bugs in Java Implementation of the Pigeon API (#4459)

diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerFlutterApiImpl.java
new file mode 100644
index 0000000..2dd98c4
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerFlutterApiImpl.java
@@ -0,0 +1,64 @@
+// 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.
+
+package io.flutter.plugins.webviewflutter;
+
+import android.webkit.DownloadListener;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerFlutterApi;
+
+/**
+ * Flutter Api implementation for {@link DownloadListener}.
+ *
+ * <p>Passes arguments of callbacks methods from a {@link DownloadListener} to Dart.
+ */
+public class DownloadListenerFlutterApiImpl extends DownloadListenerFlutterApi {
+  private final InstanceManager instanceManager;
+
+  /**
+   * Creates a Flutter api that sends messages to Dart.
+   *
+   * @param binaryMessenger handles sending messages to Dart
+   * @param instanceManager maintains instances stored to communicate with Dart objects
+   */
+  public DownloadListenerFlutterApiImpl(
+      BinaryMessenger binaryMessenger, InstanceManager instanceManager) {
+    super(binaryMessenger);
+    this.instanceManager = instanceManager;
+  }
+
+  /** Passes arguments from {@link DownloadListener#onDownloadStart} to Dart. */
+  public void onDownloadStart(
+      DownloadListener downloadListener,
+      String url,
+      String userAgent,
+      String contentDisposition,
+      String mimetype,
+      long contentLength,
+      Reply<Void> callback) {
+    onDownloadStart(
+        instanceManager.getInstanceId(downloadListener),
+        url,
+        userAgent,
+        contentDisposition,
+        mimetype,
+        contentLength,
+        callback);
+  }
+
+  /**
+   * Communicates to Dart that the reference to a {@link DownloadListener} was removed.
+   *
+   * @param downloadListener the instance whose reference will be removed
+   * @param callback reply callback with return value from Dart
+   */
+  public void dispose(DownloadListener downloadListener, Reply<Void> callback) {
+    final Long instanceId = instanceManager.removeInstance(downloadListener);
+    if (instanceId != null) {
+      dispose(instanceId, callback);
+    } else {
+      callback.reply(null);
+    }
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerHostApiImpl.java
index 202be87..9694f39 100644
--- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerHostApiImpl.java
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerHostApiImpl.java
@@ -5,40 +5,92 @@
 package io.flutter.plugins.webviewflutter;
 
 import android.webkit.DownloadListener;
-import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerFlutterApi;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerHostApi;
 
-class DownloadListenerHostApiImpl implements GeneratedAndroidWebView.DownloadListenerHostApi {
+/**
+ * Host api implementation for {@link DownloadListener}.
+ *
+ * <p>Handles creating {@link DownloadListener}s that intercommunicate with a paired Dart object.
+ */
+public class DownloadListenerHostApiImpl implements DownloadListenerHostApi {
   private final InstanceManager instanceManager;
   private final DownloadListenerCreator downloadListenerCreator;
-  private final GeneratedAndroidWebView.DownloadListenerFlutterApi downloadListenerFlutterApi;
+  private final DownloadListenerFlutterApiImpl flutterApi;
 
-  static class DownloadListenerCreator {
-    DownloadListener createDownloadListener(
-        Long instanceId, DownloadListenerFlutterApi downloadListenerFlutterApi) {
-      return (url, userAgent, contentDisposition, mimetype, contentLength) ->
-          downloadListenerFlutterApi.onDownloadStart(
-              instanceId, url, userAgent, contentDisposition, mimetype, contentLength, reply -> {});
+  /**
+   * Implementation of {@link DownloadListener} that passes arguments of callback methods to Dart.
+   *
+   * <p>No messages are sent to Dart after {@link DownloadListenerImpl#release} is called.
+   */
+  public static class DownloadListenerImpl implements DownloadListener, Releasable {
+    @Nullable private DownloadListenerFlutterApiImpl flutterApi;
+
+    /**
+     * Creates a {@link DownloadListenerImpl} that passes arguments of callbacks methods to Dart.
+     *
+     * @param flutterApi handles sending messages to Dart
+     */
+    public DownloadListenerImpl(@NonNull DownloadListenerFlutterApiImpl flutterApi) {
+      this.flutterApi = flutterApi;
+    }
+
+    @Override
+    public void onDownloadStart(
+        String url,
+        String userAgent,
+        String contentDisposition,
+        String mimetype,
+        long contentLength) {
+      if (flutterApi != null) {
+        flutterApi.onDownloadStart(
+            this, url, userAgent, contentDisposition, mimetype, contentLength, reply -> {});
+      }
+    }
+
+    @Override
+    public void release() {
+      if (flutterApi != null) {
+        flutterApi.dispose(this, reply -> {});
+      }
+      flutterApi = null;
     }
   }
 
-  DownloadListenerHostApiImpl(
+  /** Handles creating {@link DownloadListenerImpl}s for a {@link DownloadListenerHostApiImpl}. */
+  public static class DownloadListenerCreator {
+    /**
+     * Creates a {@link DownloadListenerImpl}.
+     *
+     * @param flutterApi handles sending messages to Dart
+     * @return the created {@link DownloadListenerImpl}
+     */
+    public DownloadListenerImpl createDownloadListener(DownloadListenerFlutterApiImpl flutterApi) {
+      return new DownloadListenerImpl(flutterApi);
+    }
+  }
+
+  /**
+   * Creates a host API that handles creating {@link DownloadListener}s.
+   *
+   * @param instanceManager maintains instances stored to communicate with Dart objects
+   * @param downloadListenerCreator handles creating {@link DownloadListenerImpl}s
+   * @param flutterApi handles sending messages to Dart
+   */
+  public DownloadListenerHostApiImpl(
       InstanceManager instanceManager,
       DownloadListenerCreator downloadListenerCreator,
-      DownloadListenerFlutterApi downloadListenerFlutterApi) {
+      DownloadListenerFlutterApiImpl flutterApi) {
     this.instanceManager = instanceManager;
     this.downloadListenerCreator = downloadListenerCreator;
-    this.downloadListenerFlutterApi = downloadListenerFlutterApi;
+    this.flutterApi = flutterApi;
   }
 
   @Override
   public void create(Long instanceId) {
     final DownloadListener downloadListener =
-        downloadListenerCreator.createDownloadListener(instanceId, downloadListenerFlutterApi);
+        downloadListenerCreator.createDownloadListener(flutterApi);
     instanceManager.addInstance(downloadListener, instanceId);
   }
-
-  @Override
-  public void dispose(Long instanceId) {
-    instanceManager.removeInstance(instanceId);
-  }
 }
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java
index ba2b9b1..a342748 100644
--- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// Autogenerated from Pigeon (v1.0.7), do not edit directly.
+// Autogenerated from Pigeon (v1.0.9), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 
 package io.flutter.plugins.webviewflutter;
@@ -1313,8 +1313,6 @@
   public interface JavaScriptChannelHostApi {
     void create(Long instanceId, String channelName);
 
-    void dispose(Long instanceId);
-
     /** The codec used by JavaScriptChannelHostApi. */
     static MessageCodec<Object> getCodec() {
       return JavaScriptChannelHostApiCodec.INSTANCE;
@@ -1354,31 +1352,6 @@
           channel.setMessageHandler(null);
         }
       }
-      {
-        BasicMessageChannel<Object> channel =
-            new BasicMessageChannel<>(
-                binaryMessenger, "dev.flutter.pigeon.JavaScriptChannelHostApi.dispose", getCodec());
-        if (api != null) {
-          channel.setMessageHandler(
-              (message, reply) -> {
-                Map<String, Object> wrapped = new HashMap<>();
-                try {
-                  ArrayList<Object> args = (ArrayList<Object>) message;
-                  Number instanceIdArg = (Number) args.get(0);
-                  if (instanceIdArg == null) {
-                    throw new NullPointerException("instanceIdArg unexpectedly null.");
-                  }
-                  api.dispose(instanceIdArg.longValue());
-                  wrapped.put("result", null);
-                } catch (Error | RuntimeException exception) {
-                  wrapped.put("error", wrapError(exception));
-                }
-                reply.reply(wrapped);
-              });
-        } else {
-          channel.setMessageHandler(null);
-        }
-      }
     }
   }
 
@@ -1405,6 +1378,19 @@
       return JavaScriptChannelFlutterApiCodec.INSTANCE;
     }
 
+    public void dispose(Long instanceIdArg, Reply<Void> callback) {
+      BasicMessageChannel<Object> channel =
+          new BasicMessageChannel<>(
+              binaryMessenger,
+              "dev.flutter.pigeon.JavaScriptChannelFlutterApi.dispose",
+              getCodec());
+      channel.send(
+          new ArrayList<Object>(Arrays.asList(instanceIdArg)),
+          channelReply -> {
+            callback.reply(null);
+          });
+    }
+
     public void postMessage(Long instanceIdArg, String messageArg, Reply<Void> callback) {
       BasicMessageChannel<Object> channel =
           new BasicMessageChannel<>(
@@ -1429,8 +1415,6 @@
   public interface WebViewClientHostApi {
     void create(Long instanceId, Boolean shouldOverrideUrlLoading);
 
-    void dispose(Long instanceId);
-
     /** The codec used by WebViewClientHostApi. */
     static MessageCodec<Object> getCodec() {
       return WebViewClientHostApiCodec.INSTANCE;
@@ -1471,31 +1455,6 @@
           channel.setMessageHandler(null);
         }
       }
-      {
-        BasicMessageChannel<Object> channel =
-            new BasicMessageChannel<>(
-                binaryMessenger, "dev.flutter.pigeon.WebViewClientHostApi.dispose", getCodec());
-        if (api != null) {
-          channel.setMessageHandler(
-              (message, reply) -> {
-                Map<String, Object> wrapped = new HashMap<>();
-                try {
-                  ArrayList<Object> args = (ArrayList<Object>) message;
-                  Number instanceIdArg = (Number) args.get(0);
-                  if (instanceIdArg == null) {
-                    throw new NullPointerException("instanceIdArg unexpectedly null.");
-                  }
-                  api.dispose(instanceIdArg.longValue());
-                  wrapped.put("result", null);
-                } catch (Error | RuntimeException exception) {
-                  wrapped.put("error", wrapError(exception));
-                }
-                reply.reply(wrapped);
-              });
-        } else {
-          channel.setMessageHandler(null);
-        }
-      }
     }
   }
 
@@ -1513,9 +1472,6 @@
         case (byte) 129:
           return WebResourceRequestData.fromMap((Map<String, Object>) readValue(buffer));
 
-        case (byte) 130:
-          return WebResourceRequestData.fromMap((Map<String, Object>) readValue(buffer));
-
         default:
           return super.readValueOfType(type, buffer);
       }
@@ -1529,9 +1485,6 @@
       } else if (value instanceof WebResourceRequestData) {
         stream.write(129);
         writeValue(stream, ((WebResourceRequestData) value).toMap());
-      } else if (value instanceof WebResourceRequestData) {
-        stream.write(130);
-        writeValue(stream, ((WebResourceRequestData) value).toMap());
       } else {
         super.writeValue(stream, value);
       }
@@ -1554,6 +1507,17 @@
       return WebViewClientFlutterApiCodec.INSTANCE;
     }
 
+    public void dispose(Long instanceIdArg, Reply<Void> callback) {
+      BasicMessageChannel<Object> channel =
+          new BasicMessageChannel<>(
+              binaryMessenger, "dev.flutter.pigeon.WebViewClientFlutterApi.dispose", getCodec());
+      channel.send(
+          new ArrayList<Object>(Arrays.asList(instanceIdArg)),
+          channelReply -> {
+            callback.reply(null);
+          });
+    }
+
     public void onPageStarted(
         Long instanceIdArg, Long webViewInstanceIdArg, String urlArg, Reply<Void> callback) {
       BasicMessageChannel<Object> channel =
@@ -1666,8 +1630,6 @@
   public interface DownloadListenerHostApi {
     void create(Long instanceId);
 
-    void dispose(Long instanceId);
-
     /** The codec used by DownloadListenerHostApi. */
     static MessageCodec<Object> getCodec() {
       return DownloadListenerHostApiCodec.INSTANCE;
@@ -1703,31 +1665,6 @@
           channel.setMessageHandler(null);
         }
       }
-      {
-        BasicMessageChannel<Object> channel =
-            new BasicMessageChannel<>(
-                binaryMessenger, "dev.flutter.pigeon.DownloadListenerHostApi.dispose", getCodec());
-        if (api != null) {
-          channel.setMessageHandler(
-              (message, reply) -> {
-                Map<String, Object> wrapped = new HashMap<>();
-                try {
-                  ArrayList<Object> args = (ArrayList<Object>) message;
-                  Number instanceIdArg = (Number) args.get(0);
-                  if (instanceIdArg == null) {
-                    throw new NullPointerException("instanceIdArg unexpectedly null.");
-                  }
-                  api.dispose(instanceIdArg.longValue());
-                  wrapped.put("result", null);
-                } catch (Error | RuntimeException exception) {
-                  wrapped.put("error", wrapError(exception));
-                }
-                reply.reply(wrapped);
-              });
-        } else {
-          channel.setMessageHandler(null);
-        }
-      }
     }
   }
 
@@ -1754,6 +1691,17 @@
       return DownloadListenerFlutterApiCodec.INSTANCE;
     }
 
+    public void dispose(Long instanceIdArg, Reply<Void> callback) {
+      BasicMessageChannel<Object> channel =
+          new BasicMessageChannel<>(
+              binaryMessenger, "dev.flutter.pigeon.DownloadListenerFlutterApi.dispose", getCodec());
+      channel.send(
+          new ArrayList<Object>(Arrays.asList(instanceIdArg)),
+          channelReply -> {
+            callback.reply(null);
+          });
+    }
+
     public void onDownloadStart(
         Long instanceIdArg,
         String urlArg,
@@ -1792,8 +1740,6 @@
   public interface WebChromeClientHostApi {
     void create(Long instanceId, Long webViewClientInstanceId);
 
-    void dispose(Long instanceId);
-
     /** The codec used by WebChromeClientHostApi. */
     static MessageCodec<Object> getCodec() {
       return WebChromeClientHostApiCodec.INSTANCE;
@@ -1833,31 +1779,6 @@
           channel.setMessageHandler(null);
         }
       }
-      {
-        BasicMessageChannel<Object> channel =
-            new BasicMessageChannel<>(
-                binaryMessenger, "dev.flutter.pigeon.WebChromeClientHostApi.dispose", getCodec());
-        if (api != null) {
-          channel.setMessageHandler(
-              (message, reply) -> {
-                Map<String, Object> wrapped = new HashMap<>();
-                try {
-                  ArrayList<Object> args = (ArrayList<Object>) message;
-                  Number instanceIdArg = (Number) args.get(0);
-                  if (instanceIdArg == null) {
-                    throw new NullPointerException("instanceIdArg unexpectedly null.");
-                  }
-                  api.dispose(instanceIdArg.longValue());
-                  wrapped.put("result", null);
-                } catch (Error | RuntimeException exception) {
-                  wrapped.put("error", wrapError(exception));
-                }
-                reply.reply(wrapped);
-              });
-        } else {
-          channel.setMessageHandler(null);
-        }
-      }
     }
   }
 
@@ -1884,6 +1805,17 @@
       return WebChromeClientFlutterApiCodec.INSTANCE;
     }
 
+    public void dispose(Long instanceIdArg, Reply<Void> callback) {
+      BasicMessageChannel<Object> channel =
+          new BasicMessageChannel<>(
+              binaryMessenger, "dev.flutter.pigeon.WebChromeClientFlutterApi.dispose", getCodec());
+      channel.send(
+          new ArrayList<Object>(Arrays.asList(instanceIdArg)),
+          channelReply -> {
+            callback.reply(null);
+          });
+    }
+
     public void onProgressChanged(
         Long instanceIdArg, Long webViewInstanceIdArg, Long progressArg, Reply<Void> callback) {
       BasicMessageChannel<Object> channel =
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java
index bfa7d6f..a368baf 100644
--- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java
@@ -8,32 +8,77 @@
 import java.util.HashMap;
 import java.util.Map;
 
-class InstanceManager {
+/**
+ * Maintains instances to intercommunicate with Dart objects.
+ *
+ * <p>When an instance is added with an instanceId, either can be used to retrieve the other.
+ */
+public class InstanceManager {
   private final LongSparseArray<Object> instanceIdsToInstances = new LongSparseArray<>();
   private final Map<Object, Long> instancesToInstanceIds = new HashMap<>();
 
-  /** Add a new instance with instanceId. */
-  void addInstance(Object instance, long instanceId) {
+  /**
+   * Add a new instance to the manager.
+   *
+   * <p>If an instance or instanceId has already been added, it will be replaced by the new values.
+   *
+   * @param instance the new object to be added
+   * @param instanceId unique id of the added object
+   */
+  public void addInstance(Object instance, long instanceId) {
     instancesToInstanceIds.put(instance, instanceId);
     instanceIdsToInstances.append(instanceId, instance);
   }
 
-  /** Remove the instance from the manager. */
-  void removeInstance(long instanceId) {
+  /**
+   * Remove the instance with instanceId from the manager.
+   *
+   * @param instanceId the id of the instance to be removed
+   * @return the removed instance if the manager contains the instanceId, otherwise null
+   */
+  public Object removeInstanceWithId(long instanceId) {
     final Object instance = instanceIdsToInstances.get(instanceId);
     if (instance != null) {
       instanceIdsToInstances.remove(instanceId);
       instancesToInstanceIds.remove(instance);
     }
+    return instance;
   }
 
-  /** Retrieve the Object paired with instanceId. */
-  Object getInstance(long instanceId) {
+  /**
+   * Remove the instance from the manager.
+   *
+   * @param instance the instance to be removed
+   * @return the instanceId of the removed instance if the manager contains the value, otherwise
+   *     null
+   */
+  public Long removeInstance(Object instance) {
+    final Long instanceId = instancesToInstanceIds.get(instance);
+    if (instanceId != null) {
+      instanceIdsToInstances.remove(instanceId);
+      instancesToInstanceIds.remove(instance);
+    }
+    return instanceId;
+  }
+
+  /**
+   * Retrieve the Object paired with instanceId.
+   *
+   * @param instanceId the instanceId of the desired instance
+   * @return the instance stored with the instanceId if the manager contains the value, otherwise
+   *     null
+   */
+  public Object getInstance(long instanceId) {
     return instanceIdsToInstances.get(instanceId);
   }
 
-  /** Retrieve the instanceId paired with instance. */
-  Long getInstanceId(Object instance) {
+  /**
+   * Retrieve the instanceId paired with an instance.
+   *
+   * @param instance the value paired with the desired instanceId
+   * @return the instanceId paired with instance if the manager contains the value, otherwise null
+   */
+  public Long getInstanceId(Object instance) {
     return instancesToInstanceIds.get(instance);
   }
 }
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java
index 2f987c0..96ae3ce 100644
--- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java
@@ -7,6 +7,8 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.webkit.JavascriptInterface;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import io.flutter.plugin.common.MethodChannel;
 import java.util.HashMap;
 
@@ -14,13 +16,16 @@
  * Added as a JavaScript interface to the WebView for any JavaScript channel that the Dart code sets
  * up.
  *
- * <p>Exposes a single method named `postMessage` to JavaScript, which sends a message over a method
- * channel to the Dart code.
+ * <p>Exposes a single method named `postMessage` to JavaScript, which sends a message to the Dart
+ * code.
+ *
+ * <p>No messages are sent to Dart after {@link JavaScriptChannel#release} is called.
  */
-class JavaScriptChannel {
+public class JavaScriptChannel implements Releasable {
   private final MethodChannel methodChannel;
-  final String javaScriptChannelName;
   private final Handler platformThreadHandler;
+  final String javaScriptChannelName;
+  @Nullable private JavaScriptChannelFlutterApiImpl flutterApi;
 
   /**
    * @param methodChannel the Flutter WebView method channel to which JS messages are sent
@@ -35,24 +40,58 @@
     this.platformThreadHandler = platformThreadHandler;
   }
 
+  /**
+   * Creates a {@link JavaScriptChannel} that passes arguments of callback methods to Dart.
+   *
+   * @param flutterApi the Flutter Api to which JS messages are sent
+   * @param channelName JavaScript channel the message was sent through
+   * @param platformThreadHandler handles making callbacks on the desired thread
+   */
+  public JavaScriptChannel(
+      @NonNull JavaScriptChannelFlutterApiImpl flutterApi,
+      String channelName,
+      Handler platformThreadHandler) {
+    this.flutterApi = flutterApi;
+    this.javaScriptChannelName = channelName;
+    this.platformThreadHandler = platformThreadHandler;
+    methodChannel = null;
+  }
+
   // Suppressing unused warning as this is invoked from JavaScript.
   @SuppressWarnings("unused")
   @JavascriptInterface
   public void postMessage(final String message) {
-    Runnable postMessageRunnable =
-        new Runnable() {
-          @Override
-          public void run() {
-            HashMap<String, String> arguments = new HashMap<>();
-            arguments.put("channel", javaScriptChannelName);
-            arguments.put("message", message);
-            methodChannel.invokeMethod("javascriptChannelMessage", arguments);
-          }
-        };
-    if (platformThreadHandler.getLooper() == Looper.myLooper()) {
-      postMessageRunnable.run();
-    } else {
-      platformThreadHandler.post(postMessageRunnable);
+    Runnable postMessageRunnable = null;
+
+    if (flutterApi != null) {
+      postMessageRunnable = () -> flutterApi.postMessage(this, message, reply -> {});
+    } else if (methodChannel != null) {
+      postMessageRunnable =
+          new Runnable() {
+            @Override
+            public void run() {
+              HashMap<String, String> arguments = new HashMap<>();
+              arguments.put("channel", javaScriptChannelName);
+              arguments.put("message", message);
+              methodChannel.invokeMethod("javascriptChannelMessage", arguments);
+            }
+          };
     }
+
+    if (postMessageRunnable != null) {
+      if (platformThreadHandler.getLooper() == Looper.myLooper()) {
+        postMessageRunnable.run();
+      } else {
+        platformThreadHandler.post(postMessageRunnable);
+      }
+    }
+  }
+
+  @Override
+  public void release() {
+    if (flutterApi != null) {
+      flutterApi.dispose(this, reply -> {});
+    }
+    flutterApi = null;
   }
 }
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelFlutterApiImpl.java
new file mode 100644
index 0000000..120f66d
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelFlutterApiImpl.java
@@ -0,0 +1,50 @@
+// 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.
+
+package io.flutter.plugins.webviewflutter;
+
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelFlutterApi;
+
+/**
+ * Flutter Api implementation for {@link JavaScriptChannel}.
+ *
+ * <p>Passes arguments of callbacks methods from a {@link JavaScriptChannel} to Dart.
+ */
+public class JavaScriptChannelFlutterApiImpl extends JavaScriptChannelFlutterApi {
+  private final InstanceManager instanceManager;
+
+  /**
+   * Creates a Flutter api that sends messages to Dart.
+   *
+   * @param binaryMessenger Handles sending messages to Dart.
+   * @param instanceManager Maintains instances stored to communicate with Dart objects.
+   */
+  public JavaScriptChannelFlutterApiImpl(
+      BinaryMessenger binaryMessenger, InstanceManager instanceManager) {
+    super(binaryMessenger);
+    this.instanceManager = instanceManager;
+  }
+
+  /** Passes arguments from {@link JavaScriptChannel#postMessage} to Dart. */
+  public void postMessage(
+      JavaScriptChannel javaScriptChannel, String messageArg, Reply<Void> callback) {
+    super.postMessage(instanceManager.getInstanceId(javaScriptChannel), messageArg, callback);
+  }
+
+  /**
+   * Communicates to Dart that the reference to a {@link JavaScriptChannel} was removed.
+   *
+   * @param javaScriptChannel The instance whose reference will be removed.
+   * @param callback Reply callback with return value from Dart.
+   */
+  public void dispose(JavaScriptChannel javaScriptChannel, Reply<Void> callback) {
+    final Long instanceId = instanceManager.removeInstance(javaScriptChannel);
+    if (instanceId != null) {
+      dispose(instanceId, callback);
+    } else {
+      callback.reply(null);
+    }
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelHostApiImpl.java
index 2d42d95..ac67a81 100644
--- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelHostApiImpl.java
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelHostApiImpl.java
@@ -5,44 +5,53 @@
 package io.flutter.plugins.webviewflutter;
 
 import android.os.Handler;
-import android.os.Looper;
-import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelFlutterApi;
+import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelHostApi;
 
-class JavaScriptChannelHostApiImpl implements GeneratedAndroidWebView.JavaScriptChannelHostApi {
+/**
+ * Host api implementation for {@link JavaScriptChannel}.
+ *
+ * <p>Handles creating {@link JavaScriptChannel}s that intercommunicate with a paired Dart object.
+ */
+public class JavaScriptChannelHostApiImpl implements JavaScriptChannelHostApi {
   private final InstanceManager instanceManager;
   private final JavaScriptChannelCreator javaScriptChannelCreator;
-  private final JavaScriptChannelFlutterApi javaScriptChannelFlutterApi;
+  private final JavaScriptChannelFlutterApiImpl flutterApi;
   private final Handler platformThreadHandler;
 
-  static class JavaScriptChannelCreator {
-    JavaScriptChannel createJavaScriptChannel(
-        Long instanceId,
-        JavaScriptChannelFlutterApi javaScriptChannelFlutterApi,
+  /** Handles creating {@link JavaScriptChannel}s for a {@link JavaScriptChannelHostApiImpl}. */
+  public static class JavaScriptChannelCreator {
+    /**
+     * Creates a {@link JavaScriptChannel}.
+     *
+     * @param flutterApi handles sending messages to Dart
+     * @param channelName JavaScript channel the message should be sent through
+     * @param platformThreadHandler handles making callbacks on the desired thread
+     * @return the created {@link JavaScriptChannel}
+     */
+    public JavaScriptChannel createJavaScriptChannel(
+        JavaScriptChannelFlutterApiImpl flutterApi,
         String channelName,
         Handler platformThreadHandler) {
-      return new JavaScriptChannel(null, channelName, platformThreadHandler) {
-        @Override
-        public void postMessage(String message) {
-          final Runnable postMessageRunnable =
-              () -> javaScriptChannelFlutterApi.postMessage(instanceId, message, reply -> {});
-          if (platformThreadHandler.getLooper() == Looper.myLooper()) {
-            postMessageRunnable.run();
-          } else {
-            platformThreadHandler.post(postMessageRunnable);
-          }
-        }
-      };
+      return new JavaScriptChannel(flutterApi, channelName, platformThreadHandler);
     }
   }
 
-  JavaScriptChannelHostApiImpl(
+  /**
+   * Creates a host API that handles creating {@link JavaScriptChannel}s.
+   *
+   * @param instanceManager maintains instances stored to communicate with Dart objects
+   * @param javaScriptChannelCreator handles creating {@link JavaScriptChannel}s
+   * @param flutterApi handles sending messages to Dart
+   * @param platformThreadHandler handles making callbacks on the desired thread
+   */
+  public JavaScriptChannelHostApiImpl(
       InstanceManager instanceManager,
       JavaScriptChannelCreator javaScriptChannelCreator,
-      JavaScriptChannelFlutterApi javaScriptChannelFlutterApi,
+      JavaScriptChannelFlutterApiImpl flutterApi,
       Handler platformThreadHandler) {
     this.instanceManager = instanceManager;
     this.javaScriptChannelCreator = javaScriptChannelCreator;
-    this.javaScriptChannelFlutterApi = javaScriptChannelFlutterApi;
+    this.flutterApi = flutterApi;
     this.platformThreadHandler = platformThreadHandler;
   }
 
@@ -50,12 +59,7 @@
   public void create(Long instanceId, String channelName) {
     final JavaScriptChannel javaScriptChannel =
         javaScriptChannelCreator.createJavaScriptChannel(
-            instanceId, javaScriptChannelFlutterApi, channelName, platformThreadHandler);
+            flutterApi, channelName, platformThreadHandler);
     instanceManager.addInstance(javaScriptChannel, instanceId);
   }
-
-  @Override
-  public void dispose(Long instanceId) {
-    instanceManager.removeInstance(instanceId);
-  }
 }
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/Releasable.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/Releasable.java
new file mode 100644
index 0000000..9c4ed76
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/Releasable.java
@@ -0,0 +1,14 @@
+// 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.
+
+package io.flutter.plugins.webviewflutter;
+
+/**
+ * Represents a resource, or a holder of resources, which may be released once they are no longer
+ * needed.
+ */
+interface Releasable {
+  /** Notify that that the reference to an object will be removed by a holder. */
+  void release();
+}
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java
new file mode 100644
index 0000000..2ab9275
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java
@@ -0,0 +1,56 @@
+// 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.
+
+package io.flutter.plugins.webviewflutter;
+
+import android.webkit.WebChromeClient;
+import android.webkit.WebView;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientFlutterApi;
+
+/**
+ * Flutter Api implementation for {@link WebChromeClient}.
+ *
+ * <p>Passes arguments of callbacks methods from a {@link WebChromeClient} to Dart.
+ */
+public class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi {
+  private final InstanceManager instanceManager;
+
+  /**
+   * Creates a Flutter api that sends messages to Dart.
+   *
+   * @param binaryMessenger handles sending messages to Dart
+   * @param instanceManager maintains instances stored to communicate with Dart objects
+   */
+  public WebChromeClientFlutterApiImpl(
+      BinaryMessenger binaryMessenger, InstanceManager instanceManager) {
+    super(binaryMessenger);
+    this.instanceManager = instanceManager;
+  }
+
+  /** Passes arguments from {@link WebChromeClient#onProgressChanged} to Dart. */
+  public void onProgressChanged(
+      WebChromeClient webChromeClient, WebView webView, Long progress, Reply<Void> callback) {
+    super.onProgressChanged(
+        instanceManager.getInstanceId(webChromeClient),
+        instanceManager.getInstanceId(webView),
+        progress,
+        callback);
+  }
+
+  /**
+   * Communicates to Dart that the reference to a {@link WebChromeClient}} was removed.
+   *
+   * @param webChromeClient the instance whose reference will be removed
+   * @param callback reply callback with return value from Dart
+   */
+  public void dispose(WebChromeClient webChromeClient, Reply<Void> callback) {
+    final Long instanceId = instanceManager.removeInstance(webChromeClient);
+    if (instanceId != null) {
+      dispose(instanceId, callback);
+    } else {
+      callback.reply(null);
+    }
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java
index 32f8fcb..f8bc512 100644
--- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java
@@ -11,68 +11,125 @@
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
-import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientFlutterApi;
+import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientHostApi;
 
-class WebChromeClientHostApiImpl implements GeneratedAndroidWebView.WebChromeClientHostApi {
+/**
+ * Host api implementation for {@link WebChromeClient}.
+ *
+ * <p>Handles creating {@link WebChromeClient}s that intercommunicate with a paired Dart object.
+ */
+public class WebChromeClientHostApiImpl implements WebChromeClientHostApi {
   private final InstanceManager instanceManager;
   private final WebChromeClientCreator webChromeClientCreator;
-  private final WebChromeClientFlutterApi webChromeClientFlutterApi;
+  private final WebChromeClientFlutterApiImpl flutterApi;
 
-  static class WebChromeClientCreator {
-    WebChromeClient createWebChromeClient(
-        Long instanceId,
-        InstanceManager instanceManager,
-        WebViewClient webViewClient,
-        WebChromeClientFlutterApi webChromeClientFlutterApi) {
-      return new WebChromeClient() {
-        // Verifies that a url opened by `Window.open` has a secure url.
-        @Override
-        public boolean onCreateWindow(
-            final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
-          final WebViewClient newWindowWebViewClient =
-              new WebViewClient() {
-                @RequiresApi(api = Build.VERSION_CODES.N)
-                @Override
-                public boolean shouldOverrideUrlLoading(
-                    @NonNull WebView view, @NonNull WebResourceRequest request) {
-                  webViewClient.shouldOverrideUrlLoading(view, request);
-                  return true;
-                }
+  /**
+   * Implementation of {@link WebChromeClient} that passes arguments of callback methods to Dart.
+   */
+  public static class WebChromeClientImpl extends WebChromeClient implements Releasable {
+    @Nullable private WebChromeClientFlutterApiImpl flutterApi;
+    private WebViewClient webViewClient;
 
-                @Override
-                public boolean shouldOverrideUrlLoading(WebView view, String url) {
-                  webViewClient.shouldOverrideUrlLoading(view, url);
-                  return true;
-                }
-              };
+    /**
+     * Creates a {@link WebChromeClient} that passes arguments of callbacks methods to Dart.
+     *
+     * @param flutterApi handles sending messages to Dart
+     * @param webViewClient receives forwarded calls from {@link WebChromeClient#onCreateWindow}
+     */
+    public WebChromeClientImpl(
+        @NonNull WebChromeClientFlutterApiImpl flutterApi, WebViewClient webViewClient) {
+      this.flutterApi = flutterApi;
+      this.webViewClient = webViewClient;
+    }
 
-          final WebView newWebView = new WebView(view.getContext());
-          newWebView.setWebViewClient(newWindowWebViewClient);
+    // Verifies that a url opened by `Window.open` has a secure url.
+    @Override
+    public boolean onCreateWindow(
+        final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
+      final WebViewClient newWindowWebViewClient =
+          new WebViewClient() {
+            @RequiresApi(api = Build.VERSION_CODES.N)
+            @Override
+            public boolean shouldOverrideUrlLoading(
+                @NonNull WebView view, @NonNull WebResourceRequest request) {
+              webViewClient.shouldOverrideUrlLoading(view, request);
+              return true;
+            }
 
-          final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
-          transport.setWebView(newWebView);
-          resultMsg.sendToTarget();
+            @Override
+            public boolean shouldOverrideUrlLoading(WebView view, String url) {
+              webViewClient.shouldOverrideUrlLoading(view, url);
+              return true;
+            }
+          };
 
-          return true;
-        }
+      final WebView newWebView = new WebView(view.getContext());
+      newWebView.setWebViewClient(newWindowWebViewClient);
 
-        @Override
-        public void onProgressChanged(WebView view, int progress) {
-          webChromeClientFlutterApi.onProgressChanged(
-              instanceId, instanceManager.getInstanceId(view), (long) progress, reply -> {});
-        }
-      };
+      final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
+      transport.setWebView(newWebView);
+      resultMsg.sendToTarget();
+
+      return true;
+    }
+
+    @Override
+    public void onProgressChanged(WebView view, int progress) {
+      if (flutterApi != null) {
+        flutterApi.onProgressChanged(this, view, (long) progress, reply -> {});
+      }
+    }
+
+    /**
+     * Set the {@link WebViewClient} that calls to {@link WebChromeClient#onCreateWindow} are passed
+     * to.
+     *
+     * @param webViewClient the forwarding {@link WebViewClient}
+     */
+    public void setWebViewClient(WebViewClient webViewClient) {
+      this.webViewClient = webViewClient;
+    }
+
+    @Override
+    public void release() {
+      if (flutterApi != null) {
+        flutterApi.dispose(this, reply -> {});
+      }
+      flutterApi = null;
     }
   }
 
-  WebChromeClientHostApiImpl(
+  /** Handles creating {@link WebChromeClient}s for a {@link WebChromeClientHostApiImpl}. */
+  public static class WebChromeClientCreator {
+    /**
+     * Creates a {@link DownloadListenerHostApiImpl.DownloadListenerImpl}.
+     *
+     * @param flutterApi handles sending messages to Dart
+     * @param webViewClient receives forwarded calls from {@link WebChromeClient#onCreateWindow}
+     * @return the created {@link DownloadListenerHostApiImpl.DownloadListenerImpl}
+     */
+    public WebChromeClientImpl createWebChromeClient(
+        WebChromeClientFlutterApiImpl flutterApi, WebViewClient webViewClient) {
+      return new WebChromeClientImpl(flutterApi, webViewClient);
+    }
+  }
+
+  /**
+   * Creates a host API that handles creating {@link WebChromeClient}s.
+   *
+   * @param instanceManager maintains instances stored to communicate with Dart objects
+   * @param webChromeClientCreator handles creating {@link WebChromeClient}s
+   * @param flutterApi handles sending messages to Dart
+   */
+  public WebChromeClientHostApiImpl(
       InstanceManager instanceManager,
       WebChromeClientCreator webChromeClientCreator,
-      WebChromeClientFlutterApi webChromeClientFlutterApi) {
+      WebChromeClientFlutterApiImpl flutterApi) {
     this.instanceManager = instanceManager;
     this.webChromeClientCreator = webChromeClientCreator;
-    this.webChromeClientFlutterApi = webChromeClientFlutterApi;
+    this.flutterApi = flutterApi;
   }
 
   @Override
@@ -80,13 +137,7 @@
     final WebViewClient webViewClient =
         (WebViewClient) instanceManager.getInstance(webViewClientInstanceId);
     final WebChromeClient webChromeClient =
-        webChromeClientCreator.createWebChromeClient(
-            instanceId, instanceManager, webViewClient, webChromeClientFlutterApi);
+        webChromeClientCreator.createWebChromeClient(flutterApi, webViewClient);
     instanceManager.addInstance(webChromeClient, instanceId);
   }
-
-  @Override
-  public void dispose(Long instanceId) {
-    instanceManager.removeInstance(instanceId);
-  }
 }
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java
index e70a867..239ef47 100644
--- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java
@@ -6,18 +6,38 @@
 
 import android.webkit.WebSettings;
 import android.webkit.WebView;
+import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebSettingsHostApi;
 
-class WebSettingsHostApiImpl implements GeneratedAndroidWebView.WebSettingsHostApi {
+/**
+ * Host api implementation for {@link WebSettings}.
+ *
+ * <p>Handles creating {@link WebSettings}s that intercommunicate with a paired Dart object.
+ */
+public class WebSettingsHostApiImpl implements WebSettingsHostApi {
   private final InstanceManager instanceManager;
   private final WebSettingsCreator webSettingsCreator;
 
-  static class WebSettingsCreator {
-    WebSettings createWebSettings(WebView webView) {
+  /** Handles creating {@link WebSettings} for a {@link WebSettingsHostApiImpl}. */
+  public static class WebSettingsCreator {
+    /**
+     * Creates a {@link WebSettings}.
+     *
+     * @param webView the {@link WebView} which the settings affect
+     * @return the created {@link WebSettings}
+     */
+    public WebSettings createWebSettings(WebView webView) {
       return webView.getSettings();
     }
   }
 
-  WebSettingsHostApiImpl(InstanceManager instanceManager, WebSettingsCreator webSettingsCreator) {
+  /**
+   * Creates a host API that handles creating {@link WebSettings} and invoke its methods.
+   *
+   * @param instanceManager maintains instances stored to communicate with Dart objects
+   * @param webSettingsCreator handles creating {@link WebSettings}s
+   */
+  public WebSettingsHostApiImpl(
+      InstanceManager instanceManager, WebSettingsCreator webSettingsCreator) {
     this.instanceManager = instanceManager;
     this.webSettingsCreator = webSettingsCreator;
   }
@@ -30,7 +50,7 @@
 
   @Override
   public void dispose(Long instanceId) {
-    instanceManager.removeInstance(instanceId);
+    instanceManager.removeInstanceWithId(instanceId);
   }
 
   @Override
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientFlutterApiImpl.java
new file mode 100644
index 0000000..9e462fa
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientFlutterApiImpl.java
@@ -0,0 +1,198 @@
+// 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.
+
+package io.flutter.plugins.webviewflutter;
+
+import android.annotation.SuppressLint;
+import android.os.Build;
+import android.webkit.WebResourceError;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import androidx.annotation.RequiresApi;
+import androidx.webkit.WebResourceErrorCompat;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebViewClientFlutterApi;
+
+/**
+ * Flutter Api implementation for {@link WebViewClient}.
+ *
+ * <p>Passes arguments of callbacks methods from a {@link WebViewClient} to Dart.
+ */
+public class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi {
+  private final InstanceManager instanceManager;
+
+  @RequiresApi(api = Build.VERSION_CODES.M)
+  static GeneratedAndroidWebView.WebResourceErrorData createWebResourceErrorData(
+      WebResourceError error) {
+    final GeneratedAndroidWebView.WebResourceErrorData errorData =
+        new GeneratedAndroidWebView.WebResourceErrorData();
+    errorData.setErrorCode((long) error.getErrorCode());
+    errorData.setDescription(error.getDescription().toString());
+
+    return errorData;
+  }
+
+  @SuppressLint("RequiresFeature")
+  static GeneratedAndroidWebView.WebResourceErrorData createWebResourceErrorData(
+      WebResourceErrorCompat error) {
+    final GeneratedAndroidWebView.WebResourceErrorData errorData =
+        new GeneratedAndroidWebView.WebResourceErrorData();
+    errorData.setErrorCode((long) error.getErrorCode());
+    errorData.setDescription(error.getDescription().toString());
+
+    return errorData;
+  }
+
+  @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+  static GeneratedAndroidWebView.WebResourceRequestData createWebResourceRequestData(
+      WebResourceRequest request) {
+    final GeneratedAndroidWebView.WebResourceRequestData requestData =
+        new GeneratedAndroidWebView.WebResourceRequestData();
+    requestData.setUrl(request.getUrl().toString());
+    requestData.setIsForMainFrame(request.isForMainFrame());
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+      requestData.setIsRedirect(request.isRedirect());
+    }
+    requestData.setHasGesture(request.hasGesture());
+    requestData.setMethod(request.getMethod());
+    requestData.setRequestHeaders(request.getRequestHeaders());
+
+    return requestData;
+  }
+
+  /**
+   * Creates a Flutter api that sends messages to Dart.
+   *
+   * @param binaryMessenger handles sending messages to Dart
+   * @param instanceManager maintains instances stored to communicate with Dart objects
+   */
+  public WebViewClientFlutterApiImpl(
+      BinaryMessenger binaryMessenger, InstanceManager instanceManager) {
+    super(binaryMessenger);
+    this.instanceManager = instanceManager;
+  }
+
+  /** Passes arguments from {@link WebViewClient#onPageStarted} to Dart. */
+  public void onPageStarted(
+      WebViewClient webViewClient, WebView webView, String urlArg, Reply<Void> callback) {
+    onPageStarted(
+        instanceManager.getInstanceId(webViewClient),
+        instanceManager.getInstanceId(webView),
+        urlArg,
+        callback);
+  }
+
+  /** Passes arguments from {@link WebViewClient#onPageFinished} to Dart. */
+  public void onPageFinished(
+      WebViewClient webViewClient, WebView webView, String urlArg, Reply<Void> callback) {
+    onPageFinished(
+        instanceManager.getInstanceId(webViewClient),
+        instanceManager.getInstanceId(webView),
+        urlArg,
+        callback);
+  }
+
+  /**
+   * Passes arguments from {@link WebViewClient#onReceivedError(WebView, WebResourceRequest,
+   * WebResourceError)} to Dart.
+   */
+  @RequiresApi(api = Build.VERSION_CODES.M)
+  public void onReceivedRequestError(
+      WebViewClient webViewClient,
+      WebView webView,
+      WebResourceRequest request,
+      WebResourceError error,
+      Reply<Void> callback) {
+    onReceivedRequestError(
+        instanceManager.getInstanceId(webViewClient),
+        instanceManager.getInstanceId(webView),
+        createWebResourceRequestData(request),
+        createWebResourceErrorData(error),
+        callback);
+  }
+
+  /**
+   * Passes arguments from {@link androidx.webkit.WebViewClientCompat#onReceivedError(WebView,
+   * WebResourceRequest, WebResourceError)} to Dart.
+   */
+  @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+  public void onReceivedRequestError(
+      WebViewClient webViewClient,
+      WebView webView,
+      WebResourceRequest request,
+      WebResourceErrorCompat error,
+      Reply<Void> callback) {
+    onReceivedRequestError(
+        instanceManager.getInstanceId(webViewClient),
+        instanceManager.getInstanceId(webView),
+        createWebResourceRequestData(request),
+        createWebResourceErrorData(error),
+        callback);
+  }
+
+  /**
+   * Passes arguments from {@link WebViewClient#onReceivedError(WebView, int, String, String)} to
+   * Dart.
+   */
+  public void onReceivedError(
+      WebViewClient webViewClient,
+      WebView webView,
+      Long errorCodeArg,
+      String descriptionArg,
+      String failingUrlArg,
+      Reply<Void> callback) {
+    onReceivedError(
+        instanceManager.getInstanceId(webViewClient),
+        instanceManager.getInstanceId(webView),
+        errorCodeArg,
+        descriptionArg,
+        failingUrlArg,
+        callback);
+  }
+
+  /**
+   * Passes arguments from {@link WebViewClient#shouldOverrideUrlLoading(WebView,
+   * WebResourceRequest)} to Dart.
+   */
+  @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+  public void requestLoading(
+      WebViewClient webViewClient,
+      WebView webView,
+      WebResourceRequest request,
+      Reply<Void> callback) {
+    requestLoading(
+        instanceManager.getInstanceId(webViewClient),
+        instanceManager.getInstanceId(webView),
+        createWebResourceRequestData(request),
+        callback);
+  }
+
+  /**
+   * Passes arguments from {@link WebViewClient#shouldOverrideUrlLoading(WebView, String)} to Dart.
+   */
+  public void urlLoading(
+      WebViewClient webViewClient, WebView webView, String urlArg, Reply<Void> callback) {
+    urlLoading(
+        instanceManager.getInstanceId(webViewClient),
+        instanceManager.getInstanceId(webView),
+        urlArg,
+        callback);
+  }
+
+  /**
+   * Communicates to Dart that the reference to a {@link WebViewClient} was removed.
+   *
+   * @param webViewClient the instance whose reference will be removed
+   * @param callback reply callback with return value from Dart
+   */
+  public void dispose(WebViewClient webViewClient, Reply<Void> callback) {
+    final Long instanceId = instanceManager.removeInstance(webViewClient);
+    if (instanceId != null) {
+      dispose(instanceId, callback);
+    } else {
+      callback.reply(null);
+    }
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java
index 4d17eb1..6b659fa 100644
--- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java
@@ -14,61 +14,200 @@
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.webkit.WebResourceErrorCompat;
 import androidx.webkit.WebViewClientCompat;
-import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebViewClientFlutterApi;
 
-class WebViewClientHostApiImpl implements GeneratedAndroidWebView.WebViewClientHostApi {
+/**
+ * Host api implementation for {@link WebViewClient}.
+ *
+ * <p>Handles creating {@link WebViewClient}s that intercommunicate with a paired Dart object.
+ */
+public class WebViewClientHostApiImpl implements GeneratedAndroidWebView.WebViewClientHostApi {
   private final InstanceManager instanceManager;
   private final WebViewClientCreator webViewClientCreator;
-  private final WebViewClientFlutterApi webViewClientFlutterApi;
+  private final WebViewClientFlutterApiImpl flutterApi;
 
-  @RequiresApi(api = Build.VERSION_CODES.M)
-  static GeneratedAndroidWebView.WebResourceErrorData createWebResourceErrorData(
-      WebResourceError error) {
-    final GeneratedAndroidWebView.WebResourceErrorData errorData =
-        new GeneratedAndroidWebView.WebResourceErrorData();
-    errorData.setErrorCode((long) error.getErrorCode());
-    errorData.setDescription(error.getDescription().toString());
+  /**
+   * An interface implemented by a class that extends {@link WebViewClient} and {@link Releasable}.
+   */
+  public interface ReleasableWebViewClient extends Releasable {}
 
-    return errorData;
-  }
+  /** Implementation of {@link WebViewClient} that passes arguments of callback methods to Dart. */
+  @RequiresApi(Build.VERSION_CODES.N)
+  public static class WebViewClientImpl extends WebViewClient implements ReleasableWebViewClient {
+    @Nullable private WebViewClientFlutterApiImpl flutterApi;
+    private final boolean shouldOverrideUrlLoading;
 
-  @SuppressLint("RequiresFeature")
-  static GeneratedAndroidWebView.WebResourceErrorData createWebResourceErrorData(
-      WebResourceErrorCompat error) {
-    final GeneratedAndroidWebView.WebResourceErrorData errorData =
-        new GeneratedAndroidWebView.WebResourceErrorData();
-    errorData.setErrorCode((long) error.getErrorCode());
-    errorData.setDescription(error.getDescription().toString());
-
-    return errorData;
-  }
-
-  @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
-  static GeneratedAndroidWebView.WebResourceRequestData createWebResourceRequestData(
-      WebResourceRequest request) {
-    final GeneratedAndroidWebView.WebResourceRequestData requestData =
-        new GeneratedAndroidWebView.WebResourceRequestData();
-    requestData.setUrl(request.getUrl().toString());
-    requestData.setIsForMainFrame(request.isForMainFrame());
-    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-      requestData.setIsRedirect(request.isRedirect());
+    /**
+     * Creates a {@link WebViewClient} that passes arguments of callbacks methods to Dart.
+     *
+     * @param flutterApi handles sending messages to Dart
+     * @param shouldOverrideUrlLoading whether loading a url should be overridden
+     */
+    public WebViewClientImpl(
+        @NonNull WebViewClientFlutterApiImpl flutterApi, boolean shouldOverrideUrlLoading) {
+      this.shouldOverrideUrlLoading = shouldOverrideUrlLoading;
+      this.flutterApi = flutterApi;
     }
-    requestData.setHasGesture(request.hasGesture());
-    requestData.setMethod(request.getMethod());
-    requestData.setRequestHeaders(request.getRequestHeaders());
 
-    return requestData;
+    @Override
+    public void onPageStarted(WebView view, String url, Bitmap favicon) {
+      if (flutterApi != null) {
+        flutterApi.onPageStarted(this, view, url, reply -> {});
+      }
+    }
+
+    @Override
+    public void onPageFinished(WebView view, String url) {
+      if (flutterApi != null) {
+        flutterApi.onPageFinished(this, view, url, reply -> {});
+      }
+    }
+
+    @Override
+    public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
+      if (flutterApi != null) {
+        flutterApi.onReceivedRequestError(this, view, request, error, reply -> {});
+      }
+    }
+
+    @Override
+    public void onReceivedError(
+        WebView view, int errorCode, String description, String failingUrl) {
+      if (flutterApi != null) {
+        flutterApi.onReceivedError(
+            this, view, (long) errorCode, description, failingUrl, reply -> {});
+      }
+    }
+
+    @Override
+    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
+      if (flutterApi != null) {
+        flutterApi.requestLoading(this, view, request, reply -> {});
+      }
+      return shouldOverrideUrlLoading;
+    }
+
+    @Override
+    public boolean shouldOverrideUrlLoading(WebView view, String url) {
+      if (flutterApi != null) {
+        flutterApi.urlLoading(this, view, url, reply -> {});
+      }
+      return shouldOverrideUrlLoading;
+    }
+
+    @Override
+    public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
+      // Deliberately empty. Occasionally the webview will mark events as having failed to be
+      // handled even though they were handled. We don't want to propagate those as they're not
+      // truly lost.
+    }
+
+    public void release() {
+      if (flutterApi != null) {
+        flutterApi.dispose(this, reply -> {});
+      }
+      flutterApi = null;
+    }
   }
 
-  static class WebViewClientCreator {
-    WebViewClient createWebViewClient(
-        Long instanceId,
-        InstanceManager instanceManager,
-        Boolean shouldOverrideUrlLoading,
-        WebViewClientFlutterApi webViewClientFlutterApi) {
+  /**
+   * Implementation of {@link WebViewClientCompat} that passes arguments of callback methods to
+   * Dart.
+   */
+  public static class WebViewClientCompatImpl extends WebViewClientCompat
+      implements ReleasableWebViewClient {
+    private @Nullable WebViewClientFlutterApiImpl flutterApi;
+    private final boolean shouldOverrideUrlLoading;
+
+    public WebViewClientCompatImpl(
+        @NonNull WebViewClientFlutterApiImpl flutterApi, boolean shouldOverrideUrlLoading) {
+      this.shouldOverrideUrlLoading = shouldOverrideUrlLoading;
+      this.flutterApi = flutterApi;
+    }
+
+    @Override
+    public void onPageStarted(WebView view, String url, Bitmap favicon) {
+      if (flutterApi != null) {
+        flutterApi.onPageStarted(this, view, url, reply -> {});
+      }
+    }
+
+    @Override
+    public void onPageFinished(WebView view, String url) {
+      if (flutterApi != null) {
+        flutterApi.onPageFinished(this, view, url, reply -> {});
+      }
+    }
+
+    // This method is only called when the WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR feature is
+    // enabled. The deprecated method is called when a device doesn't support this.
+    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+    @SuppressLint("RequiresFeature")
+    @Override
+    public void onReceivedError(
+        @NonNull WebView view,
+        @NonNull WebResourceRequest request,
+        @NonNull WebResourceErrorCompat error) {
+      if (flutterApi != null) {
+        flutterApi.onReceivedRequestError(this, view, request, error, reply -> {});
+      }
+    }
+
+    @Override
+    public void onReceivedError(
+        WebView view, int errorCode, String description, String failingUrl) {
+      if (flutterApi != null) {
+        flutterApi.onReceivedError(
+            this, view, (long) errorCode, description, failingUrl, reply -> {});
+      }
+    }
+
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    @Override
+    public boolean shouldOverrideUrlLoading(
+        @NonNull WebView view, @NonNull WebResourceRequest request) {
+      if (flutterApi != null) {
+        flutterApi.requestLoading(this, view, request, reply -> {});
+      }
+      return shouldOverrideUrlLoading;
+    }
+
+    @Override
+    public boolean shouldOverrideUrlLoading(WebView view, String url) {
+      if (flutterApi != null) {
+        flutterApi.urlLoading(this, view, url, reply -> {});
+      }
+      return shouldOverrideUrlLoading;
+    }
+
+    @Override
+    public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
+      // Deliberately empty. Occasionally the webview will mark events as having failed to be
+      // handled even though they were handled. We don't want to propagate those as they're not
+      // truly lost.
+    }
+
+    public void release() {
+      if (flutterApi != null) {
+        flutterApi.dispose(this, reply -> {});
+      }
+      flutterApi = null;
+    }
+  }
+
+  /** Handles creating {@link WebViewClient}s for a {@link WebViewClientHostApiImpl}. */
+  public static class WebViewClientCreator {
+    /**
+     * Creates a {@link WebViewClient}.
+     *
+     * @param flutterApi handles sending messages to Dart
+     * @return the created {@link WebViewClient}
+     */
+    public WebViewClient createWebViewClient(
+        WebViewClientFlutterApiImpl flutterApi, boolean shouldOverrideUrlLoading) {
       // WebViewClientCompat is used to get
       // shouldOverrideUrlLoading(WebView view, WebResourceRequest request)
       // invoked by the webview on older Android devices, without it pages that use iframes will
@@ -78,162 +217,33 @@
       // to bug https://bugs.chromium.org/p/chromium/issues/detail?id=925887. Also, see
       // https://github.com/flutter/flutter/issues/29446.
       if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-        return new WebViewClient() {
-          @Override
-          public void onPageStarted(WebView view, String url, Bitmap favicon) {
-            webViewClientFlutterApi.onPageStarted(
-                instanceId, instanceManager.getInstanceId(view), url, reply -> {});
-          }
-
-          @Override
-          public void onPageFinished(WebView view, String url) {
-            webViewClientFlutterApi.onPageFinished(
-                instanceId, instanceManager.getInstanceId(view), url, reply -> {});
-          }
-
-          @Override
-          public void onReceivedError(
-              WebView view, WebResourceRequest request, WebResourceError error) {
-            webViewClientFlutterApi.onReceivedRequestError(
-                instanceId,
-                instanceManager.getInstanceId(view),
-                createWebResourceRequestData(request),
-                createWebResourceErrorData(error),
-                reply -> {});
-          }
-
-          @SuppressWarnings("deprecation")
-          @Override
-          public void onReceivedError(
-              WebView view, int errorCode, String description, String failingUrl) {
-            webViewClientFlutterApi.onReceivedError(
-                instanceId,
-                instanceManager.getInstanceId(view),
-                (long) errorCode,
-                description,
-                failingUrl,
-                reply -> {});
-          }
-
-          @Override
-          public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
-            webViewClientFlutterApi.requestLoading(
-                instanceId,
-                instanceManager.getInstanceId(view),
-                createWebResourceRequestData(request),
-                reply -> {});
-            return shouldOverrideUrlLoading;
-          }
-
-          @SuppressWarnings("deprecation")
-          @Override
-          public boolean shouldOverrideUrlLoading(WebView view, String url) {
-            webViewClientFlutterApi.urlLoading(
-                instanceId, instanceManager.getInstanceId(view), url, reply -> {});
-            return shouldOverrideUrlLoading;
-          }
-
-          @Override
-          public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
-            // Deliberately empty. Occasionally the webview will mark events as having failed to be
-            // handled even though they were handled. We don't want to propagate those as they're not
-            // truly lost.
-          }
-        };
+        return new WebViewClientImpl(flutterApi, shouldOverrideUrlLoading);
       } else {
-        return new WebViewClientCompat() {
-          @Override
-          public void onPageStarted(WebView view, String url, Bitmap favicon) {
-            webViewClientFlutterApi.onPageStarted(
-                instanceId, instanceManager.getInstanceId(view), url, reply -> {});
-          }
-
-          @Override
-          public void onPageFinished(WebView view, String url) {
-            webViewClientFlutterApi.onPageFinished(
-                instanceId, instanceManager.getInstanceId(view), url, reply -> {});
-          }
-
-          // This method is only called when the WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR feature is
-          // enabled. The deprecated method is called when a device doesn't support this.
-          @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
-          @SuppressLint("RequiresFeature")
-          @Override
-          public void onReceivedError(
-              @NonNull WebView view,
-              @NonNull WebResourceRequest request,
-              @NonNull WebResourceErrorCompat error) {
-            webViewClientFlutterApi.onReceivedRequestError(
-                instanceId,
-                instanceManager.getInstanceId(view),
-                createWebResourceRequestData(request),
-                createWebResourceErrorData(error),
-                reply -> {});
-          }
-
-          @SuppressWarnings("deprecation")
-          @Override
-          public void onReceivedError(
-              WebView view, int errorCode, String description, String failingUrl) {
-            webViewClientFlutterApi.onReceivedError(
-                instanceId,
-                instanceManager.getInstanceId(view),
-                (long) errorCode,
-                description,
-                failingUrl,
-                reply -> {});
-          }
-
-          @TargetApi(Build.VERSION_CODES.LOLLIPOP)
-          @Override
-          public boolean shouldOverrideUrlLoading(
-              @NonNull WebView view, @NonNull WebResourceRequest request) {
-            webViewClientFlutterApi.requestLoading(
-                instanceId,
-                instanceManager.getInstanceId(view),
-                createWebResourceRequestData(request),
-                reply -> {});
-            return shouldOverrideUrlLoading;
-          }
-
-          @SuppressWarnings("deprecation")
-          @Override
-          public boolean shouldOverrideUrlLoading(WebView view, String url) {
-            webViewClientFlutterApi.urlLoading(
-                instanceId, instanceManager.getInstanceId(view), url, reply -> {});
-            return shouldOverrideUrlLoading;
-          }
-
-          @Override
-          public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
-            // Deliberately empty. Occasionally the webview will mark events as having failed to be
-            // handled even though they were handled. We don't want to propagate those as they're not
-            // truly lost.
-          }
-        };
+        return new WebViewClientCompatImpl(flutterApi, shouldOverrideUrlLoading);
       }
     }
   }
 
-  WebViewClientHostApiImpl(
+  /**
+   * Creates a host API that handles creating {@link WebViewClient}s.
+   *
+   * @param instanceManager maintains instances stored to communicate with Dart objects
+   * @param webViewClientCreator handles creating {@link WebViewClient}s
+   * @param flutterApi handles sending messages to Dart
+   */
+  public WebViewClientHostApiImpl(
       InstanceManager instanceManager,
       WebViewClientCreator webViewClientCreator,
-      WebViewClientFlutterApi webViewClientFlutterApi) {
+      WebViewClientFlutterApiImpl flutterApi) {
     this.instanceManager = instanceManager;
     this.webViewClientCreator = webViewClientCreator;
-    this.webViewClientFlutterApi = webViewClientFlutterApi;
+    this.flutterApi = flutterApi;
   }
 
   @Override
   public void create(Long instanceId, Boolean shouldOverrideUrlLoading) {
     final WebViewClient webViewClient =
-        webViewClientCreator.createWebViewClient(
-            instanceId, instanceManager, shouldOverrideUrlLoading, webViewClientFlutterApi);
+        webViewClientCreator.createWebViewClient(flutterApi, shouldOverrideUrlLoading);
     instanceManager.addInstance(webViewClient, instanceId);
   }
-
-  @Override
-  public void dispose(Long instanceId) {
-    instanceManager.removeInstance(instanceId);
-  }
 }
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java
index 35bdc60..2d96fc3 100644
--- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java
@@ -4,36 +4,118 @@
 
 package io.flutter.plugins.webviewflutter;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
+import android.hardware.display.DisplayManager;
 import android.view.View;
 import android.webkit.DownloadListener;
 import android.webkit.WebChromeClient;
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import io.flutter.plugin.platform.PlatformView;
+import io.flutter.plugins.webviewflutter.DownloadListenerHostApiImpl.DownloadListenerImpl;
+import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebViewHostApi;
+import io.flutter.plugins.webviewflutter.WebChromeClientHostApiImpl.WebChromeClientImpl;
+import io.flutter.plugins.webviewflutter.WebViewClientHostApiImpl.ReleasableWebViewClient;
+import java.util.HashMap;
 import java.util.Map;
 
-class WebViewHostApiImpl implements GeneratedAndroidWebView.WebViewHostApi {
+/**
+ * Host api implementation for {@link WebView}.
+ *
+ * <p>Handles creating {@link WebView}s that intercommunicate with a paired Dart object.
+ */
+public class WebViewHostApiImpl implements WebViewHostApi {
+  // TODO(bparrishMines): This can be removed once pigeon supports null values: https://github.com/flutter/flutter/issues/59118
+  // Workaround to represent null Strings since pigeon doesn't support null
+  // values.
+  private static final String nullStringIdentifier = "<null-value>";
+
   private final InstanceManager instanceManager;
   private final WebViewProxy webViewProxy;
   private final Context context;
+  // Only used with WebView using virtual displays.
+  @Nullable private final View containerView;
 
-  static class WebViewProxy {
-    WebView createWebView(Context context) {
+  /** Handles creating and calling static methods for {@link WebView}s. */
+  public static class WebViewProxy {
+    /**
+     * Creates a {@link WebViewPlatformView}.
+     *
+     * @param context an Activity Context to access application assets
+     * @return the created {@link WebViewPlatformView}
+     */
+    public WebViewPlatformView createWebView(Context context) {
       return new WebViewPlatformView(context);
     }
 
-    WebView createInputAwareWebView(Context context) {
-      return new InputAwareWebViewPlatformView(context, null);
+    /**
+     * Creates a {@link InputAwareWebViewPlatformView}.
+     *
+     * @param context an Activity Context to access application assets
+     * @param containerView parent View of the WebView
+     * @return the created {@link InputAwareWebViewPlatformView}
+     */
+    public InputAwareWebViewPlatformView createInputAwareWebView(
+        Context context, @Nullable View containerView) {
+      return new InputAwareWebViewPlatformView(context, containerView);
     }
 
-    void setWebContentsDebuggingEnabled(boolean enabled) {
+    /**
+     * Forwards call to {@link WebView#setWebContentsDebuggingEnabled}.
+     *
+     * @param enabled whether debugging should be enabled
+     */
+    public void setWebContentsDebuggingEnabled(boolean enabled) {
       WebView.setWebContentsDebuggingEnabled(enabled);
     }
   }
 
-  private static class WebViewPlatformView extends WebView implements PlatformView {
+  private static class ReleasableValue<T extends Releasable> {
+    @Nullable private T value;
+
+    ReleasableValue() {}
+
+    ReleasableValue(@Nullable T value) {
+      this.value = value;
+    }
+
+    void set(@Nullable T newValue) {
+      release();
+      value = newValue;
+    }
+
+    @Nullable
+    T get() {
+      return value;
+    }
+
+    void release() {
+      if (value != null) {
+        value.release();
+      }
+      value = null;
+    }
+  }
+
+  /** Implementation of {@link WebView} that can be used as a Flutter {@link PlatformView}s. */
+  public static class WebViewPlatformView extends WebView implements PlatformView, Releasable {
+    private final ReleasableValue<WebViewClientHostApiImpl.ReleasableWebViewClient>
+        currentWebViewClient = new ReleasableValue<>();
+    private final ReleasableValue<DownloadListenerImpl> currentDownloadListener =
+        new ReleasableValue<>();
+    private final ReleasableValue<WebChromeClientImpl> currentWebChromeClient =
+        new ReleasableValue<>();
+    private final Map<String, ReleasableValue<JavaScriptChannel>> javaScriptInterfaces =
+        new HashMap<>();
+
+    /**
+     * Creates a {@link WebViewPlatformView}.
+     *
+     * @param context an Activity Context to access application assets. This value cannot be null.
+     */
     public WebViewPlatformView(Context context) {
       super(context);
     }
@@ -47,11 +129,85 @@
     public void dispose() {
       destroy();
     }
+
+    @Override
+    public void setWebViewClient(WebViewClient webViewClient) {
+      super.setWebViewClient(webViewClient);
+      currentWebViewClient.set((ReleasableWebViewClient) webViewClient);
+
+      final WebChromeClientImpl webChromeClient = currentWebChromeClient.get();
+      if (webChromeClient != null) {
+        ((WebChromeClientImpl) webChromeClient).setWebViewClient(webViewClient);
+      }
+    }
+
+    @Override
+    public void setDownloadListener(DownloadListener listener) {
+      super.setDownloadListener(listener);
+      currentDownloadListener.set((DownloadListenerImpl) listener);
+    }
+
+    @Override
+    public void setWebChromeClient(WebChromeClient client) {
+      super.setWebChromeClient(client);
+      currentWebChromeClient.set((WebChromeClientImpl) client);
+    }
+
+    @SuppressLint("JavascriptInterface")
+    @Override
+    public void addJavascriptInterface(Object object, String name) {
+      super.addJavascriptInterface(object, name);
+      if (object instanceof JavaScriptChannel) {
+        final ReleasableValue<JavaScriptChannel> javaScriptChannel = javaScriptInterfaces.get(name);
+        if (javaScriptChannel != null && javaScriptChannel.get() != object) {
+          javaScriptChannel.release();
+        }
+        javaScriptInterfaces.put(name, new ReleasableValue<>((JavaScriptChannel) object));
+      }
+    }
+
+    @Override
+    public void removeJavascriptInterface(@NonNull String name) {
+      super.removeJavascriptInterface(name);
+      final ReleasableValue<JavaScriptChannel> javaScriptChannel = javaScriptInterfaces.get(name);
+      javaScriptChannel.release();
+      javaScriptInterfaces.remove(name);
+    }
+
+    @Override
+    public void release() {
+      currentWebViewClient.release();
+      currentDownloadListener.release();
+      currentWebChromeClient.release();
+      for (ReleasableValue<JavaScriptChannel> channel : javaScriptInterfaces.values()) {
+        channel.release();
+      }
+      javaScriptInterfaces.clear();
+    }
   }
 
-  private static class InputAwareWebViewPlatformView extends InputAwareWebView
-      implements PlatformView {
-    InputAwareWebViewPlatformView(Context context, View containerView) {
+  /**
+   * Implementation of {@link InputAwareWebView} that can be used as a Flutter {@link
+   * PlatformView}s.
+   */
+  @SuppressLint("ViewConstructor")
+  public static class InputAwareWebViewPlatformView extends InputAwareWebView
+      implements PlatformView, Releasable {
+    private final ReleasableValue<WebViewClientHostApiImpl.ReleasableWebViewClient>
+        currentWebViewClient = new ReleasableValue<>();
+    private final ReleasableValue<DownloadListenerImpl> currentDownloadListener =
+        new ReleasableValue<>();
+    private final ReleasableValue<WebChromeClientImpl> currentWebChromeClient =
+        new ReleasableValue<>();
+    private final Map<String, ReleasableValue<JavaScriptChannel>> javaScriptInterfaces =
+        new HashMap<>();
+
+    /**
+     * Creates a {@link InputAwareWebViewPlatformView}.
+     *
+     * @param context an Activity Context to access application assets. This value cannot be null.
+     */
+    public InputAwareWebViewPlatformView(Context context, View containerView) {
       super(context, containerView);
     }
 
@@ -72,7 +228,7 @@
 
     @Override
     public void dispose() {
-      dispose();
+      super.dispose();
       destroy();
     }
 
@@ -85,26 +241,104 @@
     public void onInputConnectionUnlocked() {
       unlockInputConnection();
     }
+
+    @Override
+    public void setWebViewClient(WebViewClient webViewClient) {
+      super.setWebViewClient(webViewClient);
+      currentWebViewClient.set((ReleasableWebViewClient) webViewClient);
+
+      final WebChromeClientImpl webChromeClient = currentWebChromeClient.get();
+      if (webChromeClient != null) {
+        webChromeClient.setWebViewClient(webViewClient);
+      }
+    }
+
+    @Override
+    public void setDownloadListener(DownloadListener listener) {
+      super.setDownloadListener(listener);
+      currentDownloadListener.set((DownloadListenerImpl) listener);
+    }
+
+    @Override
+    public void setWebChromeClient(WebChromeClient client) {
+      super.setWebChromeClient(client);
+      currentWebChromeClient.set((WebChromeClientImpl) client);
+    }
+
+    @SuppressLint("JavascriptInterface")
+    @Override
+    public void addJavascriptInterface(Object object, String name) {
+      super.addJavascriptInterface(object, name);
+      if (object instanceof JavaScriptChannel) {
+        final ReleasableValue<JavaScriptChannel> javaScriptChannel = javaScriptInterfaces.get(name);
+        if (javaScriptChannel != null && javaScriptChannel.get() != object) {
+          javaScriptChannel.release();
+        }
+        javaScriptInterfaces.put(name, new ReleasableValue<>((JavaScriptChannel) object));
+      }
+    }
+
+    @Override
+    public void removeJavascriptInterface(@NonNull String name) {
+      super.removeJavascriptInterface(name);
+      final ReleasableValue<JavaScriptChannel> javaScriptChannel = javaScriptInterfaces.get(name);
+      javaScriptChannel.release();
+      javaScriptInterfaces.remove(name);
+    }
+
+    @Override
+    public void release() {
+      currentWebViewClient.release();
+      currentDownloadListener.release();
+      currentWebChromeClient.release();
+      for (ReleasableValue<JavaScriptChannel> channel : javaScriptInterfaces.values()) {
+        channel.release();
+      }
+      javaScriptInterfaces.clear();
+    }
   }
 
-  WebViewHostApiImpl(InstanceManager instanceManager, WebViewProxy webViewProxy, Context context) {
+  /**
+   * Creates a host API that handles creating {@link WebView}s and invoking its methods.
+   *
+   * @param instanceManager maintains instances stored to communicate with Dart objects
+   * @param webViewProxy handles creating {@link WebView}s and calling its static methods
+   * @param context an Activity Context to access application assets. This value cannot be null.
+   * @param containerView parent of the webView
+   */
+  public WebViewHostApiImpl(
+      InstanceManager instanceManager,
+      WebViewProxy webViewProxy,
+      Context context,
+      @Nullable View containerView) {
     this.instanceManager = instanceManager;
     this.webViewProxy = webViewProxy;
     this.context = context;
+    this.containerView = containerView;
   }
 
   @Override
   public void create(Long instanceId, Boolean useHybridComposition) {
+    DisplayListenerProxy displayListenerProxy = new DisplayListenerProxy();
+    DisplayManager displayManager =
+        (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
+    displayListenerProxy.onPreWebViewInitialization(displayManager);
+
     final WebView webView =
         useHybridComposition
             ? webViewProxy.createWebView(context)
-            : webViewProxy.createInputAwareWebView(context);
+            : webViewProxy.createInputAwareWebView(context, containerView);
+
+    displayListenerProxy.onPostWebViewInitialization(displayManager);
     instanceManager.addInstance(webView, instanceId);
   }
 
   @Override
   public void dispose(Long instanceId) {
-    instanceManager.removeInstance(instanceId);
+    final WebView instance = (WebView) instanceManager.removeInstanceWithId(instanceId);
+    if (instance != null) {
+      ((Releasable) instance).release();
+    }
   }
 
   @Override
@@ -116,7 +350,8 @@
   @Override
   public String getUrl(Long instanceId) {
     final WebView webView = (WebView) instanceManager.getInstance(instanceId);
-    return webView.getUrl();
+    final String result = webView.getUrl();
+    return result != null ? result : nullStringIdentifier;
   }
 
   @Override
@@ -165,7 +400,8 @@
   @Override
   public String getTitle(Long instanceId) {
     final WebView webView = (WebView) instanceManager.getInstance(instanceId);
-    return webView.getTitle();
+    final String result = webView.getTitle();
+    return result != null ? result : nullStringIdentifier;
   }
 
   @Override
@@ -228,6 +464,6 @@
   @Override
   public void setWebChromeClient(Long instanceId, Long clientInstanceId) {
     final WebView webView = (WebView) instanceManager.getInstance(instanceId);
-    webView.setWebChromeClient((WebChromeClient) instanceManager.getInstance(instanceId));
+    webView.setWebChromeClient((WebChromeClient) instanceManager.getInstance(clientInstanceId));
   }
 }
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/DownloadListenerTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/DownloadListenerTest.java
index 5ba073c..2391193 100644
--- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/DownloadListenerTest.java
+++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/DownloadListenerTest.java
@@ -6,11 +6,13 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 
 import android.webkit.DownloadListener;
 import io.flutter.plugins.webviewflutter.DownloadListenerHostApiImpl.DownloadListenerCreator;
-import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerFlutterApi;
+import io.flutter.plugins.webviewflutter.DownloadListenerHostApiImpl.DownloadListenerImpl;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -21,45 +23,49 @@
 public class DownloadListenerTest {
   @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
 
-  @Mock public DownloadListenerFlutterApi mockFlutterApi;
+  @Mock public DownloadListenerFlutterApiImpl mockFlutterApi;
 
-  InstanceManager testInstanceManager;
-  DownloadListenerHostApiImpl testHostApiImpl;
-  DownloadListener testDownloadListener;
+  InstanceManager instanceManager;
+  DownloadListenerHostApiImpl hostApiImpl;
+  DownloadListenerImpl downloadListener;
 
   @Before
   public void setUp() {
-    testInstanceManager = new InstanceManager();
+    instanceManager = new InstanceManager();
 
     final DownloadListenerCreator downloadListenerCreator =
         new DownloadListenerCreator() {
           @Override
-          DownloadListener createDownloadListener(
-              Long instanceId, DownloadListenerFlutterApi downloadListenerFlutterApi) {
-            testDownloadListener =
-                super.createDownloadListener(instanceId, downloadListenerFlutterApi);
-            return testDownloadListener;
+          public DownloadListenerImpl createDownloadListener(
+              DownloadListenerFlutterApiImpl flutterApi) {
+            downloadListener = super.createDownloadListener(flutterApi);
+            return downloadListener;
           }
         };
 
-    testHostApiImpl =
-        new DownloadListenerHostApiImpl(
-            testInstanceManager, downloadListenerCreator, mockFlutterApi);
-    testHostApiImpl.create(0L);
+    hostApiImpl =
+        new DownloadListenerHostApiImpl(instanceManager, downloadListenerCreator, mockFlutterApi);
+    hostApiImpl.create(0L);
   }
 
   @Test
   public void postMessage() {
-    testDownloadListener.onDownloadStart(
+    downloadListener.onDownloadStart(
         "https://www.google.com", "userAgent", "contentDisposition", "mimetype", 54);
     verify(mockFlutterApi)
         .onDownloadStart(
-            eq(0L),
+            eq(downloadListener),
             eq("https://www.google.com"),
             eq("userAgent"),
             eq("contentDisposition"),
             eq("mimetype"),
             eq(54L),
             any());
+
+    reset(mockFlutterApi);
+    downloadListener.release();
+    downloadListener.onDownloadStart("", "", "", "", 23);
+    verify(mockFlutterApi, never())
+        .onDownloadStart((DownloadListener) any(), any(), any(), any(), any(), eq(23), any());
   }
 }
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/JavaScriptChannelTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/JavaScriptChannelTest.java
index 697ea0b..3de81da 100644
--- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/JavaScriptChannelTest.java
+++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/JavaScriptChannelTest.java
@@ -6,10 +6,11 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 
 import android.os.Handler;
-import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelFlutterApi;
 import io.flutter.plugins.webviewflutter.JavaScriptChannelHostApiImpl.JavaScriptChannelCreator;
 import org.junit.Before;
 import org.junit.Rule;
@@ -21,40 +22,44 @@
 public class JavaScriptChannelTest {
   @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
 
-  @Mock public GeneratedAndroidWebView.JavaScriptChannelFlutterApi mockFlutterApi;
+  @Mock public JavaScriptChannelFlutterApiImpl mockFlutterApi;
 
-  InstanceManager testInstanceManager;
-  JavaScriptChannelHostApiImpl testHostApiImpl;
-  JavaScriptChannel testJavaScriptChannel;
+  InstanceManager instanceManager;
+  JavaScriptChannelHostApiImpl hostApiImpl;
+  JavaScriptChannel javaScriptChannel;
 
   @Before
   public void setUp() {
-    testInstanceManager = new InstanceManager();
+    instanceManager = new InstanceManager();
 
     final JavaScriptChannelCreator javaScriptChannelCreator =
         new JavaScriptChannelCreator() {
           @Override
-          JavaScriptChannel createJavaScriptChannel(
-              Long instanceId,
-              JavaScriptChannelFlutterApi javaScriptChannelFlutterApi,
+          public JavaScriptChannel createJavaScriptChannel(
+              JavaScriptChannelFlutterApiImpl javaScriptChannelFlutterApi,
               String channelName,
               Handler platformThreadHandler) {
-            testJavaScriptChannel =
+            javaScriptChannel =
                 super.createJavaScriptChannel(
-                    instanceId, javaScriptChannelFlutterApi, channelName, platformThreadHandler);
-            return testJavaScriptChannel;
+                    javaScriptChannelFlutterApi, channelName, platformThreadHandler);
+            return javaScriptChannel;
           }
         };
 
-    testHostApiImpl =
+    hostApiImpl =
         new JavaScriptChannelHostApiImpl(
-            testInstanceManager, javaScriptChannelCreator, mockFlutterApi, new Handler());
-    testHostApiImpl.create(0L, "aChannelName");
+            instanceManager, javaScriptChannelCreator, mockFlutterApi, new Handler());
+    hostApiImpl.create(0L, "aChannelName");
   }
 
   @Test
   public void postMessage() {
-    testJavaScriptChannel.postMessage("A message post.");
-    verify(mockFlutterApi).postMessage(eq(0L), eq("A message post."), any());
+    javaScriptChannel.postMessage("A message post.");
+    verify(mockFlutterApi).postMessage(eq(javaScriptChannel), eq("A message post."), any());
+
+    reset(mockFlutterApi);
+    javaScriptChannel.release();
+    javaScriptChannel.postMessage("a message");
+    verify(mockFlutterApi, never()).postMessage((JavaScriptChannel) any(), any(), any());
   }
 }
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java
index 5ab3ab1..d6593ab 100644
--- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java
+++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java
@@ -6,13 +6,15 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 
 import android.webkit.WebChromeClient;
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
-import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientFlutterApi;
 import io.flutter.plugins.webviewflutter.WebChromeClientHostApiImpl.WebChromeClientCreator;
+import io.flutter.plugins.webviewflutter.WebChromeClientHostApiImpl.WebChromeClientImpl;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -23,45 +25,45 @@
 public class WebChromeClientTest {
   @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
 
-  @Mock public WebChromeClientFlutterApi mockFlutterApi;
+  @Mock public WebChromeClientFlutterApiImpl mockFlutterApi;
 
   @Mock public WebView mockWebView;
 
   @Mock public WebViewClient mockWebViewClient;
 
-  InstanceManager testInstanceManager;
-  WebChromeClientHostApiImpl testHostApiImpl;
-  WebChromeClient testWebChromeClient;
+  InstanceManager instanceManager;
+  WebChromeClientHostApiImpl hostApiImpl;
+  WebChromeClientImpl webChromeClient;
 
   @Before
   public void setUp() {
-    testInstanceManager = new InstanceManager();
-    testInstanceManager.addInstance(mockWebView, 0L);
-    testInstanceManager.addInstance(mockWebViewClient, 1L);
+    instanceManager = new InstanceManager();
+    instanceManager.addInstance(mockWebView, 0L);
+    instanceManager.addInstance(mockWebViewClient, 1L);
 
     final WebChromeClientCreator webChromeClientCreator =
         new WebChromeClientCreator() {
           @Override
-          WebChromeClient createWebChromeClient(
-              Long instanceId,
-              InstanceManager instanceManager,
-              WebViewClient webViewClient,
-              WebChromeClientFlutterApi webChromeClientFlutterApi) {
-            testWebChromeClient =
-                super.createWebChromeClient(
-                    instanceId, instanceManager, webViewClient, webChromeClientFlutterApi);
-            return testWebChromeClient;
+          public WebChromeClientImpl createWebChromeClient(
+              WebChromeClientFlutterApiImpl flutterApi, WebViewClient webViewClient) {
+            webChromeClient = super.createWebChromeClient(flutterApi, webViewClient);
+            return webChromeClient;
           }
         };
 
-    testHostApiImpl =
-        new WebChromeClientHostApiImpl(testInstanceManager, webChromeClientCreator, mockFlutterApi);
-    testHostApiImpl.create(2L, 1L);
+    hostApiImpl =
+        new WebChromeClientHostApiImpl(instanceManager, webChromeClientCreator, mockFlutterApi);
+    hostApiImpl.create(2L, 1L);
   }
 
   @Test
   public void onProgressChanged() {
-    testWebChromeClient.onProgressChanged(mockWebView, 23);
-    verify(mockFlutterApi).onProgressChanged(eq(2L), eq(0L), eq(23L), any());
+    webChromeClient.onProgressChanged(mockWebView, 23);
+    verify(mockFlutterApi).onProgressChanged(eq(webChromeClient), eq(mockWebView), eq(23L), any());
+
+    reset(mockFlutterApi);
+    webChromeClient.release();
+    webChromeClient.onProgressChanged(mockWebView, 11);
+    verify(mockFlutterApi, never()).onProgressChanged((WebChromeClient) any(), any(), any(), any());
   }
 }
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientTest.java
index f6d2054..62d2723 100644
--- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientTest.java
+++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientTest.java
@@ -6,11 +6,13 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
-import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebViewClientFlutterApi;
+import io.flutter.plugins.webviewflutter.WebViewClientHostApiImpl.WebViewClientCompatImpl;
 import io.flutter.plugins.webviewflutter.WebViewClientHostApiImpl.WebViewClientCreator;
 import org.junit.Before;
 import org.junit.Rule;
@@ -22,56 +24,76 @@
 public class WebViewClientTest {
   @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
 
-  @Mock public WebViewClientFlutterApi mockFlutterApi;
+  @Mock public WebViewClientFlutterApiImpl mockFlutterApi;
 
   @Mock public WebView mockWebView;
 
-  InstanceManager testInstanceManager;
-  WebViewClientHostApiImpl testHostApiImpl;
-  WebViewClient testWebViewClient;
+  InstanceManager instanceManager;
+  WebViewClientHostApiImpl hostApiImpl;
+  WebViewClientCompatImpl webViewClient;
 
   @Before
   public void setUp() {
-    testInstanceManager = new InstanceManager();
-    testInstanceManager.addInstance(mockWebView, 0L);
+    instanceManager = new InstanceManager();
+    instanceManager.addInstance(mockWebView, 0L);
 
     final WebViewClientCreator webViewClientCreator =
         new WebViewClientCreator() {
           @Override
-          WebViewClient createWebViewClient(
-              Long instanceId,
-              InstanceManager instanceManager,
-              Boolean shouldOverrideUrlLoading,
-              WebViewClientFlutterApi webViewClientFlutterApi) {
-            testWebViewClient =
-                super.createWebViewClient(
-                    instanceId, instanceManager, shouldOverrideUrlLoading, webViewClientFlutterApi);
-            return testWebViewClient;
+          public WebViewClient createWebViewClient(
+              WebViewClientFlutterApiImpl flutterApi, boolean shouldOverrideUrlLoading) {
+            webViewClient =
+                (WebViewClientCompatImpl)
+                    super.createWebViewClient(flutterApi, shouldOverrideUrlLoading);
+            return webViewClient;
           }
         };
 
-    testHostApiImpl =
-        new WebViewClientHostApiImpl(testInstanceManager, webViewClientCreator, mockFlutterApi);
-    testHostApiImpl.create(1L, true);
+    hostApiImpl =
+        new WebViewClientHostApiImpl(instanceManager, webViewClientCreator, mockFlutterApi);
+    hostApiImpl.create(1L, true);
   }
 
   @Test
   public void onPageStarted() {
-    testWebViewClient.onPageStarted(mockWebView, "https://www.google.com", null);
-    verify(mockFlutterApi).onPageStarted(eq(1L), eq(0L), eq("https://www.google.com"), any());
+    webViewClient.onPageStarted(mockWebView, "https://www.google.com", null);
+    verify(mockFlutterApi)
+        .onPageStarted(eq(webViewClient), eq(mockWebView), eq("https://www.google.com"), any());
+
+    reset(mockFlutterApi);
+    webViewClient.release();
+    webViewClient.onPageStarted(mockWebView, "", null);
+    verify(mockFlutterApi, never()).onPageStarted((WebViewClient) any(), any(), any(), any());
   }
 
   @Test
   public void onReceivedError() {
-    testWebViewClient.onReceivedError(mockWebView, 32, "description", "https://www.google.com");
+    webViewClient.onReceivedError(mockWebView, 32, "description", "https://www.google.com");
     verify(mockFlutterApi)
         .onReceivedError(
-            eq(1L), eq(0L), eq(32L), eq("description"), eq("https://www.google.com"), any());
+            eq(webViewClient),
+            eq(mockWebView),
+            eq(32L),
+            eq("description"),
+            eq("https://www.google.com"),
+            any());
+
+    reset(mockFlutterApi);
+    webViewClient.release();
+    webViewClient.onReceivedError(mockWebView, 33, "", "");
+    verify(mockFlutterApi, never())
+        .onReceivedError((WebViewClient) any(), any(), any(), any(), any(), any());
   }
 
   @Test
   public void urlLoading() {
-    testWebViewClient.shouldOverrideUrlLoading(mockWebView, "https://www.google.com");
-    verify(mockFlutterApi).urlLoading(eq(1L), eq(0L), eq("https://www.google.com"), any());
+    webViewClient.shouldOverrideUrlLoading(mockWebView, "https://www.google.com");
+    verify(mockFlutterApi)
+        .urlLoading(eq(webViewClient), eq(mockWebView), eq("https://www.google.com"), any());
+
+    reset(mockFlutterApi);
+    webViewClient.release();
+    webViewClient.shouldOverrideUrlLoading(mockWebView, "");
+    verify(mockFlutterApi, never()).urlLoading((WebViewClient) any(), any(), any(), any());
   }
 }
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java
index b914ce9..e506188 100644
--- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java
+++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java
@@ -13,8 +13,13 @@
 import android.content.Context;
 import android.webkit.DownloadListener;
 import android.webkit.ValueCallback;
-import android.webkit.WebView;
+import android.webkit.WebChromeClient;
 import android.webkit.WebViewClient;
+import io.flutter.plugins.webviewflutter.DownloadListenerHostApiImpl.DownloadListenerImpl;
+import io.flutter.plugins.webviewflutter.WebChromeClientHostApiImpl.WebChromeClientImpl;
+import io.flutter.plugins.webviewflutter.WebViewClientHostApiImpl.WebViewClientImpl;
+import io.flutter.plugins.webviewflutter.WebViewHostApiImpl.InputAwareWebViewPlatformView;
+import io.flutter.plugins.webviewflutter.WebViewHostApiImpl.WebViewPlatformView;
 import java.util.HashMap;
 import org.junit.Before;
 import org.junit.Rule;
@@ -27,7 +32,7 @@
 public class WebViewTest {
   @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
 
-  @Mock public WebView mockWebView;
+  @Mock public WebViewPlatformView mockWebView;
 
   @Mock WebViewHostApiImpl.WebViewProxy mockWebViewProxy;
 
@@ -40,45 +45,115 @@
   public void setUp() {
     testInstanceManager = new InstanceManager();
     when(mockWebViewProxy.createWebView(mockContext)).thenReturn(mockWebView);
-    testHostApiImpl = new WebViewHostApiImpl(testInstanceManager, mockWebViewProxy, mockContext);
+    testHostApiImpl =
+        new WebViewHostApiImpl(testInstanceManager, mockWebViewProxy, mockContext, null);
     testHostApiImpl.create(0L, true);
   }
 
   @Test
-  public void errorCodes() {
-    assertEquals(
-        FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_AUTHENTICATION),
-        "authentication");
-    assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_BAD_URL), "badUrl");
-    assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_CONNECT), "connect");
-    assertEquals(
-        FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FAILED_SSL_HANDSHAKE),
-        "failedSslHandshake");
-    assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FILE), "file");
-    assertEquals(
-        FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FILE_NOT_FOUND), "fileNotFound");
-    assertEquals(
-        FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_HOST_LOOKUP), "hostLookup");
-    assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_IO), "io");
-    assertEquals(
-        FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_PROXY_AUTHENTICATION),
-        "proxyAuthentication");
-    assertEquals(
-        FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_REDIRECT_LOOP), "redirectLoop");
-    assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_TIMEOUT), "timeout");
-    assertEquals(
-        FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_TOO_MANY_REQUESTS),
-        "tooManyRequests");
-    assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNKNOWN), "unknown");
-    assertEquals(
-        FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSAFE_RESOURCE),
-        "unsafeResource");
-    assertEquals(
-        FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME),
-        "unsupportedAuthScheme");
-    assertEquals(
-        FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSUPPORTED_SCHEME),
-        "unsupportedScheme");
+  public void releaseWebView() {
+    final WebViewPlatformView webView = new WebViewPlatformView(mockContext);
+
+    final WebViewClientImpl mockWebViewClient = mock(WebViewClientImpl.class);
+    final WebChromeClientImpl mockWebChromeClient = mock(WebChromeClientImpl.class);
+    final DownloadListenerImpl mockDownloadListener = mock(DownloadListenerImpl.class);
+    final JavaScriptChannel mockJavaScriptChannel = mock(JavaScriptChannel.class);
+
+    webView.setWebViewClient(mockWebViewClient);
+    webView.setWebChromeClient(mockWebChromeClient);
+    webView.setDownloadListener(mockDownloadListener);
+    webView.addJavascriptInterface(mockJavaScriptChannel, "jchannel");
+
+    webView.release();
+
+    verify(mockWebViewClient).release();
+    verify(mockWebChromeClient).release();
+    verify(mockDownloadListener).release();
+    verify(mockJavaScriptChannel).release();
+  }
+
+  @Test
+  public void releaseWebViewDependents() {
+    final WebViewPlatformView webView = new WebViewPlatformView(mockContext);
+
+    final WebViewClientImpl mockWebViewClient = mock(WebViewClientImpl.class);
+    final WebChromeClientImpl mockWebChromeClient = mock(WebChromeClientImpl.class);
+    final DownloadListenerImpl mockDownloadListener = mock(DownloadListenerImpl.class);
+    final JavaScriptChannel mockJavaScriptChannel = mock(JavaScriptChannel.class);
+    final JavaScriptChannel mockJavaScriptChannel2 = mock(JavaScriptChannel.class);
+
+    webView.setWebViewClient(mockWebViewClient);
+    webView.setWebChromeClient(mockWebChromeClient);
+    webView.setDownloadListener(mockDownloadListener);
+    webView.addJavascriptInterface(mockJavaScriptChannel, "jchannel");
+
+    // Release should be called on the object added above.
+    webView.addJavascriptInterface(mockJavaScriptChannel2, "jchannel");
+    verify(mockJavaScriptChannel).release();
+
+    webView.setWebViewClient(null);
+    webView.setWebChromeClient(null);
+    webView.setDownloadListener(null);
+    webView.removeJavascriptInterface("jchannel");
+
+    verify(mockWebViewClient).release();
+    verify(mockWebChromeClient).release();
+    verify(mockDownloadListener).release();
+    verify(mockJavaScriptChannel2).release();
+  }
+
+  @Test
+  public void releaseInputAwareWebView() {
+    final InputAwareWebViewPlatformView webView =
+        new InputAwareWebViewPlatformView(mockContext, null);
+
+    final WebViewClientImpl mockWebViewClient = mock(WebViewClientImpl.class);
+    final WebChromeClientImpl mockWebChromeClient = mock(WebChromeClientImpl.class);
+    final DownloadListenerImpl mockDownloadListener = mock(DownloadListenerImpl.class);
+    final JavaScriptChannel mockJavaScriptChannel = mock(JavaScriptChannel.class);
+
+    webView.setWebViewClient(mockWebViewClient);
+    webView.setWebChromeClient(mockWebChromeClient);
+    webView.setDownloadListener(mockDownloadListener);
+    webView.addJavascriptInterface(mockJavaScriptChannel, "jchannel");
+
+    webView.release();
+
+    verify(mockWebViewClient).release();
+    verify(mockWebChromeClient).release();
+    verify(mockDownloadListener).release();
+    verify(mockJavaScriptChannel).release();
+  }
+
+  @Test
+  public void releaseInputAwareWebViewDependents() {
+    final InputAwareWebViewPlatformView webView =
+        new InputAwareWebViewPlatformView(mockContext, null);
+
+    final WebViewClientImpl mockWebViewClient = mock(WebViewClientImpl.class);
+    final WebChromeClientImpl mockWebChromeClient = mock(WebChromeClientImpl.class);
+    final DownloadListenerImpl mockDownloadListener = mock(DownloadListenerImpl.class);
+    final JavaScriptChannel mockJavaScriptChannel = mock(JavaScriptChannel.class);
+    final JavaScriptChannel mockJavaScriptChannel2 = mock(JavaScriptChannel.class);
+
+    webView.setWebViewClient(mockWebViewClient);
+    webView.setWebChromeClient(mockWebChromeClient);
+    webView.setDownloadListener(mockDownloadListener);
+    webView.addJavascriptInterface(mockJavaScriptChannel, "jchannel");
+
+    // Release should be called on the object added above.
+    webView.addJavascriptInterface(mockJavaScriptChannel2, "jchannel");
+    verify(mockJavaScriptChannel).release();
+
+    webView.setWebViewClient(null);
+    webView.setWebChromeClient(null);
+    webView.setDownloadListener(null);
+    webView.removeJavascriptInterface("jchannel");
+
+    verify(mockWebViewClient).release();
+    verify(mockWebChromeClient).release();
+    verify(mockDownloadListener).release();
+    verify(mockJavaScriptChannel2).release();
   }
 
   @Test
@@ -195,7 +270,8 @@
 
   @Test
   public void addJavaScriptChannel() {
-    final JavaScriptChannel javaScriptChannel = new JavaScriptChannel(null, "aName", null);
+    final JavaScriptChannel javaScriptChannel =
+        new JavaScriptChannel(mock(JavaScriptChannelFlutterApiImpl.class), "aName", null);
     testInstanceManager.addInstance(javaScriptChannel, 1L);
 
     testHostApiImpl.addJavaScriptChannel(0L, 1L);
@@ -204,7 +280,8 @@
 
   @Test
   public void removeJavaScriptChannel() {
-    final JavaScriptChannel javaScriptChannel = new JavaScriptChannel(null, "aName", null);
+    final JavaScriptChannel javaScriptChannel =
+        new JavaScriptChannel(mock(JavaScriptChannelFlutterApiImpl.class), "aName", null);
     testInstanceManager.addInstance(javaScriptChannel, 1L);
 
     testHostApiImpl.removeJavaScriptChannel(0L, 1L);
@@ -219,4 +296,13 @@
     testHostApiImpl.setDownloadListener(0L, 1L);
     verify(mockWebView).setDownloadListener(mockDownloadListener);
   }
+
+  @Test
+  public void setWebChromeClient() {
+    final WebChromeClient mockWebChromeClient = mock(WebChromeClient.class);
+    testInstanceManager.addInstance(mockWebChromeClient, 1L);
+
+    testHostApiImpl.setWebChromeClient(0L, 1L);
+    verify(mockWebView).setWebChromeClient(mockWebChromeClient);
+  }
 }