diff --git a/packages/google_sign_in/google_sign_in_android/CHANGELOG.md b/packages/google_sign_in/google_sign_in_android/CHANGELOG.md
index 2d9aae4ff7..499856fe9a 100644
--- a/packages/google_sign_in/google_sign_in_android/CHANGELOG.md
+++ b/packages/google_sign_in/google_sign_in_android/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 6.1.17
+
+* Converts method channels to Pigeon.
+
 ## 6.1.16
 
 * Updates Guava to version 32.0.1.
diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java
index d27cd5e116..eb4635a817 100644
--- a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java
+++ b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java
@@ -31,11 +31,10 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin;
 import io.flutter.embedding.engine.plugins.activity.ActivityAware;
 import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
 import io.flutter.plugin.common.BinaryMessenger;
-import io.flutter.plugin.common.MethodCall;
 import io.flutter.plugin.common.MethodChannel;
-import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
-import io.flutter.plugin.common.MethodChannel.Result;
 import io.flutter.plugin.common.PluginRegistry;
+import io.flutter.plugins.googlesignin.Messages.FlutterError;
+import io.flutter.plugins.googlesignin.Messages.GoogleSignInApi;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -45,21 +44,9 @@ import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 
 /** Google sign-in plugin for Flutter. */
-public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, ActivityAware {
-  private static final String CHANNEL_NAME = "plugins.flutter.io/google_sign_in_android";
-
-  private static final String METHOD_INIT = "init";
-  private static final String METHOD_SIGN_IN_SILENTLY = "signInSilently";
-  private static final String METHOD_SIGN_IN = "signIn";
-  private static final String METHOD_GET_TOKENS = "getTokens";
-  private static final String METHOD_SIGN_OUT = "signOut";
-  private static final String METHOD_DISCONNECT = "disconnect";
-  private static final String METHOD_IS_SIGNED_IN = "isSignedIn";
-  private static final String METHOD_CLEAR_AUTH_CACHE = "clearAuthCache";
-  private static final String METHOD_REQUEST_SCOPES = "requestScopes";
-
+public class GoogleSignInPlugin implements FlutterPlugin, ActivityAware {
   private Delegate delegate;
-  private MethodChannel channel;
+  private @Nullable BinaryMessenger messenger;
   private ActivityPluginBinding activityPluginBinding;
 
   @SuppressWarnings("deprecation")
@@ -75,9 +62,9 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act
       @NonNull BinaryMessenger messenger,
       @NonNull Context context,
       @NonNull GoogleSignInWrapper googleSignInWrapper) {
-    channel = new MethodChannel(messenger, CHANNEL_NAME);
+    this.messenger = messenger;
     delegate = new Delegate(context, googleSignInWrapper);
-    channel.setMethodCallHandler(this);
+    GoogleSignInApi.setup(messenger, delegate);
   }
 
   @VisibleForTesting
@@ -88,8 +75,10 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act
 
   private void dispose() {
     delegate = null;
-    channel.setMethodCallHandler(null);
-    channel = null;
+    if (messenger != null) {
+      GoogleSignInApi.setup(messenger, null);
+      messenger = null;
+    }
   }
 
   private void attachToActivity(ActivityPluginBinding activityPluginBinding) {
@@ -136,10 +125,15 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act
     disposeActivity();
   }
 
-  @Override
-  public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
+  // TODO(stuartmorgan): Remove this, and convert the unit tests to IDelegate tests. This is left
+  // here only to allow the existing tests to continue to work unchanged during the Pigeon migration
+  // to ensure that the refactoring didn't change any behavior, and is not actually used by the
+  // plugin.
+  @VisibleForTesting
+  void onMethodCall(
+      @NonNull io.flutter.plugin.common.MethodCall call, @NonNull MethodChannel.Result result) {
     switch (call.method) {
-      case METHOD_INIT:
+      case "init":
         String signInOption = Objects.requireNonNull(call.argument("signInOption"));
         List<String> requestedScopes = Objects.requireNonNull(call.argument("scopes"));
         String hostedDomain = call.argument("hostedDomain");
@@ -157,38 +151,38 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act
             forceCodeForRefreshToken);
         break;
 
-      case METHOD_SIGN_IN_SILENTLY:
+      case "signInSilently":
         delegate.signInSilently(result);
         break;
 
-      case METHOD_SIGN_IN:
+      case "signIn":
         delegate.signIn(result);
         break;
 
-      case METHOD_GET_TOKENS:
+      case "getTokens":
         String email = Objects.requireNonNull(call.argument("email"));
         boolean shouldRecoverAuth = Objects.requireNonNull(call.argument("shouldRecoverAuth"));
         delegate.getTokens(result, email, shouldRecoverAuth);
         break;
 
-      case METHOD_SIGN_OUT:
+      case "signOut":
         delegate.signOut(result);
         break;
 
-      case METHOD_CLEAR_AUTH_CACHE:
+      case "clearAuthCache":
         String token = Objects.requireNonNull(call.argument("token"));
         delegate.clearAuthCache(result, token);
         break;
 
-      case METHOD_DISCONNECT:
+      case "disconnect":
         delegate.disconnect(result);
         break;
 
-      case METHOD_IS_SIGNED_IN:
+      case "isSignedIn":
         delegate.isSignedIn(result);
         break;
 
-      case METHOD_REQUEST_SCOPES:
+      case "requestScopes":
         List<String> scopes = Objects.requireNonNull(call.argument("scopes"));
         delegate.requestScopes(result, scopes);
         break;
@@ -206,7 +200,7 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act
   public interface IDelegate {
     /** Initializes this delegate so that it is ready to perform other operations. */
     void init(
-        @NonNull Result result,
+        @NonNull MethodChannel.Result result,
         @NonNull String signInOption,
         @NonNull List<String> requestedScopes,
         @Nullable String hostedDomain,
@@ -218,13 +212,13 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act
      * Returns the account information for the user who is signed in to this app. If no user is
      * signed in, tries to sign the user in without displaying any user interface.
      */
-    void signInSilently(@NonNull Result result);
+    void signInSilently(@NonNull MethodChannel.Result result);
 
     /**
      * Signs the user in via the sign-in user interface, including the OAuth consent flow if scopes
      * were requested.
      */
-    void signIn(@NonNull Result result);
+    void signIn(@NonNull MethodChannel.Result result);
 
     /**
      * Gets an OAuth access token with the scopes that were specified during initialization for the
@@ -234,28 +228,97 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act
      * complete, the method will attempt to recover authentication and rerun method.
      */
     void getTokens(
-        final @NonNull Result result, final @NonNull String email, final boolean shouldRecoverAuth);
+        final @NonNull MethodChannel.Result result,
+        final @NonNull String email,
+        final boolean shouldRecoverAuth);
 
     /**
      * Clears the token from any client cache forcing the next {@link #getTokens} call to fetch a
      * new one.
      */
-    void clearAuthCache(final @NonNull Result result, final @NonNull String token);
+    void clearAuthCache(final @NonNull MethodChannel.Result result, final @NonNull String token);
 
     /**
      * Signs the user out. Their credentials may remain valid, meaning they'll be able to silently
      * sign back in.
      */
-    void signOut(@NonNull Result result);
+    void signOut(@NonNull MethodChannel.Result result);
 
     /** Signs the user out, and revokes their credentials. */
-    void disconnect(@NonNull Result result);
+    void disconnect(@NonNull MethodChannel.Result result);
 
     /** Checks if there is a signed in user. */
-    void isSignedIn(@NonNull Result result);
+    void isSignedIn(@NonNull MethodChannel.Result result);
 
     /** Prompts the user to grant an additional Oauth scopes. */
-    void requestScopes(final @NonNull Result result, final @NonNull List<String> scopes);
+    void requestScopes(
+        final @NonNull MethodChannel.Result result, final @NonNull List<String> scopes);
+  }
+
+  /**
+   * Helper class for supporting the legacy IDelegate interface based on raw method channels, which
+   * handles converting any FlutterErrors (or other {@code Throwable}s in case any non- FlutterError
+   * exceptions slip through) thrown by the new code paths into {@code error} callbacks.
+   *
+   * @param <T> The Result type of the result to convert from.
+   */
+  private abstract static class ErrorConvertingMethodChannelResult<T>
+      implements Messages.Result<T> {
+    final @NonNull MethodChannel.Result result;
+
+    public ErrorConvertingMethodChannelResult(@NonNull MethodChannel.Result result) {
+      this.result = result;
+    }
+
+    @Override
+    public void error(@NonNull Throwable error) {
+      if (error instanceof FlutterError) {
+        FlutterError flutterError = (FlutterError) error;
+        result.error(flutterError.code, flutterError.getMessage(), flutterError.details);
+      } else {
+        result.error("exception", error.getMessage(), null);
+      }
+    }
+  }
+
+  /**
+   * Helper class for supporting the legacy IDelegate interface based on raw method channels, which
+   * handles converting responses from methods that return {@code Messages.UserData}.
+   */
+  private static class UserDataMethodChannelResult
+      extends ErrorConvertingMethodChannelResult<Messages.UserData> {
+    public UserDataMethodChannelResult(MethodChannel.Result result) {
+      super(result);
+    }
+
+    @Override
+    public void success(Messages.UserData data) {
+      Map<String, Object> response = new HashMap<>();
+      response.put("email", data.getEmail());
+      response.put("id", data.getId());
+      response.put("idToken", data.getIdToken());
+      response.put("serverAuthCode", data.getServerAuthCode());
+      response.put("displayName", data.getDisplayName());
+      if (data.getPhotoUrl() != null) {
+        response.put("photoUrl", data.getPhotoUrl());
+      }
+      result.success(response);
+    }
+  }
+
+  /**
+   * Helper class for supporting the legacy IDelegate interface based on raw method channels, which
+   * handles converting responses from methods that return {@code Void}.
+   */
+  private static class VoidMethodChannelResult extends ErrorConvertingMethodChannelResult<Void> {
+    public VoidMethodChannelResult(MethodChannel.Result result) {
+      super(result);
+    }
+
+    @Override
+    public void success(Void unused) {
+      result.success(null);
+    }
   }
 
   /**
@@ -263,11 +326,16 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act
    * class for use in other plugins that wrap basic sign-in functionality.
    *
    * <p>All methods in this class assume that they are run to completion before any other method is
-   * invoked. In this context, "run to completion" means that their {@link Result} argument has been
-   * completed (either successfully or in error). This class provides no synchronization constructs
-   * to guarantee such behavior; callers are responsible for providing such guarantees.
+   * invoked. In this context, "run to completion" means that their {@link MethodChannel.Result}
+   * argument has been completed (either successfully or in error). This class provides no
+   * synchronization constructs to guarantee such behavior; callers are responsible for providing
+   * such guarantees.
    */
-  public static class Delegate implements IDelegate, PluginRegistry.ActivityResultListener {
+  // TODO(stuartmorgan): Remove this in a breaking change, replacing it with something using
+  // structured types rather than strings and dictionaries left over from the pre-Pigeon method
+  // channel implementation.
+  public static class Delegate
+      implements IDelegate, PluginRegistry.ActivityResultListener, GoogleSignInApi {
     private static final int REQUEST_CODE_SIGNIN = 53293;
     private static final int REQUEST_CODE_RECOVER_AUTH = 53294;
     @VisibleForTesting static final int REQUEST_CODE_REQUEST_SCOPE = 53295;
@@ -291,6 +359,7 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act
     private PluginRegistry.Registrar registrar;
     // Only set activity for v2 embedder. Always access activity from getActivity() method.
     private @Nullable Activity activity;
+    // TODO(stuartmorgan): See whether this can be replaced with background channels.
     private final BackgroundTaskRunner backgroundTaskRunner = new BackgroundTaskRunner(1);
     private final GoogleSignInWrapper googleSignInWrapper;
 
@@ -318,16 +387,44 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act
       return registrar != null ? registrar.activity() : activity;
     }
 
-    private void checkAndSetPendingOperation(String method, Result result) {
-      checkAndSetPendingOperation(method, result, null);
-    }
-
-    private void checkAndSetPendingOperation(String method, Result result, Object data) {
+    private void checkAndSetPendingOperation(
+        String method,
+        Messages.Result<Messages.UserData> userDataResult,
+        Messages.Result<Void> voidResult,
+        Messages.Result<Boolean> boolResult,
+        Messages.Result<String> stringResult,
+        Object data) {
       if (pendingOperation != null) {
         throw new IllegalStateException(
             "Concurrent operations detected: " + pendingOperation.method + ", " + method);
       }
-      pendingOperation = new PendingOperation(method, result, data);
+      pendingOperation =
+          new PendingOperation(method, userDataResult, voidResult, boolResult, stringResult, data);
+    }
+
+    private void checkAndSetPendingSignInOperation(
+        String method, @NonNull Messages.Result<Messages.UserData> result) {
+      checkAndSetPendingOperation(method, result, null, null, null, null);
+    }
+
+    private void checkAndSetPendingVoidOperation(
+        String method, @NonNull Messages.Result<Void> result) {
+      checkAndSetPendingOperation(method, null, result, null, null, null);
+    }
+
+    private void checkAndSetPendingBoolOperation(
+        String method, @NonNull Messages.Result<Boolean> result) {
+      checkAndSetPendingOperation(method, null, null, result, null, null);
+    }
+
+    private void checkAndSetPendingStringOperation(
+        String method, @NonNull Messages.Result<String> result, @Nullable Object data) {
+      checkAndSetPendingOperation(method, null, null, null, result, data);
+    }
+
+    private void checkAndSetPendingAccessTokenOperation(
+        String method, Messages.Result<String> result, @NonNull Object data) {
+      checkAndSetPendingStringOperation(method, result, data);
     }
 
     /**
@@ -335,23 +432,16 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act
      * guarantees that this will be called and completed before any other methods are invoked.
      */
     @Override
-    public void init(
-        @NonNull Result result,
-        @NonNull String signInOption,
-        @NonNull List<String> requestedScopes,
-        @Nullable String hostedDomain,
-        @Nullable String clientId,
-        @Nullable String serverClientId,
-        boolean forceCodeForRefreshToken) {
+    public void init(@NonNull Messages.InitParams params) {
       try {
         GoogleSignInOptions.Builder optionsBuilder;
 
-        switch (signInOption) {
-          case DEFAULT_GAMES_SIGN_IN:
+        switch (params.getSignInType()) {
+          case GAMES:
             optionsBuilder =
                 new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN);
             break;
-          case DEFAULT_SIGN_IN:
+          case STANDARD:
             optionsBuilder =
                 new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN).requestEmail();
             break;
@@ -363,12 +453,13 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act
         // Android apps are identified by their package name and the SHA-1 of their signing key.
         // https://developers.google.com/android/guides/client-auth
         // https://developers.google.com/identity/sign-in/android/start#configure-a-google-api-project
-        if (!Strings.isNullOrEmpty(clientId) && Strings.isNullOrEmpty(serverClientId)) {
+        String serverClientId = params.getServerClientId();
+        if (!Strings.isNullOrEmpty(params.getClientId()) && Strings.isNullOrEmpty(serverClientId)) {
           Log.w(
               "google_sign_in",
               "clientId is not supported on Android and is interpreted as serverClientId. "
                   + "Use serverClientId instead to suppress this warning.");
-          serverClientId = clientId;
+          serverClientId = params.getClientId();
         }
 
         if (Strings.isNullOrEmpty(serverClientId)) {
@@ -387,20 +478,57 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act
         }
         if (!Strings.isNullOrEmpty(serverClientId)) {
           optionsBuilder.requestIdToken(serverClientId);
-          optionsBuilder.requestServerAuthCode(serverClientId, forceCodeForRefreshToken);
+          optionsBuilder.requestServerAuthCode(
+              serverClientId, params.getForceCodeForRefreshToken());
         }
+        requestedScopes = params.getScopes();
         for (String scope : requestedScopes) {
           optionsBuilder.requestScopes(new Scope(scope));
         }
-        if (!Strings.isNullOrEmpty(hostedDomain)) {
-          optionsBuilder.setHostedDomain(hostedDomain);
+        if (!Strings.isNullOrEmpty(params.getHostedDomain())) {
+          optionsBuilder.setHostedDomain(params.getHostedDomain());
         }
 
-        this.requestedScopes = requestedScopes;
         signInClient = googleSignInWrapper.getClient(context, optionsBuilder.build());
-        result.success(null);
       } catch (Exception e) {
-        result.error(ERROR_REASON_EXCEPTION, e.getMessage(), null);
+        throw new FlutterError(ERROR_REASON_EXCEPTION, e.getMessage(), null);
+      }
+    }
+
+    // IDelegate version, for backwards compatibility.
+    @Override
+    public void init(
+        @NonNull MethodChannel.Result result,
+        @NonNull String signInOption,
+        @NonNull List<String> requestedScopes,
+        @Nullable String hostedDomain,
+        @Nullable String clientId,
+        @Nullable String serverClientId,
+        boolean forceCodeForRefreshToken) {
+      try {
+        Messages.SignInType type;
+        switch (signInOption) {
+          case DEFAULT_GAMES_SIGN_IN:
+            type = Messages.SignInType.GAMES;
+            break;
+          case DEFAULT_SIGN_IN:
+            type = Messages.SignInType.STANDARD;
+            break;
+          default:
+            throw new IllegalStateException("Unknown signInOption");
+        }
+        init(
+            new Messages.InitParams.Builder()
+                .setSignInType(type)
+                .setScopes(requestedScopes)
+                .setHostedDomain(hostedDomain)
+                .setClientId(clientId)
+                .setServerClientId(serverClientId)
+                .setForceCodeForRefreshToken(forceCodeForRefreshToken)
+                .build());
+        result.success(null);
+      } catch (FlutterError e) {
+        result.error(e.code, e.getMessage(), e.details);
       }
     }
 
@@ -409,8 +537,8 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act
      * signed in, tries to sign the user in without displaying any user interface.
      */
     @Override
-    public void signInSilently(@NonNull Result result) {
-      checkAndSetPendingOperation(METHOD_SIGN_IN_SILENTLY, result);
+    public void signInSilently(@NonNull Messages.Result<Messages.UserData> result) {
+      checkAndSetPendingSignInOperation("signInSilently", result);
       Task<GoogleSignInAccount> task = signInClient.silentSignIn();
       if (task.isComplete()) {
         // There's immediate result available.
@@ -420,68 +548,99 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act
       }
     }
 
+    // IDelegate version, for backwards compatibility.
+    @Override
+    public void signInSilently(@NonNull MethodChannel.Result result) {
+      signInSilently(new UserDataMethodChannelResult(result));
+    }
+
     /**
      * Signs the user in via the sign-in user interface, including the OAuth consent flow if scopes
      * were requested.
      */
     @Override
-    public void signIn(@NonNull Result result) {
+    public void signIn(@NonNull Messages.Result<Messages.UserData> result) {
       if (getActivity() == null) {
         throw new IllegalStateException("signIn needs a foreground activity");
       }
-      checkAndSetPendingOperation(METHOD_SIGN_IN, result);
+      checkAndSetPendingSignInOperation("signIn", result);
 
       Intent signInIntent = signInClient.getSignInIntent();
       getActivity().startActivityForResult(signInIntent, REQUEST_CODE_SIGNIN);
     }
 
+    // IDelegate version, for backwards compatibility.
+    @Override
+    public void signIn(@NonNull MethodChannel.Result result) {
+      signIn(new UserDataMethodChannelResult(result));
+    }
+
     /**
      * Signs the user out. Their credentials may remain valid, meaning they'll be able to silently
      * sign back in.
      */
     @Override
-    public void signOut(@NonNull Result result) {
-      checkAndSetPendingOperation(METHOD_SIGN_OUT, result);
+    public void signOut(@NonNull Messages.Result<Void> result) {
+      checkAndSetPendingVoidOperation("signOut", result);
 
       signInClient
           .signOut()
           .addOnCompleteListener(
               task -> {
                 if (task.isSuccessful()) {
-                  finishWithSuccess(null);
+                  finishWithSuccess();
                 } else {
                   finishWithError(ERROR_REASON_STATUS, "Failed to signout.");
                 }
               });
     }
 
+    // IDelegate version, for backwards compatibility.
+    @Override
+    public void signOut(@NonNull MethodChannel.Result result) {
+      signOut(new VoidMethodChannelResult(result));
+    }
+
     /** Signs the user out, and revokes their credentials. */
     @Override
-    public void disconnect(@NonNull Result result) {
-      checkAndSetPendingOperation(METHOD_DISCONNECT, result);
+    public void disconnect(@NonNull Messages.Result<Void> result) {
+      checkAndSetPendingVoidOperation("disconnect", result);
 
       signInClient
           .revokeAccess()
           .addOnCompleteListener(
               task -> {
                 if (task.isSuccessful()) {
-                  finishWithSuccess(null);
+                  finishWithSuccess();
                 } else {
                   finishWithError(ERROR_REASON_STATUS, "Failed to disconnect.");
                 }
               });
     }
 
-    /** Checks if there is a signed in user. */
+    // IDelegate version, for backwards compatibility.
     @Override
-    public void isSignedIn(final @NonNull Result result) {
-      boolean value = GoogleSignIn.getLastSignedInAccount(context) != null;
-      result.success(value);
+    public void disconnect(@NonNull MethodChannel.Result result) {
+      signOut(new VoidMethodChannelResult(result));
+    }
+
+    /** Checks if there is a signed in user. */
+    @NonNull
+    @Override
+    public Boolean isSignedIn() {
+      return GoogleSignIn.getLastSignedInAccount(context) != null;
+    }
+
+    // IDelegate version, for backwards compatibility.
+    @Override
+    public void isSignedIn(final @NonNull MethodChannel.Result result) {
+      result.success(isSignedIn());
     }
 
     @Override
-    public void requestScopes(@NonNull Result result, @NonNull List<String> scopes) {
-      checkAndSetPendingOperation(METHOD_REQUEST_SCOPES, result);
+    public void requestScopes(
+        @NonNull List<String> scopes, @NonNull Messages.Result<Boolean> result) {
+      checkAndSetPendingBoolOperation("requestScopes", result);
 
       GoogleSignInAccount account = googleSignInWrapper.getLastSignedInAccount(context);
       if (account == null) {
@@ -499,7 +658,7 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act
       }
 
       if (wrappedScopes.isEmpty()) {
-        finishWithSuccess(true);
+        finishWithBoolean(true);
         return;
       }
 
@@ -507,6 +666,19 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act
           getActivity(), REQUEST_CODE_REQUEST_SCOPE, account, wrappedScopes.toArray(new Scope[0]));
     }
 
+    // IDelegate version, for backwards compatibility.
+    @Override
+    public void requestScopes(@NonNull MethodChannel.Result result, @NonNull List<String> scopes) {
+      requestScopes(
+          scopes,
+          new ErrorConvertingMethodChannelResult<Boolean>(result) {
+            @Override
+            public void success(Boolean value) {
+              result.success(value);
+            }
+          });
+    }
+
     private void onSignInResult(Task<GoogleSignInAccount> completedTask) {
       try {
         GoogleSignInAccount account = completedTask.getResult(ApiException.class);
@@ -521,16 +693,21 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act
     }
 
     private void onSignInAccount(GoogleSignInAccount account) {
-      Map<String, Object> response = new HashMap<>();
-      response.put("email", account.getEmail());
-      response.put("id", account.getId());
-      response.put("idToken", account.getIdToken());
-      response.put("serverAuthCode", account.getServerAuthCode());
-      response.put("displayName", account.getDisplayName());
+      final Messages.UserData.Builder builder =
+          new Messages.UserData.Builder()
+              // TODO(stuartmorgan): Test with games sign-in; according to docs these could be null
+              // as the games login request is currently constructed, but the public Dart API
+              // assumes they are non-null, so the sign-in query may need to change to
+              // include requestEmail() and requestProfile().
+              .setEmail(account.getEmail())
+              .setId(account.getId())
+              .setIdToken(account.getIdToken())
+              .setServerAuthCode(account.getServerAuthCode())
+              .setDisplayName(account.getDisplayName());
       if (account.getPhotoUrl() != null) {
-        response.put("photoUrl", account.getPhotoUrl().toString());
+        builder.setPhotoUrl(account.getPhotoUrl().toString());
       }
-      finishWithSuccess(response);
+      finishWithUserData(builder.build());
     }
 
     private String errorCodeForStatus(int statusCode) {
@@ -550,31 +727,67 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act
       }
     }
 
-    private void finishWithSuccess(Object data) {
-      pendingOperation.result.success(data);
+    private void finishWithSuccess() {
+      Objects.requireNonNull(pendingOperation.voidResult).success(null);
+      pendingOperation = null;
+    }
+
+    private void finishWithBoolean(Boolean value) {
+      Objects.requireNonNull(pendingOperation.boolResult).success(value);
+      pendingOperation = null;
+    }
+
+    private void finishWithUserData(Messages.UserData data) {
+      Objects.requireNonNull(pendingOperation.userDataResult).success(data);
       pendingOperation = null;
     }
 
     private void finishWithError(String errorCode, String errorMessage) {
-      pendingOperation.result.error(errorCode, errorMessage, null);
+      Messages.Result<?> result;
+      if (pendingOperation.userDataResult != null) {
+        result = pendingOperation.userDataResult;
+      } else if (pendingOperation.boolResult != null) {
+        result = pendingOperation.boolResult;
+      } else if (pendingOperation.stringResult != null) {
+        result = pendingOperation.stringResult;
+      } else {
+        result = pendingOperation.voidResult;
+      }
+      Objects.requireNonNull(result).error(new FlutterError(errorCode, errorMessage, null));
       pendingOperation = null;
     }
 
     private static class PendingOperation {
-      final String method;
-      final Result result;
-      final Object data;
+      final @NonNull String method;
+      final @Nullable Messages.Result<Messages.UserData> userDataResult;
+      final @Nullable Messages.Result<Void> voidResult;
+      final @Nullable Messages.Result<Boolean> boolResult;
+      final @Nullable Messages.Result<String> stringResult;
+      final @Nullable Object data;
 
-      PendingOperation(String method, Result result, Object data) {
+      PendingOperation(
+          @NonNull String method,
+          @Nullable Messages.Result<Messages.UserData> userDataResult,
+          @Nullable Messages.Result<Void> voidResult,
+          @Nullable Messages.Result<Boolean> boolResult,
+          @Nullable Messages.Result<String> stringResult,
+          @Nullable Object data) {
+        assert (userDataResult != null
+            || voidResult != null
+            || boolResult != null
+            || stringResult != null);
         this.method = method;
-        this.result = result;
+        this.userDataResult = userDataResult;
+        this.voidResult = voidResult;
+        this.boolResult = boolResult;
+        this.stringResult = stringResult;
         this.data = data;
       }
     }
 
     /** Clears the token kept in the client side cache. */
     @Override
-    public void clearAuthCache(final @NonNull Result result, final @NonNull String token) {
+    public void clearAuthCache(@NonNull String token, @NonNull Messages.Result<Void> result) {
       Callable<Void> clearTokenTask =
           () -> {
             GoogleAuthUtil.clearToken(context, token);
@@ -588,14 +801,23 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act
               result.success(clearTokenFuture.get());
             } catch (ExecutionException e) {
               @Nullable Throwable cause = e.getCause();
-              result.error(ERROR_REASON_EXCEPTION, cause == null ? null : cause.getMessage(), null);
+              result.error(
+                  new FlutterError(
+                      ERROR_REASON_EXCEPTION, cause == null ? null : cause.getMessage(), null));
             } catch (InterruptedException e) {
-              result.error(ERROR_REASON_EXCEPTION, e.getMessage(), null);
+              result.error(new FlutterError(ERROR_REASON_EXCEPTION, e.getMessage(), null));
               Thread.currentThread().interrupt();
             }
           });
     }
 
+    // IDelegate version, for backwards compatibility.
+    @Override
+    public void clearAuthCache(
+        final @NonNull MethodChannel.Result result, final @NonNull String token) {
+      clearAuthCache(token, new VoidMethodChannelResult(result));
+    }
+
     /**
      * Gets an OAuth access token with the scopes that were specified during initialization for the
      * user with the specified email address.
@@ -604,10 +826,10 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act
      * complete, the method will attempt to recover authentication and rerun method.
      */
     @Override
-    public void getTokens(
-        @NonNull final Result result,
-        @NonNull final String email,
-        final boolean shouldRecoverAuth) {
+    public void getAccessToken(
+        @NonNull String email,
+        @NonNull Boolean shouldRecoverAuth,
+        @NonNull Messages.Result<String> result) {
       Callable<String> getTokenTask =
           () -> {
             Account account = new Account(email, "com.google");
@@ -622,41 +844,60 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act
           getTokenTask,
           tokenFuture -> {
             try {
-              String token = tokenFuture.get();
-              HashMap<String, String> tokenResult = new HashMap<>();
-              tokenResult.put("accessToken", token);
-              result.success(tokenResult);
+              result.success(tokenFuture.get());
             } catch (ExecutionException e) {
               if (e.getCause() instanceof UserRecoverableAuthException) {
                 if (shouldRecoverAuth && pendingOperation == null) {
                   Activity activity = getActivity();
                   if (activity == null) {
                     result.error(
-                        ERROR_USER_RECOVERABLE_AUTH,
-                        "Cannot recover auth because app is not in foreground. "
-                            + e.getLocalizedMessage(),
-                        null);
+                        new FlutterError(
+                            ERROR_USER_RECOVERABLE_AUTH,
+                            "Cannot recover auth because app is not in foreground. "
+                                + e.getLocalizedMessage(),
+                            null));
                   } else {
-                    checkAndSetPendingOperation(METHOD_GET_TOKENS, result, email);
+                    checkAndSetPendingAccessTokenOperation("getTokens", result, email);
                     Intent recoveryIntent =
                         ((UserRecoverableAuthException) e.getCause()).getIntent();
                     activity.startActivityForResult(recoveryIntent, REQUEST_CODE_RECOVER_AUTH);
                   }
                 } else {
-                  result.error(ERROR_USER_RECOVERABLE_AUTH, e.getLocalizedMessage(), null);
+                  result.error(
+                      new FlutterError(ERROR_USER_RECOVERABLE_AUTH, e.getLocalizedMessage(), null));
                 }
               } else {
                 @Nullable Throwable cause = e.getCause();
                 result.error(
-                    ERROR_REASON_EXCEPTION, cause == null ? null : cause.getMessage(), null);
+                    new FlutterError(
+                        ERROR_REASON_EXCEPTION, cause == null ? null : cause.getMessage(), null));
               }
             } catch (InterruptedException e) {
-              result.error(ERROR_REASON_EXCEPTION, e.getMessage(), null);
+              result.error(new FlutterError(ERROR_REASON_EXCEPTION, e.getMessage(), null));
               Thread.currentThread().interrupt();
             }
           });
     }
 
+    // IDelegate version, for backwards compatibility.
+    @Override
+    public void getTokens(
+        @NonNull final MethodChannel.Result result,
+        @NonNull final String email,
+        final boolean shouldRecoverAuth) {
+      getAccessToken(
+          email,
+          shouldRecoverAuth,
+          new ErrorConvertingMethodChannelResult<String>(result) {
+            @Override
+            public void success(String value) {
+              HashMap<String, String> tokenResult = new HashMap<>();
+              tokenResult.put("accessToken", value);
+              result.success(tokenResult);
+            }
+          });
+    }
+
     @Override
     public boolean onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
       if (pendingOperation == null) {
@@ -666,10 +907,10 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act
         case REQUEST_CODE_RECOVER_AUTH:
           if (resultCode == Activity.RESULT_OK) {
             // Recover the previous result and data and attempt to get tokens again.
-            Result result = pendingOperation.result;
-            String email = (String) pendingOperation.data;
+            Messages.Result<String> result = Objects.requireNonNull(pendingOperation.stringResult);
+            String email = (String) Objects.requireNonNull(pendingOperation.data);
             pendingOperation = null;
-            getTokens(result, email, false);
+            getAccessToken(email, false, result);
           } else {
             finishWithError(
                 ERROR_FAILURE_TO_RECOVER_AUTH, "Failed attempt to recover authentication");
@@ -686,7 +927,7 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act
           }
           return true;
         case REQUEST_CODE_REQUEST_SCOPE:
-          finishWithSuccess(resultCode == Activity.RESULT_OK);
+          finishWithBoolean(resultCode == Activity.RESULT_OK);
           return true;
         default:
           return false;
diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/Messages.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/Messages.java
new file mode 100644
index 0000000000..aa94bbbf72
--- /dev/null
+++ b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/Messages.java
@@ -0,0 +1,712 @@
+// 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.
+// Autogenerated from Pigeon (v10.1.0), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+
+package io.flutter.plugins.googlesignin;
+
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import io.flutter.plugin.common.BasicMessageChannel;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.MessageCodec;
+import io.flutter.plugin.common.StandardMessageCodec;
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Generated class from Pigeon. */
+@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"})
+public class Messages {
+
+  /** Error class for passing custom error details to Flutter via a thrown PlatformException. */
+  public static class FlutterError extends RuntimeException {
+
+    /** The error code. */
+    public final String code;
+
+    /** The error details. Must be a datatype supported by the api codec. */
+    public final Object details;
+
+    public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details) {
+      super(message);
+      this.code = code;
+      this.details = details;
+    }
+  }
+
+  @NonNull
+  protected static ArrayList<Object> wrapError(@NonNull Throwable exception) {
+    ArrayList<Object> errorList = new ArrayList<Object>(3);
+    if (exception instanceof FlutterError) {
+      FlutterError error = (FlutterError) exception;
+      errorList.add(error.code);
+      errorList.add(error.getMessage());
+      errorList.add(error.details);
+    } else {
+      errorList.add(exception.toString());
+      errorList.add(exception.getClass().getSimpleName());
+      errorList.add(
+          "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception));
+    }
+    return errorList;
+  }
+
+  /** Pigeon version of SignInOption. */
+  public enum SignInType {
+    /** Default configuration. */
+    STANDARD(0),
+    /** Recommended configuration for game sign in. */
+    GAMES(1);
+
+    final int index;
+
+    private SignInType(final int index) {
+      this.index = index;
+    }
+  }
+
+  /**
+   * Pigeon version of SignInInitParams.
+   *
+   * <p>See SignInInitParams for details.
+   *
+   * <p>Generated class from Pigeon that represents data sent in messages.
+   */
+  public static final class InitParams {
+    private @NonNull List<String> scopes;
+
+    public @NonNull List<String> getScopes() {
+      return scopes;
+    }
+
+    public void setScopes(@NonNull List<String> setterArg) {
+      if (setterArg == null) {
+        throw new IllegalStateException("Nonnull field \"scopes\" is null.");
+      }
+      this.scopes = setterArg;
+    }
+
+    private @NonNull SignInType signInType;
+
+    public @NonNull SignInType getSignInType() {
+      return signInType;
+    }
+
+    public void setSignInType(@NonNull SignInType setterArg) {
+      if (setterArg == null) {
+        throw new IllegalStateException("Nonnull field \"signInType\" is null.");
+      }
+      this.signInType = setterArg;
+    }
+
+    private @Nullable String hostedDomain;
+
+    public @Nullable String getHostedDomain() {
+      return hostedDomain;
+    }
+
+    public void setHostedDomain(@Nullable String setterArg) {
+      this.hostedDomain = setterArg;
+    }
+
+    private @Nullable String clientId;
+
+    public @Nullable String getClientId() {
+      return clientId;
+    }
+
+    public void setClientId(@Nullable String setterArg) {
+      this.clientId = setterArg;
+    }
+
+    private @Nullable String serverClientId;
+
+    public @Nullable String getServerClientId() {
+      return serverClientId;
+    }
+
+    public void setServerClientId(@Nullable String setterArg) {
+      this.serverClientId = setterArg;
+    }
+
+    private @NonNull Boolean forceCodeForRefreshToken;
+
+    public @NonNull Boolean getForceCodeForRefreshToken() {
+      return forceCodeForRefreshToken;
+    }
+
+    public void setForceCodeForRefreshToken(@NonNull Boolean setterArg) {
+      if (setterArg == null) {
+        throw new IllegalStateException("Nonnull field \"forceCodeForRefreshToken\" is null.");
+      }
+      this.forceCodeForRefreshToken = setterArg;
+    }
+
+    /** Constructor is non-public to enforce null safety; use Builder. */
+    InitParams() {}
+
+    public static final class Builder {
+
+      private @Nullable List<String> scopes;
+
+      public @NonNull Builder setScopes(@NonNull List<String> setterArg) {
+        this.scopes = setterArg;
+        return this;
+      }
+
+      private @Nullable SignInType signInType;
+
+      public @NonNull Builder setSignInType(@NonNull SignInType setterArg) {
+        this.signInType = setterArg;
+        return this;
+      }
+
+      private @Nullable String hostedDomain;
+
+      public @NonNull Builder setHostedDomain(@Nullable String setterArg) {
+        this.hostedDomain = setterArg;
+        return this;
+      }
+
+      private @Nullable String clientId;
+
+      public @NonNull Builder setClientId(@Nullable String setterArg) {
+        this.clientId = setterArg;
+        return this;
+      }
+
+      private @Nullable String serverClientId;
+
+      public @NonNull Builder setServerClientId(@Nullable String setterArg) {
+        this.serverClientId = setterArg;
+        return this;
+      }
+
+      private @Nullable Boolean forceCodeForRefreshToken;
+
+      public @NonNull Builder setForceCodeForRefreshToken(@NonNull Boolean setterArg) {
+        this.forceCodeForRefreshToken = setterArg;
+        return this;
+      }
+
+      public @NonNull InitParams build() {
+        InitParams pigeonReturn = new InitParams();
+        pigeonReturn.setScopes(scopes);
+        pigeonReturn.setSignInType(signInType);
+        pigeonReturn.setHostedDomain(hostedDomain);
+        pigeonReturn.setClientId(clientId);
+        pigeonReturn.setServerClientId(serverClientId);
+        pigeonReturn.setForceCodeForRefreshToken(forceCodeForRefreshToken);
+        return pigeonReturn;
+      }
+    }
+
+    @NonNull
+    ArrayList<Object> toList() {
+      ArrayList<Object> toListResult = new ArrayList<Object>(6);
+      toListResult.add(scopes);
+      toListResult.add(signInType == null ? null : signInType.index);
+      toListResult.add(hostedDomain);
+      toListResult.add(clientId);
+      toListResult.add(serverClientId);
+      toListResult.add(forceCodeForRefreshToken);
+      return toListResult;
+    }
+
+    static @NonNull InitParams fromList(@NonNull ArrayList<Object> list) {
+      InitParams pigeonResult = new InitParams();
+      Object scopes = list.get(0);
+      pigeonResult.setScopes((List<String>) scopes);
+      Object signInType = list.get(1);
+      pigeonResult.setSignInType(signInType == null ? null : SignInType.values()[(int) signInType]);
+      Object hostedDomain = list.get(2);
+      pigeonResult.setHostedDomain((String) hostedDomain);
+      Object clientId = list.get(3);
+      pigeonResult.setClientId((String) clientId);
+      Object serverClientId = list.get(4);
+      pigeonResult.setServerClientId((String) serverClientId);
+      Object forceCodeForRefreshToken = list.get(5);
+      pigeonResult.setForceCodeForRefreshToken((Boolean) forceCodeForRefreshToken);
+      return pigeonResult;
+    }
+  }
+
+  /**
+   * Pigeon version of GoogleSignInUserData.
+   *
+   * <p>See GoogleSignInUserData for details.
+   *
+   * <p>Generated class from Pigeon that represents data sent in messages.
+   */
+  public static final class UserData {
+    private @Nullable String displayName;
+
+    public @Nullable String getDisplayName() {
+      return displayName;
+    }
+
+    public void setDisplayName(@Nullable String setterArg) {
+      this.displayName = setterArg;
+    }
+
+    private @NonNull String email;
+
+    public @NonNull String getEmail() {
+      return email;
+    }
+
+    public void setEmail(@NonNull String setterArg) {
+      if (setterArg == null) {
+        throw new IllegalStateException("Nonnull field \"email\" is null.");
+      }
+      this.email = setterArg;
+    }
+
+    private @NonNull String id;
+
+    public @NonNull String getId() {
+      return id;
+    }
+
+    public void setId(@NonNull String setterArg) {
+      if (setterArg == null) {
+        throw new IllegalStateException("Nonnull field \"id\" is null.");
+      }
+      this.id = setterArg;
+    }
+
+    private @Nullable String photoUrl;
+
+    public @Nullable String getPhotoUrl() {
+      return photoUrl;
+    }
+
+    public void setPhotoUrl(@Nullable String setterArg) {
+      this.photoUrl = setterArg;
+    }
+
+    private @Nullable String idToken;
+
+    public @Nullable String getIdToken() {
+      return idToken;
+    }
+
+    public void setIdToken(@Nullable String setterArg) {
+      this.idToken = setterArg;
+    }
+
+    private @Nullable String serverAuthCode;
+
+    public @Nullable String getServerAuthCode() {
+      return serverAuthCode;
+    }
+
+    public void setServerAuthCode(@Nullable String setterArg) {
+      this.serverAuthCode = setterArg;
+    }
+
+    /** Constructor is non-public to enforce null safety; use Builder. */
+    UserData() {}
+
+    public static final class Builder {
+
+      private @Nullable String displayName;
+
+      public @NonNull Builder setDisplayName(@Nullable String setterArg) {
+        this.displayName = setterArg;
+        return this;
+      }
+
+      private @Nullable String email;
+
+      public @NonNull Builder setEmail(@NonNull String setterArg) {
+        this.email = setterArg;
+        return this;
+      }
+
+      private @Nullable String id;
+
+      public @NonNull Builder setId(@NonNull String setterArg) {
+        this.id = setterArg;
+        return this;
+      }
+
+      private @Nullable String photoUrl;
+
+      public @NonNull Builder setPhotoUrl(@Nullable String setterArg) {
+        this.photoUrl = setterArg;
+        return this;
+      }
+
+      private @Nullable String idToken;
+
+      public @NonNull Builder setIdToken(@Nullable String setterArg) {
+        this.idToken = setterArg;
+        return this;
+      }
+
+      private @Nullable String serverAuthCode;
+
+      public @NonNull Builder setServerAuthCode(@Nullable String setterArg) {
+        this.serverAuthCode = setterArg;
+        return this;
+      }
+
+      public @NonNull UserData build() {
+        UserData pigeonReturn = new UserData();
+        pigeonReturn.setDisplayName(displayName);
+        pigeonReturn.setEmail(email);
+        pigeonReturn.setId(id);
+        pigeonReturn.setPhotoUrl(photoUrl);
+        pigeonReturn.setIdToken(idToken);
+        pigeonReturn.setServerAuthCode(serverAuthCode);
+        return pigeonReturn;
+      }
+    }
+
+    @NonNull
+    ArrayList<Object> toList() {
+      ArrayList<Object> toListResult = new ArrayList<Object>(6);
+      toListResult.add(displayName);
+      toListResult.add(email);
+      toListResult.add(id);
+      toListResult.add(photoUrl);
+      toListResult.add(idToken);
+      toListResult.add(serverAuthCode);
+      return toListResult;
+    }
+
+    static @NonNull UserData fromList(@NonNull ArrayList<Object> list) {
+      UserData pigeonResult = new UserData();
+      Object displayName = list.get(0);
+      pigeonResult.setDisplayName((String) displayName);
+      Object email = list.get(1);
+      pigeonResult.setEmail((String) email);
+      Object id = list.get(2);
+      pigeonResult.setId((String) id);
+      Object photoUrl = list.get(3);
+      pigeonResult.setPhotoUrl((String) photoUrl);
+      Object idToken = list.get(4);
+      pigeonResult.setIdToken((String) idToken);
+      Object serverAuthCode = list.get(5);
+      pigeonResult.setServerAuthCode((String) serverAuthCode);
+      return pigeonResult;
+    }
+  }
+
+  public interface Result<T> {
+    @SuppressWarnings("UnknownNullness")
+    void success(T result);
+
+    void error(@NonNull Throwable error);
+  }
+
+  private static class GoogleSignInApiCodec extends StandardMessageCodec {
+    public static final GoogleSignInApiCodec INSTANCE = new GoogleSignInApiCodec();
+
+    private GoogleSignInApiCodec() {}
+
+    @Override
+    protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) {
+      switch (type) {
+        case (byte) 128:
+          return InitParams.fromList((ArrayList<Object>) readValue(buffer));
+        case (byte) 129:
+          return UserData.fromList((ArrayList<Object>) readValue(buffer));
+        default:
+          return super.readValueOfType(type, buffer);
+      }
+    }
+
+    @Override
+    protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) {
+      if (value instanceof InitParams) {
+        stream.write(128);
+        writeValue(stream, ((InitParams) value).toList());
+      } else if (value instanceof UserData) {
+        stream.write(129);
+        writeValue(stream, ((UserData) value).toList());
+      } else {
+        super.writeValue(stream, value);
+      }
+    }
+  }
+
+  /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
+  public interface GoogleSignInApi {
+    /** Initializes a sign in request with the given parameters. */
+    void init(@NonNull InitParams params);
+    /** Starts a silent sign in. */
+    void signInSilently(@NonNull Result<UserData> result);
+    /** Starts a sign in with user interaction. */
+    void signIn(@NonNull Result<UserData> result);
+    /** Requests the access token for the current sign in. */
+    void getAccessToken(
+        @NonNull String email, @NonNull Boolean shouldRecoverAuth, @NonNull Result<String> result);
+    /** Signs out the current user. */
+    void signOut(@NonNull Result<Void> result);
+    /** Revokes scope grants to the application. */
+    void disconnect(@NonNull Result<Void> result);
+    /** Returns whether the user is currently signed in. */
+    @NonNull
+    Boolean isSignedIn();
+    /** Clears the authentication caching for the given token, requiring a new sign in. */
+    void clearAuthCache(@NonNull String token, @NonNull Result<Void> result);
+    /** Requests access to the given scopes. */
+    void requestScopes(@NonNull List<String> scopes, @NonNull Result<Boolean> result);
+
+    /** The codec used by GoogleSignInApi. */
+    static @NonNull MessageCodec<Object> getCodec() {
+      return GoogleSignInApiCodec.INSTANCE;
+    }
+    /**
+     * Sets up an instance of `GoogleSignInApi` to handle messages through the `binaryMessenger`.
+     */
+    static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable GoogleSignInApi api) {
+      {
+        BasicMessageChannel<Object> channel =
+            new BasicMessageChannel<>(
+                binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.init", getCodec());
+        if (api != null) {
+          channel.setMessageHandler(
+              (message, reply) -> {
+                ArrayList<Object> wrapped = new ArrayList<Object>();
+                ArrayList<Object> args = (ArrayList<Object>) message;
+                InitParams paramsArg = (InitParams) args.get(0);
+                try {
+                  api.init(paramsArg);
+                  wrapped.add(0, null);
+                } catch (Throwable exception) {
+                  ArrayList<Object> wrappedError = wrapError(exception);
+                  wrapped = wrappedError;
+                }
+                reply.reply(wrapped);
+              });
+        } else {
+          channel.setMessageHandler(null);
+        }
+      }
+      {
+        BasicMessageChannel<Object> channel =
+            new BasicMessageChannel<>(
+                binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.signInSilently", getCodec());
+        if (api != null) {
+          channel.setMessageHandler(
+              (message, reply) -> {
+                ArrayList<Object> wrapped = new ArrayList<Object>();
+                Result<UserData> resultCallback =
+                    new Result<UserData>() {
+                      public void success(UserData result) {
+                        wrapped.add(0, result);
+                        reply.reply(wrapped);
+                      }
+
+                      public void error(Throwable error) {
+                        ArrayList<Object> wrappedError = wrapError(error);
+                        reply.reply(wrappedError);
+                      }
+                    };
+
+                api.signInSilently(resultCallback);
+              });
+        } else {
+          channel.setMessageHandler(null);
+        }
+      }
+      {
+        BasicMessageChannel<Object> channel =
+            new BasicMessageChannel<>(
+                binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.signIn", getCodec());
+        if (api != null) {
+          channel.setMessageHandler(
+              (message, reply) -> {
+                ArrayList<Object> wrapped = new ArrayList<Object>();
+                Result<UserData> resultCallback =
+                    new Result<UserData>() {
+                      public void success(UserData result) {
+                        wrapped.add(0, result);
+                        reply.reply(wrapped);
+                      }
+
+                      public void error(Throwable error) {
+                        ArrayList<Object> wrappedError = wrapError(error);
+                        reply.reply(wrappedError);
+                      }
+                    };
+
+                api.signIn(resultCallback);
+              });
+        } else {
+          channel.setMessageHandler(null);
+        }
+      }
+      {
+        BasicMessageChannel<Object> channel =
+            new BasicMessageChannel<>(
+                binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.getAccessToken", getCodec());
+        if (api != null) {
+          channel.setMessageHandler(
+              (message, reply) -> {
+                ArrayList<Object> wrapped = new ArrayList<Object>();
+                ArrayList<Object> args = (ArrayList<Object>) message;
+                String emailArg = (String) args.get(0);
+                Boolean shouldRecoverAuthArg = (Boolean) args.get(1);
+                Result<String> resultCallback =
+                    new Result<String>() {
+                      public void success(String result) {
+                        wrapped.add(0, result);
+                        reply.reply(wrapped);
+                      }
+
+                      public void error(Throwable error) {
+                        ArrayList<Object> wrappedError = wrapError(error);
+                        reply.reply(wrappedError);
+                      }
+                    };
+
+                api.getAccessToken(emailArg, shouldRecoverAuthArg, resultCallback);
+              });
+        } else {
+          channel.setMessageHandler(null);
+        }
+      }
+      {
+        BasicMessageChannel<Object> channel =
+            new BasicMessageChannel<>(
+                binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.signOut", getCodec());
+        if (api != null) {
+          channel.setMessageHandler(
+              (message, reply) -> {
+                ArrayList<Object> wrapped = new ArrayList<Object>();
+                Result<Void> resultCallback =
+                    new Result<Void>() {
+                      public void success(Void result) {
+                        wrapped.add(0, null);
+                        reply.reply(wrapped);
+                      }
+
+                      public void error(Throwable error) {
+                        ArrayList<Object> wrappedError = wrapError(error);
+                        reply.reply(wrappedError);
+                      }
+                    };
+
+                api.signOut(resultCallback);
+              });
+        } else {
+          channel.setMessageHandler(null);
+        }
+      }
+      {
+        BasicMessageChannel<Object> channel =
+            new BasicMessageChannel<>(
+                binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.disconnect", getCodec());
+        if (api != null) {
+          channel.setMessageHandler(
+              (message, reply) -> {
+                ArrayList<Object> wrapped = new ArrayList<Object>();
+                Result<Void> resultCallback =
+                    new Result<Void>() {
+                      public void success(Void result) {
+                        wrapped.add(0, null);
+                        reply.reply(wrapped);
+                      }
+
+                      public void error(Throwable error) {
+                        ArrayList<Object> wrappedError = wrapError(error);
+                        reply.reply(wrappedError);
+                      }
+                    };
+
+                api.disconnect(resultCallback);
+              });
+        } else {
+          channel.setMessageHandler(null);
+        }
+      }
+      {
+        BasicMessageChannel<Object> channel =
+            new BasicMessageChannel<>(
+                binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.isSignedIn", getCodec());
+        if (api != null) {
+          channel.setMessageHandler(
+              (message, reply) -> {
+                ArrayList<Object> wrapped = new ArrayList<Object>();
+                try {
+                  Boolean output = api.isSignedIn();
+                  wrapped.add(0, output);
+                } catch (Throwable exception) {
+                  ArrayList<Object> wrappedError = wrapError(exception);
+                  wrapped = wrappedError;
+                }
+                reply.reply(wrapped);
+              });
+        } else {
+          channel.setMessageHandler(null);
+        }
+      }
+      {
+        BasicMessageChannel<Object> channel =
+            new BasicMessageChannel<>(
+                binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.clearAuthCache", getCodec());
+        if (api != null) {
+          channel.setMessageHandler(
+              (message, reply) -> {
+                ArrayList<Object> wrapped = new ArrayList<Object>();
+                ArrayList<Object> args = (ArrayList<Object>) message;
+                String tokenArg = (String) args.get(0);
+                Result<Void> resultCallback =
+                    new Result<Void>() {
+                      public void success(Void result) {
+                        wrapped.add(0, null);
+                        reply.reply(wrapped);
+                      }
+
+                      public void error(Throwable error) {
+                        ArrayList<Object> wrappedError = wrapError(error);
+                        reply.reply(wrappedError);
+                      }
+                    };
+
+                api.clearAuthCache(tokenArg, resultCallback);
+              });
+        } else {
+          channel.setMessageHandler(null);
+        }
+      }
+      {
+        BasicMessageChannel<Object> channel =
+            new BasicMessageChannel<>(
+                binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.requestScopes", getCodec());
+        if (api != null) {
+          channel.setMessageHandler(
+              (message, reply) -> {
+                ArrayList<Object> wrapped = new ArrayList<Object>();
+                ArrayList<Object> args = (ArrayList<Object>) message;
+                List<String> scopesArg = (List<String>) args.get(0);
+                Result<Boolean> resultCallback =
+                    new Result<Boolean>() {
+                      public void success(Boolean result) {
+                        wrapped.add(0, result);
+                        reply.reply(wrapped);
+                      }
+
+                      public void error(Throwable error) {
+                        ArrayList<Object> wrappedError = wrapError(error);
+                        reply.reply(wrappedError);
+                      }
+                    };
+
+                api.requestScopes(scopesArg, resultCallback);
+              });
+        } else {
+          channel.setMessageHandler(null);
+        }
+      }
+    }
+  }
+}
diff --git a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInLegacyMethodChannelTest.java b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInLegacyMethodChannelTest.java
new file mode 100644
index 0000000000..4d8c79e22a
--- /dev/null
+++ b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInLegacyMethodChannelTest.java
@@ -0,0 +1,378 @@
+// 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.googlesignin;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
+import com.google.android.gms.auth.api.signin.GoogleSignInClient;
+import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
+import com.google.android.gms.common.api.ApiException;
+import com.google.android.gms.common.api.CommonStatusCodes;
+import com.google.android.gms.common.api.Scope;
+import com.google.android.gms.common.api.Status;
+import com.google.android.gms.tasks.Task;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugin.common.PluginRegistry;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+
+public class GoogleSignInLegacyMethodChannelTest {
+  @Mock Context mockContext;
+  @Mock Resources mockResources;
+  @Mock Activity mockActivity;
+  @Mock BinaryMessenger mockMessenger;
+  @Spy MethodChannel.Result result;
+  @Mock GoogleSignInWrapper mockGoogleSignIn;
+  @Mock GoogleSignInAccount account;
+  @Mock GoogleSignInClient mockClient;
+  @Mock Task<GoogleSignInAccount> mockSignInTask;
+
+  @SuppressWarnings("deprecation")
+  @Mock
+  PluginRegistry.Registrar mockRegistrar;
+
+  private GoogleSignInPlugin plugin;
+  private AutoCloseable mockCloseable;
+
+  @Before
+  public void setUp() {
+    mockCloseable = MockitoAnnotations.openMocks(this);
+    when(mockRegistrar.messenger()).thenReturn(mockMessenger);
+    when(mockRegistrar.context()).thenReturn(mockContext);
+    when(mockRegistrar.activity()).thenReturn(mockActivity);
+    when(mockContext.getResources()).thenReturn(mockResources);
+    plugin = new GoogleSignInPlugin();
+    plugin.initInstance(mockRegistrar.messenger(), mockRegistrar.context(), mockGoogleSignIn);
+    plugin.setUpRegistrar(mockRegistrar);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    mockCloseable.close();
+  }
+
+  @Test
+  public void requestScopes_ResultErrorIfAccountIsNull() {
+    HashMap<String, List<String>> arguments = new HashMap<>();
+    arguments.put("scopes", Collections.singletonList("requestedScope"));
+    MethodCall methodCall = new MethodCall("requestScopes", arguments);
+    when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(null);
+    plugin.onMethodCall(methodCall, result);
+    verify(result).error("sign_in_required", "No account to grant scopes.", null);
+  }
+
+  @Test
+  public void requestScopes_ResultTrueIfAlreadyGranted() {
+    HashMap<String, List<String>> arguments = new HashMap<>();
+    arguments.put("scopes", Collections.singletonList("requestedScope"));
+
+    MethodCall methodCall = new MethodCall("requestScopes", arguments);
+    Scope requestedScope = new Scope("requestedScope");
+    when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account);
+    when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope));
+    when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(true);
+
+    plugin.onMethodCall(methodCall, result);
+    verify(result).success(true);
+  }
+
+  @Test
+  public void requestScopes_RequestsPermissionIfNotGranted() {
+    HashMap<String, List<String>> arguments = new HashMap<>();
+    arguments.put("scopes", Collections.singletonList("requestedScope"));
+    MethodCall methodCall = new MethodCall("requestScopes", arguments);
+    Scope requestedScope = new Scope("requestedScope");
+
+    when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account);
+    when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope));
+    when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false);
+
+    plugin.onMethodCall(methodCall, result);
+
+    verify(mockGoogleSignIn)
+        .requestPermissions(mockActivity, 53295, account, new Scope[] {requestedScope});
+  }
+
+  @Test
+  public void requestScopes_ReturnsFalseIfPermissionDenied() {
+    HashMap<String, List<String>> arguments = new HashMap<>();
+    arguments.put("scopes", Collections.singletonList("requestedScope"));
+    MethodCall methodCall = new MethodCall("requestScopes", arguments);
+    Scope requestedScope = new Scope("requestedScope");
+
+    ArgumentCaptor<PluginRegistry.ActivityResultListener> captor =
+        ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class);
+    verify(mockRegistrar).addActivityResultListener(captor.capture());
+    PluginRegistry.ActivityResultListener listener = captor.getValue();
+
+    when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account);
+    when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope));
+    when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false);
+
+    plugin.onMethodCall(methodCall, result);
+    listener.onActivityResult(
+        GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE,
+        Activity.RESULT_CANCELED,
+        new Intent());
+
+    verify(result).success(false);
+  }
+
+  @Test
+  public void requestScopes_ReturnsTrueIfPermissionGranted() {
+    HashMap<String, List<String>> arguments = new HashMap<>();
+    arguments.put("scopes", Collections.singletonList("requestedScope"));
+    MethodCall methodCall = new MethodCall("requestScopes", arguments);
+    Scope requestedScope = new Scope("requestedScope");
+
+    ArgumentCaptor<PluginRegistry.ActivityResultListener> captor =
+        ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class);
+    verify(mockRegistrar).addActivityResultListener(captor.capture());
+    PluginRegistry.ActivityResultListener listener = captor.getValue();
+
+    when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account);
+    when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope));
+    when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false);
+
+    plugin.onMethodCall(methodCall, result);
+    listener.onActivityResult(
+        GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent());
+
+    verify(result).success(true);
+  }
+
+  @Test
+  public void requestScopes_mayBeCalledRepeatedly_ifAlreadyGranted() {
+    HashMap<String, List<String>> arguments = new HashMap<>();
+    arguments.put("scopes", Collections.singletonList("requestedScope"));
+    MethodCall methodCall = new MethodCall("requestScopes", arguments);
+    Scope requestedScope = new Scope("requestedScope");
+
+    ArgumentCaptor<PluginRegistry.ActivityResultListener> captor =
+        ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class);
+    verify(mockRegistrar).addActivityResultListener(captor.capture());
+    PluginRegistry.ActivityResultListener listener = captor.getValue();
+
+    when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account);
+    when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope));
+    when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false);
+
+    plugin.onMethodCall(methodCall, result);
+    listener.onActivityResult(
+        GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent());
+    plugin.onMethodCall(methodCall, result);
+    listener.onActivityResult(
+        GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent());
+
+    verify(result, times(2)).success(true);
+  }
+
+  @Test
+  public void requestScopes_mayBeCalledRepeatedly_ifNotSignedIn() {
+    HashMap<String, List<String>> arguments = new HashMap<>();
+    arguments.put("scopes", Collections.singletonList("requestedScope"));
+    MethodCall methodCall = new MethodCall("requestScopes", arguments);
+    Scope requestedScope = new Scope("requestedScope");
+
+    ArgumentCaptor<PluginRegistry.ActivityResultListener> captor =
+        ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class);
+    verify(mockRegistrar).addActivityResultListener(captor.capture());
+    PluginRegistry.ActivityResultListener listener = captor.getValue();
+
+    when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(null);
+
+    plugin.onMethodCall(methodCall, result);
+    listener.onActivityResult(
+        GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent());
+    plugin.onMethodCall(methodCall, result);
+    listener.onActivityResult(
+        GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent());
+
+    verify(result, times(2)).error("sign_in_required", "No account to grant scopes.", null);
+  }
+
+  @Test(expected = IllegalStateException.class)
+  public void signInThrowsWithoutActivity() {
+    final GoogleSignInPlugin plugin = new GoogleSignInPlugin();
+    plugin.initInstance(
+        mock(BinaryMessenger.class), mock(Context.class), mock(GoogleSignInWrapper.class));
+
+    plugin.onMethodCall(new MethodCall("signIn", null), null);
+  }
+
+  @Test
+  public void signInSilentlyThatImmediatelyCompletesWithoutResultFinishesWithError()
+      throws ApiException {
+    final String clientId = "fakeClientId";
+    MethodCall methodCall = buildInitMethodCall(clientId, null);
+    initAndAssertServerClientId(methodCall, clientId);
+
+    ApiException exception =
+        new ApiException(new Status(CommonStatusCodes.SIGN_IN_REQUIRED, "Error text"));
+    when(mockClient.silentSignIn()).thenReturn(mockSignInTask);
+    when(mockSignInTask.isComplete()).thenReturn(true);
+    when(mockSignInTask.getResult(ApiException.class)).thenThrow(exception);
+
+    plugin.onMethodCall(new MethodCall("signInSilently", null), result);
+    verify(result)
+        .error(
+            "sign_in_required",
+            "com.google.android.gms.common.api.ApiException: 4: Error text",
+            null);
+  }
+
+  @Test
+  public void init_LoadsServerClientIdFromResources() {
+    final String packageName = "fakePackageName";
+    final String serverClientId = "fakeServerClientId";
+    final int resourceId = 1;
+    MethodCall methodCall = buildInitMethodCall(null, null);
+    when(mockContext.getPackageName()).thenReturn(packageName);
+    when(mockResources.getIdentifier("default_web_client_id", "string", packageName))
+        .thenReturn(resourceId);
+    when(mockContext.getString(resourceId)).thenReturn(serverClientId);
+    initAndAssertServerClientId(methodCall, serverClientId);
+  }
+
+  @Test
+  public void init_InterpretsClientIdAsServerClientId() {
+    final String clientId = "fakeClientId";
+    MethodCall methodCall = buildInitMethodCall(clientId, null);
+    initAndAssertServerClientId(methodCall, clientId);
+  }
+
+  @Test
+  public void init_ForwardsServerClientId() {
+    final String serverClientId = "fakeServerClientId";
+    MethodCall methodCall = buildInitMethodCall(null, serverClientId);
+    initAndAssertServerClientId(methodCall, serverClientId);
+  }
+
+  @Test
+  public void init_IgnoresClientIdIfServerClientIdIsProvided() {
+    final String clientId = "fakeClientId";
+    final String serverClientId = "fakeServerClientId";
+    MethodCall methodCall = buildInitMethodCall(clientId, serverClientId);
+    initAndAssertServerClientId(methodCall, serverClientId);
+  }
+
+  @Test
+  public void init_PassesForceCodeForRefreshTokenFalseWithServerClientIdParameter() {
+    MethodCall methodCall = buildInitMethodCall("fakeClientId", "fakeServerClientId", false);
+
+    initAndAssertForceCodeForRefreshToken(methodCall, false);
+  }
+
+  @Test
+  public void init_PassesForceCodeForRefreshTokenTrueWithServerClientIdParameter() {
+    MethodCall methodCall = buildInitMethodCall("fakeClientId", "fakeServerClientId", true);
+
+    initAndAssertForceCodeForRefreshToken(methodCall, true);
+  }
+
+  @Test
+  public void init_PassesForceCodeForRefreshTokenFalseWithServerClientIdFromResources() {
+    final String packageName = "fakePackageName";
+    final String serverClientId = "fakeServerClientId";
+    final int resourceId = 1;
+    MethodCall methodCall = buildInitMethodCall(null, null, false);
+    when(mockContext.getPackageName()).thenReturn(packageName);
+    when(mockResources.getIdentifier("default_web_client_id", "string", packageName))
+        .thenReturn(resourceId);
+    when(mockContext.getString(resourceId)).thenReturn(serverClientId);
+
+    initAndAssertForceCodeForRefreshToken(methodCall, false);
+  }
+
+  @Test
+  public void init_PassesForceCodeForRefreshTokenTrueWithServerClientIdFromResources() {
+    final String packageName = "fakePackageName";
+    final String serverClientId = "fakeServerClientId";
+    final int resourceId = 1;
+    MethodCall methodCall = buildInitMethodCall(null, null, true);
+    when(mockContext.getPackageName()).thenReturn(packageName);
+    when(mockResources.getIdentifier("default_web_client_id", "string", packageName))
+        .thenReturn(resourceId);
+    when(mockContext.getString(resourceId)).thenReturn(serverClientId);
+
+    initAndAssertForceCodeForRefreshToken(methodCall, true);
+  }
+
+  public void initAndAssertServerClientId(MethodCall methodCall, String serverClientId) {
+    ArgumentCaptor<GoogleSignInOptions> optionsCaptor =
+        ArgumentCaptor.forClass(GoogleSignInOptions.class);
+    when(mockGoogleSignIn.getClient(any(Context.class), optionsCaptor.capture()))
+        .thenReturn(mockClient);
+    plugin.onMethodCall(methodCall, result);
+    verify(result).success(null);
+    Assert.assertEquals(serverClientId, optionsCaptor.getValue().getServerClientId());
+  }
+
+  public void initAndAssertForceCodeForRefreshToken(
+      MethodCall methodCall, boolean forceCodeForRefreshToken) {
+    ArgumentCaptor<GoogleSignInOptions> optionsCaptor =
+        ArgumentCaptor.forClass(GoogleSignInOptions.class);
+    when(mockGoogleSignIn.getClient(any(Context.class), optionsCaptor.capture()))
+        .thenReturn(mockClient);
+    plugin.onMethodCall(methodCall, result);
+    verify(result).success(null);
+    Assert.assertEquals(
+        forceCodeForRefreshToken, optionsCaptor.getValue().isForceCodeForRefreshToken());
+  }
+
+  private static MethodCall buildInitMethodCall(String clientId, String serverClientId) {
+    return buildInitMethodCall(
+        "SignInOption.standard", Collections.<String>emptyList(), clientId, serverClientId, false);
+  }
+
+  private static MethodCall buildInitMethodCall(
+      String clientId, String serverClientId, boolean forceCodeForRefreshToken) {
+    return buildInitMethodCall(
+        "SignInOption.standard",
+        Collections.<String>emptyList(),
+        clientId,
+        serverClientId,
+        forceCodeForRefreshToken);
+  }
+
+  private static MethodCall buildInitMethodCall(
+      String signInOption,
+      List<String> scopes,
+      String clientId,
+      String serverClientId,
+      boolean forceCodeForRefreshToken) {
+    HashMap<String, Object> arguments = new HashMap<>();
+    arguments.put("signInOption", signInOption);
+    arguments.put("scopes", scopes);
+    if (clientId != null) {
+      arguments.put("clientId", clientId);
+    }
+    if (serverClientId != null) {
+      arguments.put("serverClientId", serverClientId);
+    }
+    arguments.put("forceCodeForRefreshToken", forceCodeForRefreshToken);
+    return new MethodCall("init", arguments);
+  }
+}
diff --git a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java
index 627ac40a48..b317d8042f 100644
--- a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java
+++ b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java
@@ -23,11 +23,10 @@ import com.google.android.gms.common.api.Scope;
 import com.google.android.gms.common.api.Status;
 import com.google.android.gms.tasks.Task;
 import io.flutter.plugin.common.BinaryMessenger;
