Reland "[flutter_tools] Removes the need of a no-op plugin implementations #48614" (#49085)

diff --git a/dev/devicelab/bin/tasks/plugin_dependencies_test.dart b/dev/devicelab/bin/tasks/plugin_dependencies_test.dart
index f3714f2..6e32c2d 100644
--- a/dev/devicelab/bin/tasks/plugin_dependencies_test.dart
+++ b/dev/devicelab/bin/tasks/plugin_dependencies_test.dart
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:convert';
 import 'dart:io';
 
 import 'package:flutter_devicelab/framework/framework.dart';
@@ -74,10 +75,6 @@
         );
       });
 
-      // https://github.com/flutter/flutter/issues/46898
-      // https://github.com/flutter/flutter/issues/39657
-      File(path.join(pluginCDirectory.path, 'android', 'build.gradle')).deleteSync();
-
       final File pluginCpubspec = File(path.join(pluginCDirectory.path, 'pubspec.yaml'));
       await pluginCpubspec.writeAsString('''
 name: plugin_c
@@ -177,30 +174,32 @@
       }
 
       final String flutterPluginsDependenciesFileContent = flutterPluginsDependenciesFile.readAsStringSync();
-      const String kExpectedPluginsDependenciesContent =
-        '{'
-          '\"_info\":\"// This is a generated file; do not edit or check into version control.\",'
-          '\"dependencyGraph\":['
-            '{'
-              '\"name\":\"plugin_a\",'
-              '\"dependencies\":[\"plugin_b\",\"plugin_c\"]'
-            '},'
-            '{'
-              '\"name\":\"plugin_b\",'
-              '\"dependencies\":[]'
-            '},'
-            '{'
-              '\"name\":\"plugin_c\",'
-              '\"dependencies\":[]'
-            '}'
-          ']'
-        '}';
 
-      if (flutterPluginsDependenciesFileContent != kExpectedPluginsDependenciesContent) {
+      final Map<String, dynamic> jsonContent = json.decode(flutterPluginsDependenciesFileContent) as Map<String, dynamic>;
+
+      // Verify the dependencyGraph object is valid. The rest of the contents of this file are not relevant to the
+      // dependency graph and are tested by unit tests.
+      final List<dynamic> dependencyGraph = jsonContent['dependencyGraph'] as List<dynamic>;
+      const String kExpectedPluginsDependenciesContent =
+        '['
+          '{'
+            '\"name\":\"plugin_a\",'
+            '\"dependencies\":[\"plugin_b\",\"plugin_c\"]'
+          '},'
+          '{'
+            '\"name\":\"plugin_b\",'
+            '\"dependencies\":[]'
+          '},'
+          '{'
+            '\"name\":\"plugin_c\",'
+            '\"dependencies\":[]'
+          '}'
+        ']';
+      final String graphString = json.encode(dependencyGraph);
+      if (graphString != kExpectedPluginsDependenciesContent) {
         return TaskResult.failure(
           'Unexpected file content in ${flutterPluginsDependenciesFile.path}: '
-          'Found "$flutterPluginsDependenciesFileContent" instead of '
-          '"$kExpectedPluginsDependenciesContent"'
+          'Found "$graphString" instead of "$kExpectedPluginsDependenciesContent"'
         );
       }
 
diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart
index efee39f..e3aa595 100644
--- a/packages/flutter_tools/lib/src/plugins.dart
+++ b/packages/flutter_tools/lib/src/plugins.dart
@@ -11,6 +11,7 @@
 import 'android/gradle.dart';
 import 'base/common.dart';
 import 'base/file_system.dart';
+import 'base/time.dart';
 import 'convert.dart';
 import 'dart/package_map.dart';
 import 'features.dart';
@@ -18,6 +19,7 @@
 import 'macos/cocoapods.dart';
 import 'platform_plugins.dart';
 import 'project.dart';
+import 'version.dart';
 
 void _renderTemplateToFile(String template, dynamic context, String filePath) {
   final String renderedTemplate =
@@ -263,7 +265,7 @@
   final Map<String, PluginPlatform> platforms;
 }
 
-Plugin _pluginFromPubspec(String name, Uri packageRoot) {
+Plugin _pluginFromPackage(String name, Uri packageRoot) {
   final String pubspecPath = globals.fs.path.fromUri(packageRoot.resolve('pubspec.yaml'));
   if (!globals.fs.isFileSync(pubspecPath)) {
     return null;
@@ -302,7 +304,7 @@
   }
   packages.forEach((String name, Uri uri) {
     final Uri packageRoot = uri.resolve('..');
-    final Plugin plugin = _pluginFromPubspec(name, packageRoot);
+    final Plugin plugin = _pluginFromPackage(name, packageRoot);
     if (plugin != null) {
       plugins.add(plugin);
     }
@@ -310,55 +312,163 @@
   return plugins;
 }
 
-/// Writes the .flutter-plugins and .flutter-plugins-dependencies files based on the list of plugins.
-/// If there aren't any plugins, then the files aren't written to disk.
-///
-/// Finally, returns [true] if .flutter-plugins or .flutter-plugins-dependencies have changed,
-/// otherwise returns [false].
-bool _writeFlutterPluginsList(FlutterProject project, List<Plugin> plugins) {
-  final List<dynamic> directAppDependencies = <dynamic>[];
-  const String info = 'This is a generated file; do not edit or check into version control.';
-  final StringBuffer flutterPluginsBuffer = StringBuffer('# $info\n');
+  /// Filters [plugins] to those supported by [platformKey].
+  List<Map<String, dynamic>> _filterPluginsByPlatform(List<Plugin>plugins, String platformKey) {
+    final Iterable<Plugin> platformPlugins = plugins.where((Plugin p) {
+      return p.platforms.containsKey(platformKey);
+    });
 
-  final Set<String> pluginNames = <String>{};
-  for (final Plugin plugin in plugins) {
-    pluginNames.add(plugin.name);
+    final Set<String> pluginNames = platformPlugins.map((Plugin plugin) => plugin.name).toSet();
+    final List<Map<String, dynamic>> list = <Map<String, dynamic>>[];
+    for (final Plugin plugin in platformPlugins) {
+      list.add(<String, dynamic>{
+        'name': plugin.name,
+        'path': fsUtils.escapePath(plugin.path),
+        'dependencies': <String>[...plugin.dependencies.where(pluginNames.contains)],
+      });
+    }
+    return list;
   }
+
+/// Writes the .flutter-plugins-dependencies file based on the list of plugins.
+/// If there aren't any plugins, then the files aren't written to disk. The resulting
+/// file looks something like this (order of keys is not guaranteed):
+/// {
+///   "info": "This is a generated file; do not edit or check into version control.",
+///   "plugins": {
+///     "ios": [
+///       {
+///         "name": "test",
+///         "path": "test_path",
+///         "dependencies": [
+///           "plugin-a",
+///           "plugin-b"
+///         ]
+///       }
+///     ],
+///     "android": [],
+///     "macos": [],
+///     "linux": [],
+///     "windows": [],
+///     "web": []
+///   },
+///   "dependencyGraph": [
+///     {
+///       "name": "plugin-a",
+///       "dependencies": [
+///         "plugin-b",
+///         "plugin-c"
+///       ]
+///     },
+///     {
+///       "name": "plugin-b",
+///       "dependencies": [
+///         "plugin-c"
+///       ]
+///     },
+///     {
+///       "name": "plugin-c",
+///       "dependencies": []
+///     }
+///   ],
+///   "date_created": "1970-01-01 00:00:00.000",
+///   "version": "0.0.0-unknown"
+/// }
+///
+///
+/// Finally, returns [true] if the plugins list has changed, otherwise returns [false].
+bool _writeFlutterPluginsList(FlutterProject project, List<Plugin> plugins) {
+  final File pluginsFile = project.flutterPluginsDependenciesFile;
+  if (plugins.isEmpty) {
+    if (pluginsFile.existsSync()) {
+      pluginsFile.deleteSync();
+      return true;
+    }
+    return false;
+  }
+
+  final String iosKey = project.ios.pluginConfigKey;
+  final String androidKey = project.android.pluginConfigKey;
+  final String macosKey = project.macos.pluginConfigKey;
+  final String linuxKey = project.linux.pluginConfigKey;
+  final String windowsKey = project.windows.pluginConfigKey;
+  final String webKey = project.web.pluginConfigKey;
+
+  final Map<String, dynamic> pluginsMap = <String, dynamic>{};
+  pluginsMap[iosKey] = _filterPluginsByPlatform(plugins, iosKey);
+  pluginsMap[androidKey] = _filterPluginsByPlatform(plugins, androidKey);
+  pluginsMap[macosKey] = _filterPluginsByPlatform(plugins, macosKey);
+  pluginsMap[linuxKey] = _filterPluginsByPlatform(plugins, linuxKey);
+  pluginsMap[windowsKey] = _filterPluginsByPlatform(plugins, windowsKey);
+  pluginsMap[webKey] = _filterPluginsByPlatform(plugins, webKey);
+
+  final Map<String, dynamic> result = <String, dynamic> {};
+
+  result['info'] =  'This is a generated file; do not edit or check into version control.';
+  result['plugins'] = pluginsMap;
+  /// The dependencyGraph object is kept for backwards compatibility, but
+  /// should be removed once migration is complete.
+  /// https://github.com/flutter/flutter/issues/48918
+  result['dependencyGraph'] = _createPluginLegacyDependencyGraph(plugins);
+  result['date_created'] = systemClock.now().toString();
+  result['version'] = flutterVersion.frameworkVersion;
+
+  // Only notify if the plugins list has changed. [date_created] will always be different,
+  // [version] is not relevant for this check.
+  final String oldPluginsFileStringContent = _readFileContent(pluginsFile);
+  bool pluginsChanged = true;
+  if (oldPluginsFileStringContent != null) {
+    pluginsChanged = oldPluginsFileStringContent.contains(pluginsMap.toString());
+  }
+  final String pluginFileContent = json.encode(result);
+  pluginsFile.writeAsStringSync(pluginFileContent, flush: true);
+
+  return pluginsChanged;
+}
+
+List<dynamic> _createPluginLegacyDependencyGraph(List<Plugin> plugins) {
+  final List<dynamic> directAppDependencies = <dynamic>[];
+
+  final Set<String> pluginNames = plugins.map((Plugin plugin) => plugin.name).toSet();
   for (final Plugin plugin in plugins) {
-    flutterPluginsBuffer.write('${plugin.name}=${fsUtils.escapePath(plugin.path)}\n');
     directAppDependencies.add(<String, dynamic>{
       'name': plugin.name,
       // Extract the plugin dependencies which happen to be plugins.
       'dependencies': <String>[...plugin.dependencies.where(pluginNames.contains)],
     });
   }
+  return directAppDependencies;
+}
+
+// The .flutter-plugins file will be DEPRECATED in favor of .flutter-plugins-dependencies.
+// TODO(franciscojma): Remove this method once deprecated.
+// https://github.com/flutter/flutter/issues/48918
+//
+/// Writes the .flutter-plugins files based on the list of plugins.
+/// If there aren't any plugins, then the files aren't written to disk.
+///
+/// Finally, returns [true] if .flutter-plugins has changed, otherwise returns [false].
+bool _writeFlutterPluginsListLegacy(FlutterProject project, List<Plugin> plugins) {
   final File pluginsFile = project.flutterPluginsFile;
-  final String oldPluginFileContent = _readFileContent(pluginsFile);
-  final String pluginFileContent = flutterPluginsBuffer.toString();
-  if (pluginNames.isNotEmpty) {
-    pluginsFile.writeAsStringSync(pluginFileContent, flush: true);
-  } else {
+  if (plugins.isEmpty) {
     if (pluginsFile.existsSync()) {
       pluginsFile.deleteSync();
+      return true;
     }
+    return false;
   }
 
-  final File dependenciesFile = project.flutterPluginsDependenciesFile;
-  final String oldDependenciesFileContent = _readFileContent(dependenciesFile);
-  final String dependenciesFileContent = json.encode(<String, dynamic>{
-      '_info': '// $info',
-      'dependencyGraph': directAppDependencies,
-    });
-  if (pluginNames.isNotEmpty) {
-    dependenciesFile.writeAsStringSync(dependenciesFileContent, flush: true);
-  } else {
-    if (dependenciesFile.existsSync()) {
-      dependenciesFile.deleteSync();
-    }
-  }
+  const String info = 'This is a generated file; do not edit or check into version control.';
+  final StringBuffer flutterPluginsBuffer = StringBuffer('# $info\n');
 
-  return oldPluginFileContent != _readFileContent(pluginsFile)
-      || oldDependenciesFileContent != _readFileContent(dependenciesFile);
+  for (final Plugin plugin in plugins) {
+    flutterPluginsBuffer.write('${plugin.name}=${fsUtils.escapePath(plugin.path)}\n');
+  }
+  final String oldPluginFileContent = _readFileContent(pluginsFile);
+  final String pluginFileContent = flutterPluginsBuffer.toString();
+  pluginsFile.writeAsStringSync(pluginFileContent, flush: true);
+
+  return oldPluginFileContent != _readFileContent(pluginsFile);
 }
 
 /// Returns the contents of [File] or [null] if that file does not exist.
@@ -782,8 +892,13 @@
 /// Assumes `pub get` has been executed since last change to `pubspec.yaml`.
 void refreshPluginsList(FlutterProject project, {bool checkProjects = false}) {
   final List<Plugin> plugins = findPlugins(project);
+
+  // TODO(franciscojma): Remove once migration is complete.
+  // Write the legacy plugin files to avoid breaking existing apps.
+  final bool legacyChanged = _writeFlutterPluginsListLegacy(project, plugins);
+
   final bool changed = _writeFlutterPluginsList(project, plugins);
-  if (changed) {
+  if (changed || legacyChanged) {
     if (!checkProjects || project.ios.existsSync()) {
       cocoaPods.invalidatePodInstallOutput(project.ios);
     }
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
index fd8a480..f397a33 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -20,6 +20,7 @@
 import 'globals.dart' as globals;
 import 'ios/plist_parser.dart';
 import 'ios/xcodeproj.dart' as xcode;
+import 'platform_plugins.dart';
 import 'plugins.dart';
 import 'template.dart';
 
@@ -251,6 +252,16 @@
   }
 }
 
+/// Base class for projects per platform.
+abstract class FlutterProjectPlatform {
+
+  /// Plugin's platform config key, e.g., "macos", "ios".
+  String get pluginConfigKey;
+
+  /// Whether the platform exists in the project.
+  bool existsSync();
+}
+
 /// Represents an Xcode-based sub-project.
 ///
 /// This defines interfaces common to iOS and macOS projects.
@@ -300,12 +311,15 @@
 ///
 /// Instances will reflect the contents of the `ios/` sub-folder of
 /// Flutter applications and the `.ios/` sub-folder of Flutter module projects.
-class IosProject implements XcodeBasedProject {
+class IosProject extends FlutterProjectPlatform implements XcodeBasedProject {
   IosProject.fromFlutter(this.parent);
 
   @override
   final FlutterProject parent;
 
+  @override
+  String get pluginConfigKey => IOSPlugin.kConfigKey;
+
   static final RegExp _productBundleIdPattern = RegExp(r'''^\s*PRODUCT_BUNDLE_IDENTIFIER\s*=\s*(["']?)(.*?)\1;\s*$''');
   static const String _productBundleIdVariable = r'$(PRODUCT_BUNDLE_IDENTIFIER)';
   static const String _hostAppBundleName = 'Runner';
@@ -574,12 +588,15 @@
 ///
 /// Instances will reflect the contents of the `android/` sub-folder of
 /// Flutter applications and the `.android/` sub-folder of Flutter module projects.
-class AndroidProject {
+class AndroidProject extends FlutterProjectPlatform {
   AndroidProject._(this.parent);
 
   /// The parent of this project.
   final FlutterProject parent;
 
+  @override
+  String get pluginConfigKey => AndroidPlugin.kConfigKey;
+
   static final RegExp _applicationIdPattern = RegExp('^\\s*applicationId\\s+[\'\"](.*)[\'\"]\\s*\$');
   static final RegExp _kotlinPluginPattern = RegExp('^\\s*apply plugin\:\\s+[\'\"]kotlin-android[\'\"]\\s*\$');
   static final RegExp _groupPattern = RegExp('^\\s*group\\s+[\'\"](.*)[\'\"]\\s*\$');
@@ -627,6 +644,7 @@
   }
 
   /// Whether the current flutter project has an Android sub-project.
+  @override
   bool existsSync() {
     return parent.isModule || _editableHostAppDirectory.existsSync();
   }
@@ -760,12 +778,16 @@
 }
 
 /// Represents the web sub-project of a Flutter project.
-class WebProject {
+class WebProject extends FlutterProjectPlatform {
   WebProject._(this.parent);
 
   final FlutterProject parent;
 
+  @override
+  String get pluginConfigKey => WebPlugin.kConfigKey;
+
   /// Whether this flutter project has a web sub-project.
+  @override
   bool existsSync() {
     return parent.directory.childDirectory('web').existsSync()
       && indexFile.existsSync();
@@ -810,12 +832,15 @@
 }
 
 /// The macOS sub project.
-class MacOSProject implements XcodeBasedProject {
+class MacOSProject extends FlutterProjectPlatform implements XcodeBasedProject {
   MacOSProject._(this.parent);
 
   @override
   final FlutterProject parent;
 
+  @override
+  String get pluginConfigKey => MacOSPlugin.kConfigKey;
+
   static const String _hostAppBundleName = 'Runner';
 
   @override
@@ -895,11 +920,15 @@
 }
 
 /// The Windows sub project
-class WindowsProject {
+class WindowsProject extends FlutterProjectPlatform {
   WindowsProject._(this.project);
 
   final FlutterProject project;
 
+  @override
+  String get pluginConfigKey => WindowsPlugin.kConfigKey;
+
+  @override
   bool existsSync() => _editableDirectory.existsSync();
 
   Directory get _editableDirectory => project.directory.childDirectory('windows');
@@ -933,11 +962,14 @@
 }
 
 /// The Linux sub project.
-class LinuxProject {
+class LinuxProject extends FlutterProjectPlatform {
   LinuxProject._(this.project);
 
   final FlutterProject project;
 
+  @override
+  String get pluginConfigKey => LinuxPlugin.kConfigKey;
+
   Directory get _editableDirectory => project.directory.childDirectory('linux');
 
   /// The directory in the project that is managed by Flutter. As much as
@@ -950,6 +982,7 @@
   /// checked in should live here.
   Directory get ephemeralDirectory => managedDirectory.childDirectory('ephemeral');
 
+  @override
   bool existsSync() => _editableDirectory.existsSync();
 
   /// The Linux project makefile.
diff --git a/packages/flutter_tools/lib/src/version.dart b/packages/flutter_tools/lib/src/version.dart
index d6dc8c8..470f4ed 100644
--- a/packages/flutter_tools/lib/src/version.dart
+++ b/packages/flutter_tools/lib/src/version.dart
@@ -16,6 +16,8 @@
 import 'convert.dart';
 import 'globals.dart' as globals;
 
+FlutterVersion get flutterVersion => context.get<FlutterVersion>();
+
 class FlutterVersion {
   FlutterVersion([this._clock = const SystemClock()]) {
     _frameworkRevision = _runGit(gitLog(<String>['-n', '1', '--pretty=format:%H']).join(' '));
diff --git a/packages/flutter_tools/test/general.shard/plugins_test.dart b/packages/flutter_tools/test/general.shard/plugins_test.dart
index 635d0e9..2117656 100644
--- a/packages/flutter_tools/test/general.shard/plugins_test.dart
+++ b/packages/flutter_tools/test/general.shard/plugins_test.dart
@@ -2,13 +2,17 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:convert';
+
 import 'package:file/file.dart';
 import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/time.dart';
 import 'package:flutter_tools/src/dart/package_map.dart';
 import 'package:flutter_tools/src/features.dart';
 import 'package:flutter_tools/src/ios/xcodeproj.dart';
 import 'package:flutter_tools/src/plugins.dart';
 import 'package:flutter_tools/src/project.dart';
+import 'package:flutter_tools/src/version.dart';
 import 'package:meta/meta.dart';
 import 'package:mockito/mockito.dart';
 
@@ -23,15 +27,22 @@
     MockMacOSProject macosProject;
     MockAndroidProject androidProject;
     MockWebProject webProject;
+    MockWindowsProject windowsProject;
+    MockLinuxProject linuxProject;
     File packagesFile;
     Directory dummyPackageDirectory;
+    SystemClock mockClock;
+    FlutterVersion mockVersion;
 
     setUp(() async {
       fs = MemoryFileSystem();
+      mockClock = MockClock();
+      mockVersion = MockFlutterVersion();
 
       // Add basic properties to the Flutter project and subprojects
       flutterProject = MockFlutterProject();
       when(flutterProject.directory).thenReturn(fs.directory('/'));
+      // TODO(franciscojma): Remove logic for .flutter-plugins it's deprecated.
       when(flutterProject.flutterPluginsFile).thenReturn(flutterProject.directory.childFile('.flutter-plugins'));
       when(flutterProject.flutterPluginsDependenciesFile).thenReturn(flutterProject.directory.childFile('.flutter-plugins-dependencies'));
       iosProject = MockIosProject();
@@ -39,18 +50,41 @@
       when(iosProject.pluginRegistrantHost).thenReturn(flutterProject.directory.childDirectory('Runner'));
       when(iosProject.podfile).thenReturn(flutterProject.directory.childDirectory('ios').childFile('Podfile'));
       when(iosProject.podManifestLock).thenReturn(flutterProject.directory.childDirectory('ios').childFile('Podfile.lock'));
+      when(iosProject.pluginConfigKey).thenReturn('ios');
+      when(iosProject.existsSync()).thenReturn(false);
       macosProject = MockMacOSProject();
       when(flutterProject.macos).thenReturn(macosProject);
       when(macosProject.podfile).thenReturn(flutterProject.directory.childDirectory('macos').childFile('Podfile'));
       when(macosProject.podManifestLock).thenReturn(flutterProject.directory.childDirectory('macos').childFile('Podfile.lock'));
+      when(macosProject.pluginConfigKey).thenReturn('macos');
+      when(macosProject.existsSync()).thenReturn(false);
       androidProject = MockAndroidProject();
       when(flutterProject.android).thenReturn(androidProject);
       when(androidProject.pluginRegistrantHost).thenReturn(flutterProject.directory.childDirectory('android').childDirectory('app'));
       when(androidProject.hostAppGradleRoot).thenReturn(flutterProject.directory.childDirectory('android'));
+      when(androidProject.pluginConfigKey).thenReturn('android');
+      when(androidProject.existsSync()).thenReturn(false);
       webProject = MockWebProject();
       when(flutterProject.web).thenReturn(webProject);
       when(webProject.libDirectory).thenReturn(flutterProject.directory.childDirectory('lib'));
       when(webProject.existsSync()).thenReturn(true);
+      when(webProject.pluginConfigKey).thenReturn('web');
+      when(webProject.existsSync()).thenReturn(false);
+      windowsProject = MockWindowsProject();
+      when(flutterProject.windows).thenReturn(windowsProject);
+      when(windowsProject.pluginConfigKey).thenReturn('windows');
+      when(windowsProject.existsSync()).thenReturn(false);
+      linuxProject = MockLinuxProject();
+      when(flutterProject.linux).thenReturn(linuxProject);
+      when(linuxProject.pluginConfigKey).thenReturn('linux');
+      when(linuxProject.existsSync()).thenReturn(false);
+
+      when(mockClock.now()).thenAnswer(
+        (Invocation _) => DateTime(1970, 1, 1)
+      );
+      when(mockVersion.frameworkVersion).thenAnswer(
+        (Invocation _) => '1.0.0'
+      );
 
       // Set up a simple .packages file for all the tests to use, pointing to one package.
       dummyPackageDirectory = fs.directory('/pubcache/apackage/lib/');
@@ -67,6 +101,18 @@
       platforms:
         ios:
           pluginClass: FLESomePlugin
+        macos:
+          pluginClass: FLESomePlugin
+        windows:
+          pluginClass: FLESomePlugin
+        linux:
+          pluginClass: FLESomePlugin
+        web:
+          pluginClass: SomePlugin
+          fileName: lib/SomeFile.dart
+        android:
+          pluginClass: SomePlugin
+          package: AndroidPackage
   ''');
     }
 
@@ -239,8 +285,8 @@
 
       testUsingContext('Refreshing the plugin list deletes the plugin file when there were plugins but no longer are', () {
         flutterProject.flutterPluginsFile.createSync();
-        when(iosProject.existsSync()).thenReturn(false);
-        when(macosProject.existsSync()).thenReturn(false);
+        flutterProject.flutterPluginsDependenciesFile.createSync();
+
         refreshPluginsList(flutterProject);
         expect(flutterProject.flutterPluginsFile.existsSync(), false);
         expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), false);
@@ -251,8 +297,8 @@
 
       testUsingContext('Refreshing the plugin list creates a plugin directory when there are plugins', () {
         configureDummyPackageAsPlugin();
-        when(iosProject.existsSync()).thenReturn(false);
-        when(macosProject.existsSync()).thenReturn(false);
+        when(iosProject.existsSync()).thenReturn(true);
+
         refreshPluginsList(flutterProject);
         expect(flutterProject.flutterPluginsFile.existsSync(), true);
         expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), true);
@@ -265,11 +311,20 @@
         createPluginWithDependencies(name: 'plugin-a', dependencies: const <String>['plugin-b', 'plugin-c', 'random-package']);
         createPluginWithDependencies(name: 'plugin-b', dependencies: const <String>['plugin-c']);
         createPluginWithDependencies(name: 'plugin-c', dependencies: const <String>[]);
-        when(iosProject.existsSync()).thenReturn(false);
-        when(macosProject.existsSync()).thenReturn(false);
+        when(iosProject.existsSync()).thenReturn(true);
+
+        final DateTime dateCreated = DateTime(1970, 1, 1);
+        when(mockClock.now()).thenAnswer(
+          (Invocation _) => dateCreated
+        );
+        const String version = '1.0.0';
+        when(mockVersion.frameworkVersion).thenAnswer(
+          (Invocation _) => version
+        );
 
         refreshPluginsList(flutterProject);
 
+        // Verify .flutter-plugins-dependencies is configured correctly.
         expect(flutterProject.flutterPluginsFile.existsSync(), true);
         expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), true);
         expect(flutterProject.flutterPluginsFile.readAsStringSync(),
@@ -279,28 +334,79 @@
           'plugin-c=/.tmp_rand0/plugin.rand2/\n'
           ''
         );
-        expect(flutterProject.flutterPluginsDependenciesFile.readAsStringSync(),
-          '{'
-            '"_info":"// This is a generated file; do not edit or check into version control.",'
-            '"dependencyGraph":['
-              '{'
-                '"name":"plugin-a",'
-                '"dependencies":["plugin-b","plugin-c"]'
-              '},'
-              '{'
-                '"name":"plugin-b",'
-                '"dependencies":["plugin-c"]'
-              '},'
-              '{'
-                '"name":"plugin-c",'
-                '"dependencies":[]'
-              '}'
-            ']'
-          '}'
-        );
+
+        final String pluginsString = flutterProject.flutterPluginsDependenciesFile.readAsStringSync();
+        final Map<String, dynamic> jsonContent = json.decode(pluginsString) as  Map<String, dynamic>;
+        expect(jsonContent['info'], 'This is a generated file; do not edit or check into version control.');
+
+        final Map<String, dynamic> plugins = jsonContent['plugins'] as Map<String, dynamic>;
+        final List<dynamic> expectedPlugins = <dynamic>[
+          <String, dynamic> {
+            'name': 'plugin-a',
+            'path': '/.tmp_rand0/plugin.rand0/',
+            'dependencies': <String>[
+              'plugin-b',
+              'plugin-c'
+            ]
+          },
+          <String, dynamic> {
+            'name': 'plugin-b',
+            'path': '/.tmp_rand0/plugin.rand1/',
+            'dependencies': <String>[
+              'plugin-c'
+            ]
+          },
+          <String, dynamic> {
+            'name': 'plugin-c',
+            'path': '/.tmp_rand0/plugin.rand2/',
+            'dependencies': <String>[]
+          },
+        ];
+        expect(plugins['ios'], expectedPlugins);
+        expect(plugins['android'], expectedPlugins);
+        expect(plugins['macos'], <dynamic>[]);
+        expect(plugins['windows'], <dynamic>[]);
+        expect(plugins['linux'], <dynamic>[]);
+        expect(plugins['web'], <dynamic>[]);
+
+        final List<dynamic> expectedDependencyGraph = <dynamic>[
+          <String, dynamic> {
+            'name': 'plugin-a',
+            'dependencies': <String>[
+              'plugin-b',
+              'plugin-c'
+            ]
+          },
+          <String, dynamic> {
+            'name': 'plugin-b',
+            'dependencies': <String>[
+              'plugin-c'
+            ]
+          },
+          <String, dynamic> {
+            'name': 'plugin-c',
+            'dependencies': <String>[]
+          },
+        ];
+
+        expect(jsonContent['dependencyGraph'], expectedDependencyGraph);
+        expect(jsonContent['date_created'], dateCreated.toString());
+        expect(jsonContent['version'], version);
+
+        // Make sure tests are updated if a new object is added/removed.
+        final List<String> expectedKeys = <String>[
+          'info',
+          'plugins',
+          'dependencyGraph',
+          'date_created',
+          'version',
+        ];
+        expect(jsonContent.keys, expectedKeys);
       }, overrides: <Type, Generator>{
         FileSystem: () => fs,
         ProcessManager: () => FakeProcessManager.any(),
+        SystemClock: () => mockClock,
+        FlutterVersion: () => mockVersion
       });
 
       testUsingContext('Changes to the plugin list invalidates the Cocoapod lockfiles', () {
@@ -315,6 +421,31 @@
       }, overrides: <Type, Generator>{
         FileSystem: () => fs,
         ProcessManager: () => FakeProcessManager.any(),
+        SystemClock: () => mockClock,
+        FlutterVersion: () => mockVersion
+      });
+
+      testUsingContext('No changes to the plugin list does not invalidate the Cocoapod lockfiles', () {
+        configureDummyPackageAsPlugin();
+        when(iosProject.existsSync()).thenReturn(true);
+        when(macosProject.existsSync()).thenReturn(true);
+
+        // First call will create the .flutter-plugins-dependencies and the legacy .flutter-plugins file.
+        // Since there was no plugins list, the lock files will be invalidated.
+        // The second call is where the plugins list is compared to the existing one, and if there is no change,
+        // the podfiles shouldn't be invalidated.
+        refreshPluginsList(flutterProject);
+        simulatePodInstallRun(iosProject);
+        simulatePodInstallRun(macosProject);
+
+        refreshPluginsList(flutterProject);
+        expect(iosProject.podManifestLock.existsSync(), true);
+        expect(macosProject.podManifestLock.existsSync(), true);
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+        ProcessManager: () => FakeProcessManager.any(),
+        SystemClock: () => mockClock,
+        FlutterVersion: () => mockVersion
       });
     });
 
@@ -600,6 +731,7 @@
       testUsingContext('Registrant for web doesn\'t escape slashes in imports', () async {
         when(flutterProject.isModule).thenReturn(true);
         when(featureFlags.isWebEnabled).thenReturn(true);
+        when(webProject.existsSync()).thenReturn(true);
 
         final Directory webPluginWithNestedFile =
             fs.systemTempDirectory.createTempSync('web_plugin_with_nested');
@@ -648,3 +780,5 @@
 class MockMacOSProject extends Mock implements MacOSProject {}
 class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
 class MockWebProject extends Mock implements WebProject {}
+class MockWindowsProject extends Mock implements WindowsProject {}
+class MockLinuxProject extends Mock implements LinuxProject {}