Reland: "Fix how Gradle resolves Android plugin" (#137115)

Relands #97823

When the tool migrated to `.flutter-plugins-dependencies`, the Gradle plugin was never changed.
Until now, the plugin had the heuristic that a plugin with a `android/build.gradle` file supported the Android platform.

Also applies schema of `getPluginDependencies` to `getPluginList` which uses a `List` of Object instead of `Properties`.

Fixes #97729
Cause of the error: https://github.com/flutter/flutter/blob/5f105a6ca7a5ac7b8bc9b241f4c2d86f4188cf5c/packages/flutter_tools/gradle/flutter.gradle#L421C25-L421C25

Fixes #98048
The deprecated line `include ":$name"` in `settings.gradle` (pluginEach) in old projects causes the `project.rootProject.findProject` to also find the plugin "project", so it is not failing on the `afterEvaluate` method. But the plugin shouldn't be included in the first place as it fails with `Could not find method implementation() for arguments` error in special cases.

Related to #48918, see [_writeFlutterPluginsListLegacy](https://github.com/flutter/flutter/blob/27bc1cf61a5b54bf655062be63050123abb617e4/packages/flutter_tools/lib/src/flutter_plugins.dart#L248).

Co-authored-by: Emmanuel Garcia <egarciad@google.com>
diff --git a/packages/flutter_tools/gradle/module_plugin_loader.gradle b/packages/flutter_tools/gradle/module_plugin_loader.gradle
index 2e3a800..6e52b5a 100644
--- a/packages/flutter_tools/gradle/module_plugin_loader.gradle
+++ b/packages/flutter_tools/gradle/module_plugin_loader.gradle
@@ -5,41 +5,26 @@
 // This file is included from `<module>/.android/include_flutter.groovy`,
 // so it can be versioned with the Flutter SDK.
 
-import groovy.json.JsonSlurper
+import java.nio.file.Paths
+
+File pathToThisDirectory = buildscript.sourceFile.parentFile
+apply from: Paths.get(pathToThisDirectory.absolutePath, "src", "main", "groovy", "native_plugin_loader.groovy")
 
 def moduleProjectRoot = project(':flutter').projectDir.parentFile.parentFile
 
-def object = null;
-String flutterModulePath = project(':flutter').projectDir.parentFile.getAbsolutePath()
-// If this logic is changed, also change the logic in app_plugin_loader.gradle.
-def pluginsFile = new File(moduleProjectRoot, '.flutter-plugins-dependencies')
-if (pluginsFile.exists()) {
-    object = new JsonSlurper().parseText(pluginsFile.text)
-    assert object instanceof Map
-    assert object.plugins instanceof Map
-    assert object.plugins.android instanceof List
-    // Includes the Flutter plugins that support the Android platform.
-    object.plugins.android.each { androidPlugin ->
-        assert androidPlugin.name instanceof String
-        assert androidPlugin.path instanceof String
-        // Skip plugins that have no native build (such as a Dart-only
-        // implementation of a federated plugin).
-        def needsBuild = androidPlugin.containsKey('native_build') ? androidPlugin['native_build'] : true
-        if (!needsBuild) {
-            return
-        }
-        def pluginDirectory = new File(androidPlugin.path, 'android')
-        assert pluginDirectory.exists()
-        include ":${androidPlugin.name}"
-        project(":${androidPlugin.name}").projectDir = pluginDirectory
-    }
+List<Map<String, Object>> nativePlugins = nativePluginLoader.getPlugins(moduleProjectRoot)
+nativePlugins.each { androidPlugin ->
+    def pluginDirectory = new File(androidPlugin.path as String, 'android')
+    assert pluginDirectory.exists()
+    include ":${androidPlugin.name}"
+    project(":${androidPlugin.name}").projectDir = pluginDirectory
 }
 
+String flutterModulePath = project(':flutter').projectDir.parentFile.getAbsolutePath()
 gradle.getGradle().projectsLoaded { g ->
     g.rootProject.beforeEvaluate { p ->
         p.subprojects { subproject ->
-            if (object != null && object.plugins != null && object.plugins.android != null
-                    && object.plugins.android.name.contains(subproject.name)) {
+            if (nativePlugins.name.contains(subproject.name)) {
                 File androidPluginBuildOutputDir = new File(flutterModulePath + File.separator
                         + "plugins_build_output" + File.separator + subproject.name);
                 if (!androidPluginBuildOutputDir.exists()) {
diff --git a/packages/flutter_tools/gradle/src/main/groovy/app_plugin_loader.groovy b/packages/flutter_tools/gradle/src/main/groovy/app_plugin_loader.groovy
index 402ab64..1696521 100644
--- a/packages/flutter_tools/gradle/src/main/groovy/app_plugin_loader.groovy
+++ b/packages/flutter_tools/gradle/src/main/groovy/app_plugin_loader.groovy
@@ -1,39 +1,29 @@
-import groovy.json.JsonSlurper
 import org.gradle.api.Plugin
 import org.gradle.api.initialization.Settings
 
+import java.nio.file.Paths
+
 apply plugin: FlutterAppPluginLoaderPlugin
 
 class FlutterAppPluginLoaderPlugin implements Plugin<Settings> {
-    // This string must match _kFlutterPluginsHasNativeBuildKey defined in
-    // packages/flutter_tools/lib/src/flutter_plugins.dart.
-    private final String nativeBuildKey = 'native_build'
-
     @Override
     void apply(Settings settings) {
         def flutterProjectRoot = settings.settingsDir.parentFile
 
-        // If this logic is changed, also change the logic in module_plugin_loader.gradle.
-        def pluginsFile = new File(flutterProjectRoot, '.flutter-plugins-dependencies')
-        if (!pluginsFile.exists()) {
-            return
+        if(!settings.ext.hasProperty('flutterSdkPath')) {
+            def properties = new Properties()
+            def localPropertiesFile = new File(settings.rootProject.projectDir, "local.properties")
+            localPropertiesFile.withInputStream { properties.load(it) }
+            settings.ext.flutterSdkPath = properties.getProperty("flutter.sdk")
+            assert settings.ext.flutterSdkPath != null, "flutter.sdk not set in local.properties"
         }
+        
+        // Load shared gradle functions
+        settings.apply from: Paths.get(settings.ext.flutterSdkPath, "packages", "flutter_tools", "gradle", "src", "main", "groovy", "native_plugin_loader.groovy")
 
-        def object = new JsonSlurper().parseText(pluginsFile.text)
-        assert object instanceof Map
-        assert object.plugins instanceof Map
-        assert object.plugins.android instanceof List
-        // Includes the Flutter plugins that support the Android platform.
-        object.plugins.android.each { androidPlugin ->
-            assert androidPlugin.name instanceof String
-            assert androidPlugin.path instanceof String
-            // Skip plugins that have no native build (such as a Dart-only implementation
-            // of a federated plugin).
-            def needsBuild = androidPlugin.containsKey(nativeBuildKey) ? androidPlugin[nativeBuildKey] : true
-            if (!needsBuild) {
-                return
-            }
-            def pluginDirectory = new File(androidPlugin.path, 'android')
+        List<Map<String, Object>> nativePlugins = settings.ext.nativePluginLoader.getPlugins(flutterProjectRoot)
+        nativePlugins.each { androidPlugin ->
+            def pluginDirectory = new File(androidPlugin.path as String, 'android')
             assert pluginDirectory.exists()
             settings.include(":${androidPlugin.name}")
             settings.project(":${androidPlugin.name}").projectDir = pluginDirectory
diff --git a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy
index bdd4e40..95c9779 100644
--- a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy
+++ b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy
@@ -3,7 +3,6 @@
 // found in the LICENSE file.
 
 import com.android.build.OutputFile
-import groovy.json.JsonSlurper
 import groovy.json.JsonGenerator
 import groovy.xml.QName
 import java.nio.file.Paths
@@ -65,7 +64,7 @@
      * Specifies the relative directory to the Flutter project directory.
      * In an app project, this is ../.. since the app's build.gradle is under android/app.
      */
-    String source
+    String source = '../..'
 
     /** Allows to override the target file. Otherwise, the target is lib/main.dart. */
     String target
@@ -222,6 +221,9 @@
             }
         }
 
+        // Load shared gradle functions
+        project.apply from: Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools", "gradle", "src", "main", "groovy", "native_plugin_loader.groovy")
+
         project.extensions.create("flutter", FlutterExtension)
         this.addFlutterTasks(project)
 
@@ -357,7 +359,7 @@
         // This prevents duplicated classes when using custom build types. That is, a custom build
         // type like profile is used, and the plugin and app projects have API dependencies on the
         // embedding.
-        if (!isFlutterAppProject() || getPluginList().size() == 0) {
+        if (!isFlutterAppProject() || getPluginList(project).size() == 0) {
             addApiDependencies(project, buildType.name,
                     "io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion")
         }
@@ -390,19 +392,77 @@
      * Configures the Flutter plugin dependencies.
      *
      * The plugins are added to pubspec.yaml. Then, upon running `flutter pub get`,
-     * the tool generates a `.flutter-plugins` file, which contains a 1:1 map to each plugin location.
+     * the tool generates a `.flutter-plugins-dependencies` file, which contains a map to each plugin location.
      * Finally, the project's `settings.gradle` loads each plugin's android directory as a subproject.
      */
-    private void configurePlugins() {
-        getPluginList().each(this.&configurePluginProject)
-        getPluginDependencies().each(this.&configurePluginDependencies)
+    private void configurePlugins(Project project) {
+        configureLegacyPluginEachProjects(project)
+        getPluginList(project).each(this.&configurePluginProject)
+        getPluginList(project).each(this.&configurePluginDependencies)
+    }
+
+    // TODO(54566, 48918): Can remove once the issues are resolved.
+    //  This means all references to `.flutter-plugins` are then removed and
+    //  apps only depend exclusively on the `plugins` property in `.flutter-plugins-dependencies`.
+    /**
+     * Workaround to load non-native plugins for developers who may still use an
+     * old `settings.gradle` which includes all the plugins from the
+     * `.flutter-plugins` file, even if not made for Android.
+     * The settings.gradle then:
+     *     1) tries to add the android plugin implementation, which does not
+     *        exist at all, but is also not included successfully
+     *        (which does not throw an error and therefore isn't a problem), or
+     *     2) includes the plugin successfully as a valid android plugin
+     *        directory exists, even if the surrounding flutter package does not
+     *        support the android platform (see e.g. apple_maps_flutter: 1.0.1).
+     *        So as it's included successfully it expects to be added as API.
+     *        This is only possible by taking all plugins into account, which
+     *        only appear on the `dependencyGraph` and in the `.flutter-plugins` file.
+     * So in summary the plugins are currently selected from the `dependencyGraph`
+     * and filtered then with the [doesSupportAndroidPlatform] method instead of
+     * just using the `plugins.android` list.
+     */
+    private configureLegacyPluginEachProjects(Project project) {
+        File settingsGradle = new File(project.projectDir.parentFile, 'settings.gradle')
+        try {
+            if (!settingsGradle.text.contains("'.flutter-plugins'")) {
+                return
+            }
+        } catch (FileNotFoundException ignored) {
+            throw new GradleException("settings.gradle does not exist: ${settingsGradle.absolutePath}")
+        }
+        List<Map<String, Object>> deps = getPluginDependencies(project)
+        List<String> plugins = getPluginList(project).collect { it.name as String }
+        deps.removeIf { plugins.contains(it.name) }
+        deps.each {
+            Project pluginProject = project.rootProject.findProject(":${it.name}")
+            if (pluginProject == null) {
+                // Plugin was not included in `settings.gradle`, but is listed in `.flutter-plugins`.
+                project.logger.error("Plugin project :${it.name} listed, but not found. Please fix your settings.gradle.")
+            } else if (doesSupportAndroidPlatform(pluginProject.projectDir.parentFile.path as String)) {
+                // Plugin has a functioning `android` folder and is included successfully, although it's not supported.
+                // It must be configured nonetheless, to not throw an "Unresolved reference" exception.
+                configurePluginProject(it)
+            } else {
+                // Plugin has no or an empty `android` folder. No action required.
+            }
+        }
+    }
+
+    // TODO(54566): Can remove this function and its call sites once resolved.
+    /**
+     * Returns `true` if the given path contains an `android/build.gradle` file.
+     */
+    private static Boolean doesSupportAndroidPlatform(String path) {
+        File editableAndroidProject = new File(path, 'android' + File.separator + 'build.gradle')
+        return editableAndroidProject.exists()
     }
 
     /** Adds the plugin project dependency to the app project. */
-    private void configurePluginProject(String pluginName, String _) {
-        Project pluginProject = project.rootProject.findProject(":$pluginName")
+    private void configurePluginProject(Map<String, Object> pluginObject) {
+        assert(pluginObject.name instanceof String)
+        Project pluginProject = project.rootProject.findProject(":${pluginObject.name}")
         if (pluginProject == null) {
-            project.logger.error("Plugin project :$pluginName not found. Please update settings.gradle.")
             return
         }
         // Add plugin dependency to the app project.
@@ -441,7 +501,7 @@
         pluginProject.afterEvaluate {
             // Checks if there is a mismatch between the plugin compileSdkVersion and the project compileSdkVersion.
             if (pluginProject.android.compileSdkVersion > project.android.compileSdkVersion) {
-                project.logger.quiet("Warning: The plugin ${pluginName} requires Android SDK version ${getCompileSdkFromProject(pluginProject)} or higher.")
+                project.logger.quiet("Warning: The plugin ${pluginObject.name} requires Android SDK version ${getCompileSdkFromProject(pluginProject)} or higher.")
                 project.logger.quiet("For more information about build configuration, see $kWebsiteDeploymentAndroidBuildConfig.")
             }
 
@@ -493,10 +553,14 @@
             String ndkVersionIfUnspecified = "21.1.6352462" /* The default for AGP 4.1.0 used in old templates. */
             String projectNdkVersion = project.android.ndkVersion ?: ndkVersionIfUnspecified
             String maxPluginNdkVersion = projectNdkVersion
-            int numProcessedPlugins = getPluginList().size()
+            int numProcessedPlugins = getPluginList(project).size()
 
-            getPluginList().each { plugin ->
-                Project pluginProject = project.rootProject.findProject(plugin.key)
+            getPluginList(project).each { pluginObject ->
+                assert(pluginObject.name instanceof String)
+                Project pluginProject = project.rootProject.findProject(":${pluginObject.name}")
+                if (pluginProject == null) {
+                    return
+                }
                 pluginProject.afterEvaluate {
                     // Default to int min if using a preview version to skip the sdk check.
                     int pluginCompileSdkVersion = Integer.MIN_VALUE;
@@ -531,34 +595,24 @@
     }
 
     /**
-     * Returns `true` if the given path contains an `android/build.gradle` file.
-     */
-    private Boolean doesSupportAndroidPlatform(String path) {
-        File editableAndroidProject = new File(path, 'android' + File.separator + 'build.gradle')
-        return editableAndroidProject.exists()
-    }
-
-    /**
      * Add the dependencies on other plugin projects to the plugin project.
      * A plugin A can depend on plugin B. As a result, this dependency must be surfaced by
      * making the Gradle plugin project A depend on the Gradle plugin project B.
      */
-    private void configurePluginDependencies(Object dependencyObject) {
-        assert(dependencyObject.name instanceof String)
-        Project pluginProject = project.rootProject.findProject(":${dependencyObject.name}")
-        if (pluginProject == null ||
-            !doesSupportAndroidPlatform(pluginProject.projectDir.parentFile.path)) {
+    private void configurePluginDependencies(Map<String, Object> pluginObject) {
+        assert(pluginObject.name instanceof String)
+        Project pluginProject = project.rootProject.findProject(":${pluginObject.name}")
+        if (pluginProject == null) {
             return
         }
-        assert(dependencyObject.dependencies instanceof List)
-        dependencyObject.dependencies.each { pluginDependencyName ->
-            assert(pluginDependencyName instanceof String)
+        def dependencies = pluginObject.dependencies
+        assert(dependencies instanceof List<String>)
+        dependencies.each { pluginDependencyName ->
             if (pluginDependencyName.empty) {
                 return
             }
             Project dependencyProject = project.rootProject.findProject(":$pluginDependencyName")
-            if (dependencyProject == null ||
-                !doesSupportAndroidPlatform(dependencyProject.projectDir.parentFile.path)) {
+            if (dependencyProject == null) {
                 return
             }
             // Wait for the Android plugin to load and add the dependency to the plugin project.
@@ -570,52 +624,27 @@
         }
     }
 
-    private Properties getPluginList() {
-        File pluginsFile = new File(project.projectDir.parentFile.parentFile, '.flutter-plugins')
-        Properties allPlugins = readPropertiesIfExist(pluginsFile)
-        Properties androidPlugins = new Properties()
-        allPlugins.each { name, path ->
-            if (doesSupportAndroidPlatform(path)) {
-                androidPlugins.setProperty(name, path)
-            }
-        // TODO(amirh): log an error if this plugin was specified to be an Android
-        // plugin according to the new schema, and was missing a build.gradle file.
-        // https://github.com/flutter/flutter/issues/40784
-        }
-        return androidPlugins
+    /**
+     * Gets the list of plugins (as map) that support the Android platform.
+     *
+     * The map value contains either the plugins `name` (String),
+     * its `path` (String), or its `dependencies` (List<String>).
+     * See [NativePluginLoader#getPlugins] in packages/flutter_tools/gradle/src/main/groovy/native_plugin_loader.groovy
+     */
+    private List<Map<String, Object>> getPluginList(Project project) {
+        return project.ext.nativePluginLoader.getPlugins(getFlutterSourceDirectory())
     }
 
+    // TODO(54566, 48918): Remove in favor of [getPluginList] only, see also
+    //  https://github.com/flutter/flutter/blob/1c90ed8b64d9ed8ce2431afad8bc6e6d9acc4556/packages/flutter_tools/lib/src/flutter_plugins.dart#L212
     /** Gets the plugins dependencies from `.flutter-plugins-dependencies`. */
-    private List getPluginDependencies() {
-        // Consider a `.flutter-plugins-dependencies` file with the following content:
-        // {
-        //     "dependencyGraph": [
-        //       {
-        //         "name": "plugin-a",
-        //         "dependencies": ["plugin-b","plugin-c"]
-        //       },
-        //       {
-        //         "name": "plugin-b",
-        //         "dependencies": ["plugin-c"]
-        //       },
-        //       {
-        //         "name": "plugin-c",
-        //         "dependencies": []'
-        //       }
-        //     ]
-        //  }
-        //
-        // This means, `plugin-a` depends on `plugin-b` and `plugin-c`.
-        // `plugin-b` depends on `plugin-c`.
-        // `plugin-c` doesn't depend on anything.
-        File pluginsDependencyFile = new File(project.projectDir.parentFile.parentFile, '.flutter-plugins-dependencies')
-        if (pluginsDependencyFile.exists()) {
-            def object = new JsonSlurper().parseText(pluginsDependencyFile.text)
-            assert(object instanceof Map)
-            assert(object.dependencyGraph instanceof List)
-            return object.dependencyGraph
+    private List<Map<String, Object>> getPluginDependencies(Project project) {
+        Map meta = project.ext.nativePluginLoader.getDependenciesMetadata(getFlutterSourceDirectory())
+        if (meta == null) {
+            return []
         }
-        return []
+        assert(meta.dependencyGraph instanceof List<Map>)
+        return meta.dependencyGraph as List<Map<String, Object>>
     }
 
     private static String toCamelCase(List<String> parts) {
@@ -1226,7 +1255,7 @@
                 def nativeAssetsDir = "${project.buildDir}/../native_assets/android/jniLibs/lib/"
                 project.android.sourceSets.main.jniLibs.srcDir(nativeAssetsDir)
             }
-            configurePlugins()
+            configurePlugins(project)
             detectLowCompileSdkVersionOrNdkVersion()
             return
         }
@@ -1278,7 +1307,7 @@
                 }
             }
         }
-        configurePlugins()
+        configurePlugins(project)
         detectLowCompileSdkVersionOrNdkVersion()
     }
 
diff --git a/packages/flutter_tools/gradle/src/main/groovy/native_plugin_loader.groovy b/packages/flutter_tools/gradle/src/main/groovy/native_plugin_loader.groovy
new file mode 100644
index 0000000..f9eb7bb
--- /dev/null
+++ b/packages/flutter_tools/gradle/src/main/groovy/native_plugin_loader.groovy
@@ -0,0 +1,120 @@
+// Copyright 2014 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 groovy.json.JsonSlurper
+
+@Singleton
+class NativePluginLoader {
+
+    // This string must match _kFlutterPluginsHasNativeBuildKey defined in
+    // packages/flutter_tools/lib/src/flutter_plugins.dart.
+    static final String nativeBuildKey = "native_build"
+    static final String flutterPluginsDependenciesFile = ".flutter-plugins-dependencies"
+
+    /**
+     * Gets the list of plugins that support the Android platform.
+     * The list contains map elements with the following content:
+     * {
+     *     "name": "plugin-a",
+     *     "path": "/path/to/plugin-a",
+     *     "dependencies": ["plugin-b", "plugin-c"],
+     *     "native_build": true
+     * }
+     *
+     * Therefore the map value can either be a `String`, a `List<String>` or a `boolean`.
+     */
+    List<Map<String, Object>> getPlugins(File flutterSourceDirectory) {
+        List<Map<String, Object>> nativePlugins = []
+        def meta = getDependenciesMetadata(flutterSourceDirectory)
+        if (meta == null) {
+            return nativePlugins
+        }
+
+        assert(meta.plugins instanceof Map<String, Object>)
+        def androidPlugins = meta.plugins.android
+        assert(androidPlugins instanceof List<Map>)
+        // Includes the Flutter plugins that support the Android platform.
+        androidPlugins.each { Map<String, Object> androidPlugin ->
+            // The property types can be found in _filterPluginsByPlatform defined in
+            // packages/flutter_tools/lib/src/flutter_plugins.dart.
+            assert(androidPlugin.name instanceof String)
+            assert(androidPlugin.path instanceof String)
+            assert(androidPlugin.dependencies instanceof List<String>)
+            // Skip plugins that have no native build (such as a Dart-only implementation
+            // of a federated plugin).
+            def needsBuild = androidPlugin.containsKey(nativeBuildKey) ? androidPlugin[nativeBuildKey] : true
+            if (needsBuild) {
+                nativePlugins.add(androidPlugin)
+            }
+        }
+        return nativePlugins
+    }
+
+
+    private Map<String, Object> parsedFlutterPluginsDependencies
+
+    /**
+     * Parses <project-src>/.flutter-plugins-dependencies
+     */
+    Map<String, Object> getDependenciesMetadata(File flutterSourceDirectory) {
+        // Consider a `.flutter-plugins-dependencies` file with the following content:
+        // {
+        //     "plugins": {
+        //       "android": [
+        //         {
+        //           "name": "plugin-a",
+        //           "path": "/path/to/plugin-a",
+        //           "dependencies": ["plugin-b", "plugin-c"],
+        //           "native_build": true
+        //         },
+        //         {
+        //           "name": "plugin-b",
+        //           "path": "/path/to/plugin-b",
+        //           "dependencies": ["plugin-c"],
+        //           "native_build": true
+        //         },
+        //         {
+        //           "name": "plugin-c",
+        //           "path": "/path/to/plugin-c",
+        //           "dependencies": [],
+        //           "native_build": true
+        //         },
+        //       ],
+        //     },
+        //     "dependencyGraph": [
+        //       {
+        //         "name": "plugin-a",
+        //         "dependencies": ["plugin-b","plugin-c"]
+        //       },
+        //       {
+        //         "name": "plugin-b",
+        //         "dependencies": ["plugin-c"]
+        //       },
+        //       {
+        //         "name": "plugin-c",
+        //         "dependencies": []
+        //       }
+        //     ]
+        // }
+        // This means, `plugin-a` depends on `plugin-b` and `plugin-c`.
+        // `plugin-b` depends on `plugin-c`.
+        // `plugin-c` doesn't depend on anything.
+        if (parsedFlutterPluginsDependencies) {
+            return parsedFlutterPluginsDependencies
+        }
+        File pluginsDependencyFile = new File(flutterSourceDirectory, flutterPluginsDependenciesFile)
+        if (pluginsDependencyFile.exists()) {
+            def object = new JsonSlurper().parseText(pluginsDependencyFile.text)
+            assert(object instanceof Map<String, Object>)
+            parsedFlutterPluginsDependencies = object
+            return object
+        }
+        return null
+    }
+}
+
+// TODO(135392): Remove and use declarative form when migrated
+ext {
+    nativePluginLoader = NativePluginLoader.instance
+}
diff --git a/packages/flutter_tools/test/integration.shard/android_plugin_skip_unsupported_test.dart b/packages/flutter_tools/test/integration.shard/android_plugin_skip_unsupported_test.dart
new file mode 100644
index 0000000..4fdc521
--- /dev/null
+++ b/packages/flutter_tools/test/integration.shard/android_plugin_skip_unsupported_test.dart
@@ -0,0 +1,208 @@
+// Copyright 2014 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:file_testing/file_testing.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/cache.dart';
+
+import '../src/common.dart';
+import 'test_data/plugin_each_settings_gradle_project.dart';
+import 'test_data/plugin_project.dart';
+import 'test_data/project.dart';
+import 'test_utils.dart';
+
+void main() {
+  late Directory tempDir;
+
+  setUp(() {
+    Cache.flutterRoot = getFlutterRoot();
+    tempDir = createResolvedTempDirectorySync('flutter_plugin_test.');
+  });
+
+  tearDown(() async {
+    tryToDelete(tempDir);
+  });
+
+  // Regression test for https://github.com/flutter/flutter/issues/97729 (#137115).
+  /// Creates a project which uses a plugin, which is not supported on Android.
+  /// This means it has no entry in pubspec.yaml for:
+  /// flutter -> plugin -> platforms -> android
+  ///
+  /// [createAndroidPluginFolder] indicates that the plugin can additionally
+  /// have a functioning `android` folder.
+  Future<ProcessResult> testUnsupportedPlugin({
+    required Project project,
+    required bool createAndroidPluginFolder,
+  }) async {
+    final String flutterBin = fileSystem.path.join(
+      getFlutterRoot(),
+      'bin',
+      'flutter',
+    );
+
+    // Create dummy plugin that supports iOS and optionally Android.
+    processManager.runSync(<String>[
+      flutterBin,
+      ...getLocalEngineArguments(),
+      'create',
+      '--template=plugin',
+      '--platforms=ios${createAndroidPluginFolder ? ',android' : ''}',
+      'test_plugin',
+    ], workingDirectory: tempDir.path);
+
+    final Directory pluginAppDir = tempDir.childDirectory('test_plugin');
+
+    final File pubspecFile = pluginAppDir.childFile('pubspec.yaml');
+    String pubspecYamlSrc =
+        pubspecFile.readAsStringSync().replaceAll('\r\n', '\n');
+    if (createAndroidPluginFolder) {
+      // Override pubspec to drop support for the Android implementation.
+      pubspecYamlSrc = pubspecYamlSrc
+          .replaceFirst(
+        RegExp(r'name:.*\n'),
+        'name: test_plugin\n',
+      )
+          .replaceFirst('''
+      android:
+        package: com.example.test_plugin
+        pluginClass: TestPlugin
+''', '''
+#      android:
+#        package: com.example.test_plugin
+#        pluginClass: TestPlugin
+''');
+
+      pubspecFile.writeAsStringSync(pubspecYamlSrc);
+
+      // Check the android directory and the build.gradle file within.
+      final File pluginGradleFile =
+          pluginAppDir.childDirectory('android').childFile('build.gradle');
+      expect(pluginGradleFile, exists);
+    } else {
+      expect(pubspecYamlSrc, isNot(contains('android:')));
+    }
+
+    // Create a project which includes the plugin to test against
+    final Directory pluginExampleAppDir =
+        pluginAppDir.childDirectory('example');
+
+    await project.setUpIn(pluginExampleAppDir);
+
+    // Run flutter build apk to build plugin example project.
+    return processManager.runSync(<String>[
+      flutterBin,
+      ...getLocalEngineArguments(),
+      'build',
+      'apk',
+      '--debug',
+    ], workingDirectory: pluginExampleAppDir.path);
+  }
+
+  test('skip plugin if it does not support the Android platform', () async {
+    final Project project = PluginWithPathAndroidProject();
+    final ProcessResult buildApkResult = await testUnsupportedPlugin(
+        project: project, createAndroidPluginFolder: false);
+    expect(buildApkResult.stderr.toString(),
+        isNot(contains('Please fix your settings.gradle.')));
+    expect(buildApkResult, const ProcessResultMatcher());
+  });
+
+  test(
+      'skip plugin with android folder if it does not support the Android platform',
+      () async {
+    final Project project = PluginWithPathAndroidProject();
+    final ProcessResult buildApkResult = await testUnsupportedPlugin(
+        project: project, createAndroidPluginFolder: true);
+    expect(buildApkResult.stderr.toString(),
+        isNot(contains('Please fix your settings.gradle.')));
+    expect(buildApkResult, const ProcessResultMatcher());
+  });
+
+  // TODO(54566): Remove test when issue is resolved.
+  /// Test project with a `settings.gradle` (PluginEach) that apps were created
+  /// with until Flutter v1.22.0.
+  /// It uses the `.flutter-plugins` file to load EACH plugin.
+  test(
+      'skip plugin if it does not support the Android platform with a _plugin.each_ settings.gradle',
+      () async {
+    final Project project = PluginEachWithPathAndroidProject();
+    final ProcessResult buildApkResult = await testUnsupportedPlugin(
+        project: project, createAndroidPluginFolder: false);
+    expect(buildApkResult.stderr.toString(),
+        isNot(contains('Please fix your settings.gradle.')));
+    expect(buildApkResult, const ProcessResultMatcher());
+  });
+
+  // TODO(54566): Remove test when issue is resolved.
+  /// Test project with a `settings.gradle` (PluginEach) that apps were created
+  /// with until Flutter v1.22.0.
+  /// It uses the `.flutter-plugins` file to load EACH plugin.
+  /// The plugin includes a functional 'android' folder.
+  test(
+      'skip plugin with android folder if it does not support the Android platform with a _plugin.each_ settings.gradle',
+      () async {
+    final Project project = PluginEachWithPathAndroidProject();
+    final ProcessResult buildApkResult = await testUnsupportedPlugin(
+        project: project, createAndroidPluginFolder: true);
+    expect(buildApkResult.stderr.toString(),
+        isNot(contains('Please fix your settings.gradle.')));
+    expect(buildApkResult, const ProcessResultMatcher());
+  });
+
+  // TODO(54566): Remove test when issue is resolved.
+  /// Test project with a `settings.gradle` (PluginEach) that apps were created
+  /// with until Flutter v1.22.0.
+  /// It is compromised by removing the 'include' statement of the plugins.
+  /// As the "'.flutter-plugins'" keyword is still present, the framework
+  /// assumes that all plugins are included, which is not the case.
+  /// Therefore it should throw an error.
+  test(
+      'skip plugin if it does not support the Android platform with a compromised _plugin.each_ settings.gradle',
+      () async {
+    final Project project = PluginCompromisedEachWithPathAndroidProject();
+    final ProcessResult buildApkResult = await testUnsupportedPlugin(
+        project: project, createAndroidPluginFolder: true);
+    expect(
+      buildApkResult,
+      const ProcessResultMatcher(
+          stderrPattern: 'Please fix your settings.gradle.'),
+    );
+  });
+}
+
+const String pubspecWithPluginPath = r'''
+name: test
+environment:
+  sdk: '>=3.2.0-0 <4.0.0'
+dependencies:
+  flutter:
+    sdk: flutter
+
+  test_plugin:
+    path: ../
+''';
+
+/// Project that load's a plugin from the specified path.
+class PluginWithPathAndroidProject extends PluginProject {
+  @override
+  String get pubspec => pubspecWithPluginPath;
+}
+
+// TODO(54566): Remove class when issue is resolved.
+/// [PluginEachSettingsGradleProject] that load's a plugin from the specified
+/// path.
+class PluginEachWithPathAndroidProject extends PluginEachSettingsGradleProject {
+  @override
+  String get pubspec => pubspecWithPluginPath;
+}
+
+// TODO(54566): Remove class when issue is resolved.
+/// [PluginCompromisedEachSettingsGradleProject] that load's a plugin from the
+/// specified path.
+class PluginCompromisedEachWithPathAndroidProject
+    extends PluginCompromisedEachSettingsGradleProject {
+  @override
+  String get pubspec => pubspecWithPluginPath;
+}
diff --git a/packages/flutter_tools/test/integration.shard/test_data/plugin_each_settings_gradle_project.dart b/packages/flutter_tools/test/integration.shard/test_data/plugin_each_settings_gradle_project.dart
new file mode 100644
index 0000000..7747bed
--- /dev/null
+++ b/packages/flutter_tools/test/integration.shard/test_data/plugin_each_settings_gradle_project.dart
@@ -0,0 +1,60 @@
+// Copyright 2014 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.
+
+// TODO(54566): Remove this file when issue is resolved.
+
+import 'deferred_components_config.dart';
+import 'plugin_project.dart';
+
+/// Project to test the deprecated `settings.gradle` (PluginEach) that apps were
+/// created with until Flutter v1.22.0.
+/// It uses the `.flutter-plugins` file to load EACH plugin.
+class PluginEachSettingsGradleProject extends PluginProject {
+  @override
+  DeferredComponentsConfig get deferredComponents =>
+      PluginEachSettingsGradleDeferredComponentsConfig();
+}
+
+class PluginEachSettingsGradleDeferredComponentsConfig
+    extends PluginDeferredComponentsConfig {
+  @override
+  String get androidSettings => r'''
+include ':app'
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+    pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
+}
+plugins.each { name, path ->
+    def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+    include ":$name"
+    project(":$name").projectDir = pluginDirectory
+}
+  ''';
+}
+
+/// Project to test the deprecated `settings.gradle` (PluginEach) that apps were
+/// created with until Flutter v1.22.0.
+/// It uses the `.flutter-plugins` file to get EACH plugin.
+/// It is compromised by removing the 'include' statement of the plugins.
+class PluginCompromisedEachSettingsGradleProject extends PluginProject {
+  @override
+  DeferredComponentsConfig get deferredComponents =>
+      PluginCompromisedEachSettingsGradleDeferredComponentsConfig();
+}
+
+class PluginCompromisedEachSettingsGradleDeferredComponentsConfig
+    extends PluginDeferredComponentsConfig {
+  @override
+  String get androidSettings => r'''
+include ':app'
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+    pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
+}
+  ''';
+}
diff --git a/packages/flutter_tools/test/integration.shard/test_data/plugin_project.dart b/packages/flutter_tools/test/integration.shard/test_data/plugin_project.dart
new file mode 100644
index 0000000..2714ca6
--- /dev/null
+++ b/packages/flutter_tools/test/integration.shard/test_data/plugin_project.dart
@@ -0,0 +1,99 @@
+// Copyright 2014 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 'basic_project.dart';
+import 'deferred_components_config.dart';
+import 'deferred_components_project.dart';
+
+/// Project which can load native plugins
+class PluginProject extends BasicProject {
+  @override
+  final DeferredComponentsConfig? deferredComponents =
+      PluginDeferredComponentsConfig();
+}
+
+class PluginDeferredComponentsConfig extends BasicDeferredComponentsConfig {
+  @override
+  String get androidBuild => r'''
+buildscript {
+    ext.kotlin_version = '1.7.10'
+    repositories {
+        google()
+        mavenCentral()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:7.3.0'
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+    }
+    configurations.classpath {
+        resolutionStrategy.activateDependencyLocking()
+    }
+}
+allprojects {
+    repositories {
+        google()
+        mavenCentral()
+    }
+}
+rootProject.buildDir = '../build'
+subprojects {
+    project.buildDir = "${rootProject.buildDir}/${project.name}"
+}
+subprojects {
+    project.evaluationDependsOn(':app')
+    dependencyLocking {
+        ignoredDependencies.add('io.flutter:*')
+        lockFile = file("${rootProject.projectDir}/project-${project.name}.lockfile")
+        if (!project.hasProperty('local-engine-repo')) {
+          lockAllConfigurations()
+        }
+    }
+}
+tasks.register("clean", Delete) {
+    delete rootProject.buildDir
+}
+''';
+
+  @override
+  String get androidSettings => r'''
+include ':app'
+
+def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
+def properties = new Properties()
+
+assert localPropertiesFile.exists()
+localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
+
+def flutterSdkPath = properties.getProperty("flutter.sdk")
+assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
+''';
+
+  @override
+  String get appManifest => r'''
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.yourcompany.flavors">
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <application
+        android:name="${applicationName}"
+        android:label="flavors">
+        <activity
+            android:name=".MainActivity"
+            android:exported="true"
+            android:launchMode="singleTop"
+            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+            android:hardwareAccelerated="true"
+            android:windowSoftInputMode="adjustResize">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+        <meta-data
+            android:name="flutterEmbedding"
+            android:value="2" />
+    </application>
+</manifest>
+''';
+}