-import io.flutter.plugin.common.MethodCall;
-import io.flutter.plugin.common.MethodChannel;
 import io.flutter.plugin.common.PluginRegistry;
+import io.flutter.plugins.googlesignin.Messages.FlutterError;
+import io.flutter.plugins.googlesignin.Messages.InitParams;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
 import org.junit.After;
 import org.junit.Assert;
@@ -43,7 +42,9 @@ public class GoogleSignInTest {
   @Mock Resources mockResources;
   @Mock Activity mockActivity;
   @Mock BinaryMessenger mockMessenger;
-  @Spy MethodChannel.Result result;
+  @Spy Messages.Result<Void> voidResult;
+  @Spy Messages.Result<Boolean> boolResult;
+  @Spy Messages.Result<Messages.UserData> userDataResult;
   @Mock GoogleSignInWrapper mockGoogleSignIn;
   @Mock GoogleSignInAccount account;
   @Mock GoogleSignInClient mockClient;
@@ -53,7 +54,7 @@ public class GoogleSignInTest {
   @Mock
   PluginRegistry.Registrar mockRegistrar;
 
-  private GoogleSignInPlugin plugin;
+  private GoogleSignInPlugin.Delegate plugin;
   private AutoCloseable mockCloseable;
 
   @Before
@@ -63,8 +64,7 @@ public class GoogleSignInTest {
     when(mockRegistrar.context()).thenReturn(mockContext);
     when(mockRegistrar.activity()).thenReturn(mockActivity);
     when(mockContext.getResources()).thenReturn(mockResources);
-    plugin = new GoogleSignInPlugin();
-    plugin.initInstance(mockRegistrar.messenger(), mockRegistrar.context(), mockGoogleSignIn);
+    plugin = new GoogleSignInPlugin.Delegate(mockRegistrar.context(), mockGoogleSignIn);
     plugin.setUpRegistrar(mockRegistrar);
   }
 
@@ -75,41 +75,37 @@ public class GoogleSignInTest {
 
   @Test
   public void requestScopes_ResultErrorIfAccountIsNull() {
-    HashMap<String, List<String>> arguments = new HashMap<>();
-    arguments.put("scopes", Collections.singletonList("requestedScope"));
-    MethodCall methodCall = new MethodCall("requestScopes", arguments);
     when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(null);
-    plugin.onMethodCall(methodCall, result);
-    verify(result).error("sign_in_required", "No account to grant scopes.", null);
+
+    plugin.requestScopes(Collections.singletonList("requestedScope"), boolResult);
+
+    ArgumentCaptor<Throwable> resultCaptor = ArgumentCaptor.forClass(Throwable.class);
+    verify(boolResult).error(resultCaptor.capture());
+    FlutterError error = (FlutterError) resultCaptor.getValue();
+    Assert.assertEquals("sign_in_required", error.code);
+    Assert.assertEquals("No account to grant scopes.", error.getMessage());
   }
 
   @Test
   public void requestScopes_ResultTrueIfAlreadyGranted() {
-    HashMap<String, List<String>> arguments = new HashMap<>();
-    arguments.put("scopes", Collections.singletonList("requestedScope"));
-
-    MethodCall methodCall = new MethodCall("requestScopes", arguments);
     Scope requestedScope = new Scope("requestedScope");
     when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account);
     when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope));
     when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(true);
 
-    plugin.onMethodCall(methodCall, result);
-    verify(result).success(true);
+    plugin.requestScopes(Collections.singletonList("requestedScope"), boolResult);
+
+    verify(boolResult).success(true);
   }
 
   @Test
   public void requestScopes_RequestsPermissionIfNotGranted() {
-    HashMap<String, List<String>> arguments = new HashMap<>();
-    arguments.put("scopes", Collections.singletonList("requestedScope"));
-    MethodCall methodCall = new MethodCall("requestScopes", arguments);
     Scope requestedScope = new Scope("requestedScope");
-
     when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account);
     when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope));
     when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false);
 
-    plugin.onMethodCall(methodCall, result);
+    plugin.requestScopes(Collections.singletonList("requestedScope"), boolResult);
 
     verify(mockGoogleSignIn)
         .requestPermissions(mockActivity, 53295, account, new Scope[] {requestedScope});
@@ -117,11 +113,7 @@ public class GoogleSignInTest {
 
   @Test
   public void requestScopes_ReturnsFalseIfPermissionDenied() {
-    HashMap<String, List<String>> arguments = new HashMap<>();
-    arguments.put("scopes", Collections.singletonList("requestedScope"));
-    MethodCall methodCall = new MethodCall("requestScopes", arguments);
     Scope requestedScope = new Scope("requestedScope");
-
     ArgumentCaptor<PluginRegistry.ActivityResultListener> captor =
         ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class);
     verify(mockRegistrar).addActivityResultListener(captor.capture());
@@ -131,22 +123,18 @@ public class GoogleSignInTest {
     when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope));
     when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false);
 
-    plugin.onMethodCall(methodCall, result);
+    plugin.requestScopes(Collections.singletonList("requestedScope"), boolResult);
     listener.onActivityResult(
         GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE,
         Activity.RESULT_CANCELED,
         new Intent());
 
-    verify(result).success(false);
+    verify(boolResult).success(false);
   }
 
   @Test
   public void requestScopes_ReturnsTrueIfPermissionGranted() {
-    HashMap<String, List<String>> arguments = new HashMap<>();
-    arguments.put("scopes", Collections.singletonList("requestedScope"));
-    MethodCall methodCall = new MethodCall("requestScopes", arguments);
     Scope requestedScope = new Scope("requestedScope");
-
     ArgumentCaptor<PluginRegistry.ActivityResultListener> captor =
         ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class);
     verify(mockRegistrar).addActivityResultListener(captor.capture());
@@ -156,20 +144,17 @@ public class GoogleSignInTest {
     when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope));
     when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false);
 
-    plugin.onMethodCall(methodCall, result);
+    plugin.requestScopes(Collections.singletonList("requestedScope"), boolResult);
     listener.onActivityResult(
         GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent());
 
-    verify(result).success(true);
+    verify(boolResult).success(true);
   }
 
   @Test
   public void requestScopes_mayBeCalledRepeatedly_ifAlreadyGranted() {
-    HashMap<String, List<String>> arguments = new HashMap<>();
-    arguments.put("scopes", Collections.singletonList("requestedScope"));
-    MethodCall methodCall = new MethodCall("requestScopes", arguments);
+    List<String> requestedScopes = Collections.singletonList("requestedScope");
     Scope requestedScope = new Scope("requestedScope");
-
     ArgumentCaptor<PluginRegistry.ActivityResultListener> captor =
         ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class);
     verify(mockRegistrar).addActivityResultListener(captor.capture());
@@ -179,23 +164,19 @@ public class GoogleSignInTest {
     when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope));
     when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false);
 
-    plugin.onMethodCall(methodCall, result);
+    plugin.requestScopes(requestedScopes, boolResult);
     listener.onActivityResult(
         GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent());
-    plugin.onMethodCall(methodCall, result);
+    plugin.requestScopes(requestedScopes, boolResult);
     listener.onActivityResult(
         GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent());
 
-    verify(result, times(2)).success(true);
+    verify(boolResult, times(2)).success(true);
   }
 
   @Test
   public void requestScopes_mayBeCalledRepeatedly_ifNotSignedIn() {
-    HashMap<String, List<String>> arguments = new HashMap<>();
-    arguments.put("scopes", Collections.singletonList("requestedScope"));
-    MethodCall methodCall = new MethodCall("requestScopes", arguments);
-    Scope requestedScope = new Scope("requestedScope");
-
+    List<String> requestedScopes = Collections.singletonList("requestedScope");
     ArgumentCaptor<PluginRegistry.ActivityResultListener> captor =
         ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class);
     verify(mockRegistrar).addActivityResultListener(captor.capture());
@@ -203,31 +184,39 @@ public class GoogleSignInTest {
 
     when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(null);
 
-    plugin.onMethodCall(methodCall, result);
+    plugin.requestScopes(requestedScopes, boolResult);
     listener.onActivityResult(
         GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent());
-    plugin.onMethodCall(methodCall, result);
+    plugin.requestScopes(requestedScopes, boolResult);
     listener.onActivityResult(
         GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent());
 
-    verify(result, times(2)).error("sign_in_required", "No account to grant scopes.", null);
+    ArgumentCaptor<Throwable> resultCaptor = ArgumentCaptor.forClass(Throwable.class);
+    verify(boolResult, times(2)).error(resultCaptor.capture());
+    List<Throwable> errors = resultCaptor.getAllValues();
+    Assert.assertEquals(2, errors.size());
+    FlutterError error = (FlutterError) errors.get(0);
+    Assert.assertEquals("sign_in_required", error.code);
+    Assert.assertEquals("No account to grant scopes.", error.getMessage());
+    error = (FlutterError) errors.get(1);
+    Assert.assertEquals("sign_in_required", error.code);
+    Assert.assertEquals("No account to grant scopes.", error.getMessage());
   }
 
   @Test(expected = IllegalStateException.class)
   public void signInThrowsWithoutActivity() {
-    final GoogleSignInPlugin plugin = new GoogleSignInPlugin();
-    plugin.initInstance(
-        mock(BinaryMessenger.class), mock(Context.class), mock(GoogleSignInWrapper.class));
+    final GoogleSignInPlugin.Delegate plugin =
+        new GoogleSignInPlugin.Delegate(mock(Context.class), mock(GoogleSignInWrapper.class));
 
-    plugin.onMethodCall(new MethodCall("signIn", null), null);
+    plugin.signIn(userDataResult);
   }
 
   @Test
   public void signInSilentlyThatImmediatelyCompletesWithoutResultFinishesWithError()
       throws ApiException {
     final String clientId = "fakeClientId";
-    MethodCall methodCall = buildInitMethodCall(clientId, null);
-    initAndAssertServerClientId(methodCall, clientId);
+    InitParams params = buildInitParams(clientId, null);
+    initAndAssertServerClientId(params, clientId);
 
     ApiException exception =
         new ApiException(new Status(CommonStatusCodes.SIGN_IN_REQUIRED, "Error text"));
@@ -235,12 +224,13 @@ public class GoogleSignInTest {
     when(mockSignInTask.isComplete()).thenReturn(true);
     when(mockSignInTask.getResult(ApiException.class)).thenThrow(exception);
 
-    plugin.onMethodCall(new MethodCall("signInSilently", null), result);
-    verify(result)
-        .error(
-            "sign_in_required",
-            "com.google.android.gms.common.api.ApiException: 4: Error text",
-            null);
+    plugin.signInSilently(userDataResult);
+    ArgumentCaptor<Throwable> resultCaptor = ArgumentCaptor.forClass(Throwable.class);
+    verify(userDataResult).error(resultCaptor.capture());
+    FlutterError error = (FlutterError) resultCaptor.getValue();
+    Assert.assertEquals("sign_in_required", error.code);
+    Assert.assertEquals(
+        "com.google.android.gms.common.api.ApiException: 4: Error text", error.getMessage());
   }
 
   @Test
@@ -248,48 +238,48 @@ public class GoogleSignInTest {
     final String packageName = "fakePackageName";
     final String serverClientId = "fakeServerClientId";
     final int resourceId = 1;
-    MethodCall methodCall = buildInitMethodCall(null, null);
+    InitParams params = buildInitParams(null, null);
     when(mockContext.getPackageName()).thenReturn(packageName);
     when(mockResources.getIdentifier("default_web_client_id", "string", packageName))
         .thenReturn(resourceId);
     when(mockContext.getString(resourceId)).thenReturn(serverClientId);
-    initAndAssertServerClientId(methodCall, serverClientId);
+    initAndAssertServerClientId(params, serverClientId);
   }
 
   @Test
   public void init_InterpretsClientIdAsServerClientId() {
     final String clientId = "fakeClientId";
-    MethodCall methodCall = buildInitMethodCall(clientId, null);
-    initAndAssertServerClientId(methodCall, clientId);
+    InitParams params = buildInitParams(clientId, null);
+    initAndAssertServerClientId(params, clientId);
   }
 
   @Test
   public void init_ForwardsServerClientId() {
     final String serverClientId = "fakeServerClientId";
-    MethodCall methodCall = buildInitMethodCall(null, serverClientId);
-    initAndAssertServerClientId(methodCall, serverClientId);
+    InitParams params = buildInitParams(null, serverClientId);
+    initAndAssertServerClientId(params, serverClientId);
   }
 
   @Test
   public void init_IgnoresClientIdIfServerClientIdIsProvided() {
     final String clientId = "fakeClientId";
     final String serverClientId = "fakeServerClientId";
-    MethodCall methodCall = buildInitMethodCall(clientId, serverClientId);
-    initAndAssertServerClientId(methodCall, serverClientId);
+    InitParams params = buildInitParams(clientId, serverClientId);
+    initAndAssertServerClientId(params, serverClientId);
   }
 
   @Test
   public void init_PassesForceCodeForRefreshTokenFalseWithServerClientIdParameter() {
-    MethodCall methodCall = buildInitMethodCall("fakeClientId", "fakeServerClientId", false);
+    InitParams params = buildInitParams("fakeClientId", "fakeServerClientId", false);
 
-    initAndAssertForceCodeForRefreshToken(methodCall, false);
+    initAndAssertForceCodeForRefreshToken(params, false);
   }
 
   @Test
   public void init_PassesForceCodeForRefreshTokenTrueWithServerClientIdParameter() {
-    MethodCall methodCall = buildInitMethodCall("fakeClientId", "fakeServerClientId", true);
+    InitParams params = buildInitParams("fakeClientId", "fakeServerClientId", true);
 
-    initAndAssertForceCodeForRefreshToken(methodCall, true);
+    initAndAssertForceCodeForRefreshToken(params, true);
   }
 
   @Test
@@ -297,13 +287,13 @@ public class GoogleSignInTest {
     final String packageName = "fakePackageName";
     final String serverClientId = "fakeServerClientId";
     final int resourceId = 1;
-    MethodCall methodCall = buildInitMethodCall(null, null, false);
+    InitParams params = buildInitParams(null, null, false);
     when(mockContext.getPackageName()).thenReturn(packageName);
     when(mockResources.getIdentifier("default_web_client_id", "string", packageName))
         .thenReturn(resourceId);
     when(mockContext.getString(resourceId)).thenReturn(serverClientId);
 
-    initAndAssertForceCodeForRefreshToken(methodCall, false);
+    initAndAssertForceCodeForRefreshToken(params, false);
   }
 
   @Test
@@ -311,68 +301,66 @@ public class GoogleSignInTest {
     final String packageName = "fakePackageName";
     final String serverClientId = "fakeServerClientId";
     final int resourceId = 1;
-    MethodCall methodCall = buildInitMethodCall(null, null, true);
+    InitParams params = buildInitParams(null, null, true);
     when(mockContext.getPackageName()).thenReturn(packageName);
     when(mockResources.getIdentifier("default_web_client_id", "string", packageName))
         .thenReturn(resourceId);
     when(mockContext.getString(resourceId)).thenReturn(serverClientId);
 
-    initAndAssertForceCodeForRefreshToken(methodCall, true);
+    initAndAssertForceCodeForRefreshToken(params, true);
   }
 
-  public void initAndAssertServerClientId(MethodCall methodCall, String serverClientId) {
+  public void initAndAssertServerClientId(InitParams params, String serverClientId) {
     ArgumentCaptor<GoogleSignInOptions> optionsCaptor =
         ArgumentCaptor.forClass(GoogleSignInOptions.class);
     when(mockGoogleSignIn.getClient(any(Context.class), optionsCaptor.capture()))
         .thenReturn(mockClient);
-    plugin.onMethodCall(methodCall, result);
-    verify(result).success(null);
+    plugin.init(params);
     Assert.assertEquals(serverClientId, optionsCaptor.getValue().getServerClientId());
   }
 
   public void initAndAssertForceCodeForRefreshToken(
-      MethodCall methodCall, boolean forceCodeForRefreshToken) {
+      InitParams params, boolean forceCodeForRefreshToken) {
     ArgumentCaptor<GoogleSignInOptions> optionsCaptor =
         ArgumentCaptor.forClass(GoogleSignInOptions.class);
     when(mockGoogleSignIn.getClient(any(Context.class), optionsCaptor.capture()))
         .thenReturn(mockClient);
-    plugin.onMethodCall(methodCall, result);
-    verify(result).success(null);
+    plugin.init(params);
     Assert.assertEquals(
         forceCodeForRefreshToken, optionsCaptor.getValue().isForceCodeForRefreshToken());
   }
 
-  private static MethodCall buildInitMethodCall(String clientId, String serverClientId) {
-    return buildInitMethodCall(
-        "SignInOption.standard", Collections.<String>emptyList(), clientId, serverClientId, false);
+  private static InitParams buildInitParams(String clientId, String serverClientId) {
+    return buildInitParams(
+        Messages.SignInType.STANDARD, Collections.emptyList(), clientId, serverClientId, false);
   }
 
-  private static MethodCall buildInitMethodCall(
+  private static InitParams buildInitParams(
       String clientId, String serverClientId, boolean forceCodeForRefreshToken) {
-    return buildInitMethodCall(
-        "SignInOption.standard",
-        Collections.<String>emptyList(),
+    return buildInitParams(
+        Messages.SignInType.STANDARD,
+        Collections.emptyList(),
         clientId,
         serverClientId,
         forceCodeForRefreshToken);
   }
 
-  private static MethodCall buildInitMethodCall(
-      String signInOption,
+  private static InitParams buildInitParams(
+      Messages.SignInType signInType,
       List<String> scopes,
       String clientId,
       String serverClientId,
       boolean forceCodeForRefreshToken) {
-    HashMap<String, Object> arguments = new HashMap<>();
-    arguments.put("signInOption", signInOption);
-    arguments.put("scopes", scopes);
+    InitParams.Builder builder = new InitParams.Builder();
+    builder.setSignInType(signInType);
+    builder.setScopes(scopes);
     if (clientId != null) {
-      arguments.put("clientId", clientId);
+      builder.setClientId(clientId);
     }
     if (serverClientId != null) {
-      arguments.put("serverClientId", serverClientId);
+      builder.setServerClientId(serverClientId);
     }
-    arguments.put("forceCodeForRefreshToken", forceCodeForRefreshToken);
-    return new MethodCall("init", arguments);
+    builder.setForceCodeForRefreshToken(forceCodeForRefreshToken);
+    return builder.build();
   }
 }
diff --git a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart
index ce651d7e8d..48f4abb75c 100644
--- a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart
+++ b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart
@@ -5,18 +5,18 @@
 import 'dart:async';
 
 import 'package:flutter/foundation.dart' show visibleForTesting;
-import 'package:flutter/services.dart';
 import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart';
 
-import 'src/utils.dart';
+import 'src/messages.g.dart';
 
 /// Android implementation of [GoogleSignInPlatform].
 class GoogleSignInAndroid extends GoogleSignInPlatform {
-  /// This is only exposed for test purposes. It shouldn't be used by clients of
-  /// the plugin as it may break or change at any time.
-  @visibleForTesting
-  MethodChannel channel =
-      const MethodChannel('plugins.flutter.io/google_sign_in_android');
+  /// Creates a new plugin implementation instance.
+  GoogleSignInAndroid({
+    @visibleForTesting GoogleSignInApi? api,
+  }) : _api = api ?? GoogleSignInApi();
+
+  final GoogleSignInApi _api;
 
   /// Registers this class as the default instance of [GoogleSignInPlatform].
   static void registerWith() {
@@ -40,68 +40,84 @@ class GoogleSignInAndroid extends GoogleSignInPlatform {
 
   @override
   Future<void> initWithParams(SignInInitParameters params) {
-    return channel.invokeMethod<void>('init', <String, dynamic>{
-      'signInOption': params.signInOption.toString(),
-      'scopes': params.scopes,
-      'hostedDomain': params.hostedDomain,
-      'clientId': params.clientId,
-      'serverClientId': params.serverClientId,
-      'forceCodeForRefreshToken': params.forceCodeForRefreshToken,
-    });
+    return _api.init(InitParams(
+      signInType: _signInTypeForOption(params.signInOption),
+      scopes: params.scopes,
+      hostedDomain: params.hostedDomain,
+      clientId: params.clientId,
+      serverClientId: params.serverClientId,
+      forceCodeForRefreshToken: params.forceCodeForRefreshToken,
+    ));
   }
 
   @override
   Future<GoogleSignInUserData?> signInSilently() {
-    return channel
-        .invokeMapMethod<String, dynamic>('signInSilently')
-        .then(getUserDataFromMap);
+    return _api.signInSilently().then(_signInUserDataFromChannelData);
   }
 
   @override
   Future<GoogleSignInUserData?> signIn() {
-    return channel
-        .invokeMapMethod<String, dynamic>('signIn')
-        .then(getUserDataFromMap);
+    return _api.signIn().then(_signInUserDataFromChannelData);
   }
 
   @override
   Future<GoogleSignInTokenData> getTokens(
       {required String email, bool? shouldRecoverAuth = true}) {
-    return channel
-        .invokeMapMethod<String, dynamic>('getTokens', <String, dynamic>{
-      'email': email,
-      'shouldRecoverAuth': shouldRecoverAuth ?? true,
-    }).then((Map<String, dynamic>? result) => getTokenDataFromMap(result!));
+    return _api
+        .getAccessToken(email, shouldRecoverAuth ?? true)
+        .then((String result) => GoogleSignInTokenData(
+              accessToken: result,
+            ));
   }
 
   @override
   Future<void> signOut() {
-    return channel.invokeMapMethod<String, dynamic>('signOut');
+    return _api.signOut();
   }
 
   @override
   Future<void> disconnect() {
-    return channel.invokeMapMethod<String, dynamic>('disconnect');
+    return _api.disconnect();
   }
 
   @override
-  Future<bool> isSignedIn() async {
-    return (await channel.invokeMethod<bool>('isSignedIn'))!;
+  Future<bool> isSignedIn() {
+    return _api.isSignedIn();
   }
 
   @override
   Future<void> clearAuthCache({String? token}) {
-    return channel.invokeMethod<void>(
-      'clearAuthCache',
-      <String, String?>{'token': token},
-    );
+    // The token is not acutally nullable; see
+    // https://github.com/flutter/flutter/issues/129717
+    return _api.clearAuthCache(token!);
   }
 
   @override
-  Future<bool> requestScopes(List<String> scopes) async {
-    return (await channel.invokeMethod<bool>(
-      'requestScopes',
-      <String, List<String>>{'scopes': scopes},
-    ))!;
+  Future<bool> requestScopes(List<String> scopes) {
+    return _api.requestScopes(scopes);
+  }
+
+  SignInType _signInTypeForOption(SignInOption option) {
+    switch (option) {
+      case SignInOption.standard:
+        return SignInType.standard;
+      case SignInOption.games:
+        return SignInType.games;
+    }
+    // Handle the case where a new type is added to the platform interface in
+    // the future, and this version of the package is used with it.
+    // ignore: dead_code
+    throw UnimplementedError('Unsupported sign in option: $option');
+  }
+
+  GoogleSignInUserData _signInUserDataFromChannelData(UserData data) {
+    return GoogleSignInUserData(
+      email: data.email,
+      id: data.id,
+      displayName: data.displayName,
+      photoUrl: data.photoUrl,
+      idToken: data.idToken,
+      serverAuthCode: data.serverAuthCode,
+    );
   }
 }
diff --git a/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart b/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart
new file mode 100644
index 0000000000..14d8cb5fdc
--- /dev/null
+++ b/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart
@@ -0,0 +1,387 @@
+// 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.
+// Autogenerated from Pigeon (v10.1.0), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import
+
+import 'dart:async';
+import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
+
+import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
+import 'package:flutter/services.dart';
+
+/// Pigeon version of SignInOption.
+enum SignInType {
+  /// Default configuration.
+  standard,
+
+  /// Recommended configuration for game sign in.
+  games,
+}
+
+/// Pigeon version of SignInInitParams.
+///
+/// See SignInInitParams for details.
+class InitParams {
+  InitParams({
+    required this.scopes,
+    required this.signInType,
+    this.hostedDomain,
+    this.clientId,
+    this.serverClientId,
+    required this.forceCodeForRefreshToken,
+  });
+
+  List<String?> scopes;
+
+  SignInType signInType;
+
+  String? hostedDomain;
+
+  String? clientId;
+
+  String? serverClientId;
+
+  bool forceCodeForRefreshToken;
+
+  Object encode() {
+    return <Object?>[
+      scopes,
+      signInType.index,
+      hostedDomain,
+      clientId,
+      serverClientId,
+      forceCodeForRefreshToken,
+    ];
+  }
+
+  static InitParams decode(Object result) {
+    result as List<Object?>;
+    return InitParams(
+      scopes: (result[0] as List<Object?>?)!.cast<String?>(),
+      signInType: SignInType.values[result[1]! as int],
+      hostedDomain: result[2] as String?,
+      clientId: result[3] as String?,
+      serverClientId: result[4] as String?,
+      forceCodeForRefreshToken: result[5]! as bool,
+    );
+  }
+}
+
+/// Pigeon version of GoogleSignInUserData.
+///
+/// See GoogleSignInUserData for details.
+class UserData {
+  UserData({
+    this.displayName,
+    required this.email,
+    required this.id,
+    this.photoUrl,
+    this.idToken,
+    this.serverAuthCode,
+  });
+
+  String? displayName;
+
+  String email;
+
+  String id;
+
+  String? photoUrl;
+
+  String? idToken;
+
+  String? serverAuthCode;
+
+  Object encode() {
+    return <Object?>[
+      displayName,
+      email,
+      id,
+      photoUrl,
+      idToken,
+      serverAuthCode,
+    ];
+  }
+
+  static UserData decode(Object result) {
+    result as List<Object?>;
+    return UserData(
+      displayName: result[0] as String?,
+      email: result[1]! as String,
+      id: result[2]! as String,
+      photoUrl: result[3] as String?,
+      idToken: result[4] as String?,
+      serverAuthCode: result[5] as String?,
+    );
+  }
+}
+
+class _GoogleSignInApiCodec extends StandardMessageCodec {
+  const _GoogleSignInApiCodec();
+  @override
+  void writeValue(WriteBuffer buffer, Object? value) {
+    if (value is InitParams) {
+      buffer.putUint8(128);
+      writeValue(buffer, value.encode());
+    } else if (value is UserData) {
+      buffer.putUint8(129);
+      writeValue(buffer, value.encode());
+    } else {
+      super.writeValue(buffer, value);
+    }
+  }
+
+  @override
+  Object? readValueOfType(int type, ReadBuffer buffer) {
+    switch (type) {
+      case 128:
+        return InitParams.decode(readValue(buffer)!);
+      case 129:
+        return UserData.decode(readValue(buffer)!);
+      default:
+        return super.readValueOfType(type, buffer);
+    }
+  }
+}
+
+class GoogleSignInApi {
+  /// Constructor for [GoogleSignInApi].  The [binaryMessenger] named argument is
+  /// available for dependency injection.  If it is left null, the default
+  /// BinaryMessenger will be used which routes to the host platform.
+  GoogleSignInApi({BinaryMessenger? binaryMessenger})
+      : _binaryMessenger = binaryMessenger;
+  final BinaryMessenger? _binaryMessenger;
+
+  static const MessageCodec<Object?> codec = _GoogleSignInApiCodec();
+
+  /// Initializes a sign in request with the given parameters.
+  Future<void> init(InitParams arg_params) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.GoogleSignInApi.init', codec,
+        binaryMessenger: _binaryMessenger);
+    final List<Object?>? replyList =
+        await channel.send(<Object?>[arg_params]) as List<Object?>?;
+    if (replyList == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyList.length > 1) {
+      throw PlatformException(
+        code: replyList[0]! as String,
+        message: replyList[1] as String?,
+        details: replyList[2],
+      );
+    } else {
+      return;
+    }
+  }
+
+  /// Starts a silent sign in.
+  Future<UserData> signInSilently() async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.GoogleSignInApi.signInSilently', codec,
+        binaryMessenger: _binaryMessenger);
+    final List<Object?>? replyList = await channel.send(null) as List<Object?>?;
+    if (replyList == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyList.length > 1) {
+      throw PlatformException(
+        code: replyList[0]! as String,
+        message: replyList[1] as String?,
+        details: replyList[2],
+      );
+    } else if (replyList[0] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
+    } else {
+      return (replyList[0] as UserData?)!;
+    }
+  }
+
+  /// Starts a sign in with user interaction.
+  Future<UserData> signIn() async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.GoogleSignInApi.signIn', codec,
+        binaryMessenger: _binaryMessenger);
+    final List<Object?>? replyList = await channel.send(null) as List<Object?>?;
+    if (replyList == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyList.length > 1) {
+      throw PlatformException(
+        code: replyList[0]! as String,
+        message: replyList[1] as String?,
+        details: replyList[2],
+      );
+    } else if (replyList[0] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
+    } else {
+      return (replyList[0] as UserData?)!;
+    }
+  }
+
+  /// Requests the access token for the current sign in.
+  Future<String> getAccessToken(
+      String arg_email, bool arg_shouldRecoverAuth) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.GoogleSignInApi.getAccessToken', codec,
+        binaryMessenger: _binaryMessenger);
+    final List<Object?>? replyList = await channel
+        .send(<Object?>[arg_email, arg_shouldRecoverAuth]) as List<Object?>?;
+    if (replyList == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyList.length > 1) {
+      throw PlatformException(
+        code: replyList[0]! as String,
+        message: replyList[1] as String?,
+        details: replyList[2],
+      );
+    } else if (replyList[0] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
+    } else {
+      return (replyList[0] as String?)!;
+    }
+  }
+
+  /// Signs out the current user.
+  Future<void> signOut() async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.GoogleSignInApi.signOut', codec,
+        binaryMessenger: _binaryMessenger);
+    final List<Object?>? replyList = await channel.send(null) as List<Object?>?;
+    if (replyList == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyList.length > 1) {
+      throw PlatformException(
+        code: replyList[0]! as String,
+        message: replyList[1] as String?,
+        details: replyList[2],
+      );
+    } else {
+      return;
+    }
+  }
+
+  /// Revokes scope grants to the application.
+  Future<void> disconnect() async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.GoogleSignInApi.disconnect', codec,
+        binaryMessenger: _binaryMessenger);
+    final List<Object?>? replyList = await channel.send(null) as List<Object?>?;
+    if (replyList == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyList.length > 1) {
+      throw PlatformException(
+        code: replyList[0]! as String,
+        message: replyList[1] as String?,
+        details: replyList[2],
+      );
+    } else {
+      return;
+    }
+  }
+
+  /// Returns whether the user is currently signed in.
+  Future<bool> isSignedIn() async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.GoogleSignInApi.isSignedIn', codec,
+        binaryMessenger: _binaryMessenger);
+    final List<Object?>? replyList = await channel.send(null) as List<Object?>?;
+    if (replyList == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyList.length > 1) {
+      throw PlatformException(
+        code: replyList[0]! as String,
+        message: replyList[1] as String?,
+        details: replyList[2],
+      );
+    } else if (replyList[0] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
+    } else {
+      return (replyList[0] as bool?)!;
+    }
+  }
+
+  /// Clears the authentication caching for the given token, requiring a
+  /// new sign in.
+  Future<void> clearAuthCache(String arg_token) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.GoogleSignInApi.clearAuthCache', codec,
+        binaryMessenger: _binaryMessenger);
+    final List<Object?>? replyList =
+        await channel.send(<Object?>[arg_token]) as List<Object?>?;
+    if (replyList == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyList.length > 1) {
+      throw PlatformException(
+        code: replyList[0]! as String,
+        message: replyList[1] as String?,
+        details: replyList[2],
+      );
+    } else {
+      return;
+    }
+  }
+
+  /// Requests access to the given scopes.
+  Future<bool> requestScopes(List<String?> arg_scopes) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.GoogleSignInApi.requestScopes', codec,
+        binaryMessenger: _binaryMessenger);
+    final List<Object?>? replyList =
+        await channel.send(<Object?>[arg_scopes]) as List<Object?>?;
+    if (replyList == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyList.length > 1) {
+      throw PlatformException(
+        code: replyList[0]! as String,
+        message: replyList[1] as String?,
+        details: replyList[2],
+      );
+    } else if (replyList[0] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
+    } else {
+      return (replyList[0] as bool?)!;
+    }
+  }
+}
diff --git a/packages/google_sign_in/google_sign_in_android/lib/src/utils.dart b/packages/google_sign_in/google_sign_in_android/lib/src/utils.dart
deleted file mode 100644
index 5cd7c20b82..0000000000
--- a/packages/google_sign_in/google_sign_in_android/lib/src/utils.dart
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart';
-
-/// Converts user data coming from native code into the proper platform interface type.
-GoogleSignInUserData? getUserDataFromMap(Map<String, dynamic>? data) {
-  if (data == null) {
-    return null;
-  }
-  return GoogleSignInUserData(
-      email: data['email']! as String,
-      id: data['id']! as String,
-      displayName: data['displayName'] as String?,
-      photoUrl: data['photoUrl'] as String?,
-      idToken: data['idToken'] as String?,
-      serverAuthCode: data['serverAuthCode'] as String?);
-}
-
-/// Converts token data coming from native code into the proper platform interface type.
-GoogleSignInTokenData getTokenDataFromMap(Map<String, dynamic> data) {
-  return GoogleSignInTokenData(
-    idToken: data['idToken'] as String?,
-    accessToken: data['accessToken'] as String?,
-    serverAuthCode: data['serverAuthCode'] as String?,
-  );
-}
diff --git a/packages/google_sign_in/google_sign_in_android/pigeons/copyright.txt b/packages/google_sign_in/google_sign_in_android/pigeons/copyright.txt
new file mode 100644
index 0000000000..1236b63caf
--- /dev/null
+++ b/packages/google_sign_in/google_sign_in_android/pigeons/copyright.txt
@@ -0,0 +1,3 @@
+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.
diff --git a/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart b/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart
new file mode 100644
index 0000000000..4804d43eac
--- /dev/null
+++ b/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart
@@ -0,0 +1,106 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/pigeon.dart';
+
+@ConfigurePigeon(PigeonOptions(
+  dartOut: 'lib/src/messages.g.dart',
+  javaOut:
+      'android/src/main/java/io/flutter/plugins/googlesignin/Messages.java',
+  javaOptions: JavaOptions(package: 'io.flutter.plugins.googlesignin'),
+  copyrightHeader: 'pigeons/copyright.txt',
+))
+
+/// Pigeon version of SignInOption.
+enum SignInType {
+  /// Default configuration.
+  standard,
+
+  /// Recommended configuration for game sign in.
+  games,
+}
+
+/// Pigeon version of SignInInitParams.
+///
+/// See SignInInitParams for details.
+class InitParams {
+  /// The parameters to use when initializing the sign in process.
+  const InitParams({
+    this.scopes = const <String>[],
+    this.signInType = SignInType.standard,
+    this.hostedDomain,
+    this.clientId,
+    this.serverClientId,
+    this.forceCodeForRefreshToken = false,
+  });
+
+  // TODO(stuartmorgan): Make the generic type non-nullable once supported.
+  // https://github.com/flutter/flutter/issues/97848
+  // The Java code treats the values as non-nullable.
+  final List<String?> scopes;
+  final SignInType signInType;
+  final String? hostedDomain;
+  final String? clientId;
+  final String? serverClientId;
+  final bool forceCodeForRefreshToken;
+}
+
+/// Pigeon version of GoogleSignInUserData.
+///
+/// See GoogleSignInUserData for details.
+class UserData {
+  UserData({
+    required this.email,
+    required this.id,
+    this.displayName,
+    this.photoUrl,
+    this.idToken,
+    this.serverAuthCode,
+  });
+
+  final String? displayName;
+  final String email;
+  final String id;
+  final String? photoUrl;
+  final String? idToken;
+  final String? serverAuthCode;
+}
+
+@HostApi()
+abstract class GoogleSignInApi {
+  /// Initializes a sign in request with the given parameters.
+  void init(InitParams params);
+
+  /// Starts a silent sign in.
+  @async
+  UserData signInSilently();
+
+  /// Starts a sign in with user interaction.
+  @async
+  UserData signIn();
+
+  /// Requests the access token for the current sign in.
+  @async
+  String getAccessToken(String email, bool shouldRecoverAuth);
+
+  /// Signs out the current user.
+  @async
+  void signOut();
+
+  /// Revokes scope grants to the application.
+  @async
+  void disconnect();
+
+  /// Returns whether the user is currently signed in.
+  bool isSignedIn();
+
+  /// Clears the authentication caching for the given token, requiring a
+  /// new sign in.
+  @async
+  void clearAuthCache(String token);
+
+  /// Requests access to the given scopes.
+  @async
+  bool requestScopes(List<String> scopes);
+}
diff --git a/packages/google_sign_in/google_sign_in_android/pubspec.yaml b/packages/google_sign_in/google_sign_in_android/pubspec.yaml
index a109d41de9..58bd307388 100644
--- a/packages/google_sign_in/google_sign_in_android/pubspec.yaml
+++ b/packages/google_sign_in/google_sign_in_android/pubspec.yaml
@@ -2,7 +2,7 @@ name: google_sign_in_android
 description: Android implementation of the google_sign_in plugin.
 repository: https://github.com/flutter/packages/tree/main/packages/google_sign_in/google_sign_in_android
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22
-version: 6.1.16
+version: 6.1.17
 
 environment:
   sdk: ">=2.18.0 <4.0.0"
@@ -23,10 +23,13 @@ dependencies:
   google_sign_in_platform_interface: ^2.2.0
 
 dev_dependencies:
+  build_runner: ^2.3.0
   flutter_test:
     sdk: flutter
   integration_test:
     sdk: flutter
+  mockito: 5.4.1
+  pigeon: ^10.1.0
 
 # The example deliberately includes limited-use secrets.
 false_secrets:
diff --git a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart
index 671d7683b2..e53b9a8f54 100644
--- a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart
+++ b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart
@@ -5,65 +5,35 @@
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:google_sign_in_android/google_sign_in_android.dart';
-import 'package:google_sign_in_android/src/utils.dart';
+import 'package:google_sign_in_android/src/messages.g.dart';
 import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
 
-const Map<String, String> kUserData = <String, String>{
-  'email': 'john.doe@gmail.com',
-  'id': '8162538176523816253123',
-  'photoUrl': 'https://lh5.googleusercontent.com/photo.jpg',
-  'displayName': 'John Doe',
-  'idToken': '123',
-  'serverAuthCode': '789',
-};
+import 'google_sign_in_android_test.mocks.dart';
 
-const Map<dynamic, dynamic> kTokenData = <String, dynamic>{
-  'idToken': '123',
-  'accessToken': '456',
-  'serverAuthCode': '789',
-};
-
-const Map<String, dynamic> kDefaultResponses = <String, dynamic>{
-  'init': null,
-  'signInSilently': kUserData,
-  'signIn': kUserData,
-  'signOut': null,
-  'disconnect': null,
-  'isSignedIn': true,
-  'getTokens': kTokenData,
-  'requestScopes': true,
-};
-
-final GoogleSignInUserData? kUser = getUserDataFromMap(kUserData);
-final GoogleSignInTokenData kToken =
-    getTokenDataFromMap(kTokenData as Map<String, dynamic>);
+final GoogleSignInUserData _user = GoogleSignInUserData(
+  email: 'john.doe@gmail.com',
+  id: '8162538176523816253123',
+  photoUrl: 'https://lh5.googleusercontent.com/photo.jpg',
+  displayName: 'John Doe',
+  idToken: '123',
+  serverAuthCode: '789',
+);
+final GoogleSignInTokenData _token = GoogleSignInTokenData(
+  accessToken: '456',
+);
 
+@GenerateMocks(<Type>[GoogleSignInApi])
 void main() {
   TestWidgetsFlutterBinding.ensureInitialized();
 
-  final GoogleSignInAndroid googleSignIn = GoogleSignInAndroid();
-  final MethodChannel channel = googleSignIn.channel;
-
-  final List<MethodCall> log = <MethodCall>[];
-  late Map<String, dynamic>
-      responses; // Some tests mutate some kDefaultResponses
+  late GoogleSignInAndroid googleSignIn;
+  late MockGoogleSignInApi api;
 
   setUp(() {
-    responses = Map<String, dynamic>.from(kDefaultResponses);
-    _ambiguate(TestDefaultBinaryMessengerBinding.instance)!
-        .defaultBinaryMessenger
-        .setMockMethodCallHandler(
-      channel,
-      (MethodCall methodCall) {
-        log.add(methodCall);
-        final dynamic response = responses[methodCall.method];
-        if (response != null && response is Exception) {
-          return Future<dynamic>.error('$response');
-        }
-        return Future<dynamic>.value(response);
-      },
-    );
-    log.clear();
+    api = MockGoogleSignInApi();
+    googleSignIn = GoogleSignInAndroid(api: api);
   });
 
   test('registered instance', () {
@@ -73,111 +43,159 @@ void main() {
 
   test('signInSilently transforms platform data to GoogleSignInUserData',
       () async {
+    when(api.signInSilently()).thenAnswer((_) async => UserData(
+          email: _user.email,
+          id: _user.id,
+          photoUrl: _user.photoUrl,
+          displayName: _user.displayName,
+          idToken: _user.idToken,
+          serverAuthCode: _user.serverAuthCode,
+        ));
+
     final dynamic response = await googleSignIn.signInSilently();
-    expect(response, kUser);
+
+    expect(response, _user);
   });
+
   test('signInSilently Exceptions -> throws', () async {
-    responses['signInSilently'] = Exception('Not a user');
+    when(api.signInSilently())
+        .thenAnswer((_) async => throw PlatformException(code: 'fail'));
+
     expect(googleSignIn.signInSilently(),
         throwsA(isInstanceOf<PlatformException>()));
   });
 
   test('signIn transforms platform data to GoogleSignInUserData', () async {
+    when(api.signIn()).thenAnswer((_) async => UserData(
+          email: _user.email,
+          id: _user.id,
+          photoUrl: _user.photoUrl,
+          displayName: _user.displayName,
+          idToken: _user.idToken,
+          serverAuthCode: _user.serverAuthCode,
+        ));
+
     final dynamic response = await googleSignIn.signIn();
-    expect(response, kUser);
+
+    expect(response, _user);
   });
+
   test('signIn Exceptions -> throws', () async {
-    responses['signIn'] = Exception('Not a user');
+    when(api.signIn())
+        .thenAnswer((_) async => throw PlatformException(code: 'fail'));
+
     expect(googleSignIn.signIn(), throwsA(isInstanceOf<PlatformException>()));
   });
 
   test('getTokens transforms platform data to GoogleSignInTokenData', () async {
-    final dynamic response = await googleSignIn.getTokens(
-        email: 'example@example.com', shouldRecoverAuth: false);
-    expect(response, kToken);
-    expect(
-        log[0],
-        isMethodCall('getTokens', arguments: <String, dynamic>{
-          'email': 'example@example.com',
-          'shouldRecoverAuth': false,
-        }));
+    const bool recoverAuth = false;
+    when(api.getAccessToken(_user.email, recoverAuth))
+        .thenAnswer((_) async => _token.accessToken!);
+
+    final GoogleSignInTokenData response = await googleSignIn.getTokens(
+        email: _user.email, shouldRecoverAuth: recoverAuth);
+
+    expect(response, _token);
   });
 
   test('getTokens will not pass null for shouldRecoverAuth', () async {
-    await googleSignIn.getTokens(
-        email: 'example@example.com', shouldRecoverAuth: null);
-    expect(
-        log[0],
-        isMethodCall('getTokens', arguments: <String, dynamic>{
-          'email': 'example@example.com',
-          'shouldRecoverAuth': true,
-        }));
+    when(api.getAccessToken(_user.email, true))
+        .thenAnswer((_) async => _token.accessToken!);
+
+    final GoogleSignInTokenData response = await googleSignIn.getTokens(
+        email: _user.email, shouldRecoverAuth: null);
+
+    expect(response, _token);
   });
 
-  test('Other functions pass through arguments to the channel', () async {
-    final Map<void Function(), Matcher> tests = <void Function(), Matcher>{
-      () {
-        googleSignIn.init(
-            hostedDomain: 'example.com',
-            scopes: <String>['two', 'scopes'],
-            signInOption: SignInOption.games,
-            clientId: 'fakeClientId');
-      }: isMethodCall('init', arguments: <String, dynamic>{
-        'hostedDomain': 'example.com',
-        'scopes': <String>['two', 'scopes'],
-        'signInOption': 'SignInOption.games',
-        'clientId': 'fakeClientId',
-        'serverClientId': null,
-        'forceCodeForRefreshToken': false,
-      }),
-      () {
-        googleSignIn.initWithParams(const SignInInitParameters(
-            hostedDomain: 'example.com',
-            scopes: <String>['two', 'scopes'],
-            signInOption: SignInOption.games,
-            clientId: 'fakeClientId',
-            serverClientId: 'fakeServerClientId',
-            forceCodeForRefreshToken: true));
-      }: isMethodCall('init', arguments: <String, dynamic>{
-        'hostedDomain': 'example.com',
-        'scopes': <String>['two', 'scopes'],
-        'signInOption': 'SignInOption.games',
-        'clientId': 'fakeClientId',
-        'serverClientId': 'fakeServerClientId',
-        'forceCodeForRefreshToken': true,
-      }),
-      () {
-        googleSignIn.getTokens(
-            email: 'example@example.com', shouldRecoverAuth: false);
-      }: isMethodCall('getTokens', arguments: <String, dynamic>{
-        'email': 'example@example.com',
-        'shouldRecoverAuth': false,
-      }),
-      () {
-        googleSignIn.clearAuthCache(token: 'abc');
-      }: isMethodCall('clearAuthCache', arguments: <String, dynamic>{
-        'token': 'abc',
-      }),
-      () {
-        googleSignIn.requestScopes(<String>['newScope', 'anotherScope']);
-      }: isMethodCall('requestScopes', arguments: <String, dynamic>{
-        'scopes': <String>['newScope', 'anotherScope'],
-      }),
-      googleSignIn.signOut: isMethodCall('signOut', arguments: null),
-      googleSignIn.disconnect: isMethodCall('disconnect', arguments: null),
-      googleSignIn.isSignedIn: isMethodCall('isSignedIn', arguments: null),
-    };
+  test('initWithParams passes arguments', () async {
+    const SignInInitParameters initParams = SignInInitParameters(
+      hostedDomain: 'example.com',
+      scopes: <String>['two', 'scopes'],
+      signInOption: SignInOption.games,
+      clientId: 'fakeClientId',
+    );
 
-    for (final void Function() f in tests.keys) {
-      f();
-    }
+    await googleSignIn.init(
+      hostedDomain: initParams.hostedDomain,
+      scopes: initParams.scopes,
+      signInOption: initParams.signInOption,
+      clientId: initParams.clientId,
+    );
 
-    expect(log, tests.values);
+    final VerificationResult result = verify(api.init(captureAny));
+    final InitParams passedParams = result.captured[0] as InitParams;
+    expect(passedParams.hostedDomain, initParams.hostedDomain);
+    expect(passedParams.scopes, initParams.scopes);
+    expect(passedParams.signInType, SignInType.games);
+    expect(passedParams.clientId, initParams.clientId);
+    // These should use whatever the SignInInitParameters defaults are.
+    expect(passedParams.serverClientId, initParams.serverClientId);
+    expect(passedParams.forceCodeForRefreshToken,
+        initParams.forceCodeForRefreshToken);
+  });
+
+  test('initWithParams passes arguments', () async {
+    const SignInInitParameters initParams = SignInInitParameters(
+      hostedDomain: 'example.com',
+      scopes: <String>['two', 'scopes'],
+      signInOption: SignInOption.games,
+      clientId: 'fakeClientId',
+      serverClientId: 'fakeServerClientId',
+      forceCodeForRefreshToken: true,
+    );
+
+    await googleSignIn.initWithParams(initParams);
+
+    final VerificationResult result = verify(api.init(captureAny));
+    final InitParams passedParams = result.captured[0] as InitParams;
+    expect(passedParams.hostedDomain, initParams.hostedDomain);
+    expect(passedParams.scopes, initParams.scopes);
+    expect(passedParams.signInType, SignInType.games);
+    expect(passedParams.clientId, initParams.clientId);
+    expect(passedParams.serverClientId, initParams.serverClientId);
+    expect(passedParams.forceCodeForRefreshToken,
+        initParams.forceCodeForRefreshToken);
+  });
+
+  test('clearAuthCache passes arguments', () async {
+    const String token = 'abc';
+
+    await googleSignIn.clearAuthCache(token: token);
+
+    verify(api.clearAuthCache(token));
+  });
+
+  test('requestScopens passes arguments', () async {
+    const List<String> scopes = <String>['newScope', 'anotherScope'];
+    when(api.requestScopes(scopes)).thenAnswer((_) async => true);
+
+    final bool response = await googleSignIn.requestScopes(scopes);
+
+    expect(response, true);
+  });
+
+  test('signOut calls through', () async {
+    await googleSignIn.signOut();
+
+    verify(api.signOut());
+  });
+
+  test('disconnect calls through', () async {
+    await googleSignIn.disconnect();
+
+    verify(api.disconnect());
+  });
+
+  test('isSignedIn passes true response', () async {
+    when(api.isSignedIn()).thenAnswer((_) async => true);
+
+    expect(await googleSignIn.isSignedIn(), true);
+  });
+
+  test('isSignedIn passes false response', () async {
+    when(api.isSignedIn()).thenAnswer((_) async => false);
+
+    expect(await googleSignIn.isSignedIn(), false);
   });
 }
-
-/// This allows a value of type T or T? to be treated as a value of type T?.
-///
-/// We use this so that APIs that have become non-nullable can still be used
-/// with `!` and `?` on the stable branch.
-T? _ambiguate<T>(T? value) => value;
diff --git a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.mocks.dart b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.mocks.dart
new file mode 100644
index 0000000000..b6a5fed371
--- /dev/null
+++ b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.mocks.dart
@@ -0,0 +1,138 @@
+// Mocks generated by Mockito 5.4.1 from annotations
+// in google_sign_in_android/test/google_sign_in_android_test.dart.
+// Do not manually edit this file.
+
+// @dart=2.19
+
+// ignore_for_file: no_leading_underscores_for_library_prefixes
+import 'dart:async' as _i3;
+
+import 'package:google_sign_in_android/src/messages.g.dart' as _i2;
+import 'package:mockito/mockito.dart' as _i1;
+
+// ignore_for_file: type=lint
+// ignore_for_file: avoid_redundant_argument_values
+// ignore_for_file: avoid_setters_without_getters
+// ignore_for_file: comment_references
+// ignore_for_file: implementation_imports
+// ignore_for_file: invalid_use_of_visible_for_testing_member
+// ignore_for_file: prefer_const_constructors
+// ignore_for_file: unnecessary_parenthesis
+// ignore_for_file: camel_case_types
+// ignore_for_file: subtype_of_sealed_class
+
+class _FakeUserData_0 extends _i1.SmartFake implements _i2.UserData {
+  _FakeUserData_0(
+    Object parent,
+    Invocation parentInvocation,
+  ) : super(
+          parent,
+          parentInvocation,
+        );
+}
+
+/// A class which mocks [GoogleSignInApi].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockGoogleSignInApi extends _i1.Mock implements _i2.GoogleSignInApi {
+  MockGoogleSignInApi() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  _i3.Future<void> init(_i2.InitParams? arg_params) => (super.noSuchMethod(
+        Invocation.method(
+          #init,
+          [arg_params],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+  @override
+  _i3.Future<_i2.UserData> signInSilently() => (super.noSuchMethod(
+        Invocation.method(
+          #signInSilently,
+          [],
+        ),
+        returnValue: _i3.Future<_i2.UserData>.value(_FakeUserData_0(
+          this,
+          Invocation.method(
+            #signInSilently,
+            [],
+          ),
+        )),
+      ) as _i3.Future<_i2.UserData>);
+  @override
+  _i3.Future<_i2.UserData> signIn() => (super.noSuchMethod(
+        Invocation.method(
+          #signIn,
+          [],
+        ),
+        returnValue: _i3.Future<_i2.UserData>.value(_FakeUserData_0(
+          this,
+          Invocation.method(
+            #signIn,
+            [],
+          ),
+        )),
+      ) as _i3.Future<_i2.UserData>);
+  @override
+  _i3.Future<String> getAccessToken(
+    String? arg_email,
+    bool? arg_shouldRecoverAuth,
+  ) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #getAccessToken,
+          [
+            arg_email,
+            arg_shouldRecoverAuth,
+          ],
+        ),
+        returnValue: _i3.Future<String>.value(''),
+      ) as _i3.Future<String>);
+  @override
+  _i3.Future<void> signOut() => (super.noSuchMethod(
+        Invocation.method(
+          #signOut,
+          [],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+  @override
+  _i3.Future<void> disconnect() => (super.noSuchMethod(
+        Invocation.method(
+          #disconnect,
+          [],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+  @override
+  _i3.Future<bool> isSignedIn() => (super.noSuchMethod(
+        Invocation.method(
+          #isSignedIn,
+          [],
+        ),
+        returnValue: _i3.Future<bool>.value(false),
+      ) as _i3.Future<bool>);
+  @override
+  _i3.Future<void> clearAuthCache(String? arg_token) => (super.noSuchMethod(
+        Invocation.method(
+          #clearAuthCache,
+          [arg_token],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+  @override
+  _i3.Future<bool> requestScopes(List<String?>? arg_scopes) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #requestScopes,
+          [arg_scopes],
+        ),
+        returnValue: _i3.Future<bool>.value(false),
+      ) as _i3.Future<bool>);
+}