Revert "Revert "Add multidex flag and automatic multidex support (#90944)" (#91791)"

This reverts commit a9c31eca486249427a81bf8c00ce0237de11576b.
diff --git a/packages/flutter_tools/gradle/flutter.gradle b/packages/flutter_tools/gradle/flutter.gradle
index 5a9131d..b1b9148 100644
--- a/packages/flutter_tools/gradle/flutter.gradle
+++ b/packages/flutter_tools/gradle/flutter.gradle
@@ -215,6 +215,38 @@
             }
         }
 
+        if (project.hasProperty("multidex-enabled") &&
+            project.property("multidex-enabled").toBoolean() &&
+            project.android.defaultConfig.minSdkVersion <= 20) {
+            String flutterMultidexKeepfile = Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools",
+                "gradle", "flutter_multidex_keepfile.txt")
+            project.android {
+                defaultConfig {
+                    multiDexEnabled true
+                    manifestPlaceholders = [applicationName: "io.flutter.app.FlutterMultiDexApplication"]
+                }
+                buildTypes {
+                    release {
+                        multiDexKeepFile project.file(flutterMultidexKeepfile)
+                    }
+                }
+            }
+            project.dependencies {
+                implementation "androidx.multidex:multidex:2.0.1"
+            }
+        } else {
+            String baseApplicationName = "android.app.Application"
+            if (project.hasProperty("base-application-name")) {
+                baseApplicationName = project.property("base-application-name")
+            }
+            project.android {
+                defaultConfig {
+                    // Setting to android.app.Application is the same as omitting the attribute.
+                    manifestPlaceholders = [applicationName: baseApplicationName]
+                }
+            }
+        }
+
         if (useLocalEngine()) {
             // This is required to pass the local engine to flutter build aot.
             String engineOutPath = project.property('local-engine-out')
diff --git a/packages/flutter_tools/gradle/flutter_multidex_keepfile.txt b/packages/flutter_tools/gradle/flutter_multidex_keepfile.txt
new file mode 100644
index 0000000..34984e6
--- /dev/null
+++ b/packages/flutter_tools/gradle/flutter_multidex_keepfile.txt
@@ -0,0 +1,5 @@
+io/flutter/app/FlutterApplication.class
+io/flutter/app/FlutterMultiDexApplication.class
+io/flutter/embedding/engine/loader/FlutterLoader.class
+io/flutter/view/FlutterMain.class
+io/flutter/util/PathUtils.class
diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart
index 7bdd848..4470ff5 100644
--- a/packages/flutter_tools/lib/src/android/android_device.dart
+++ b/packages/flutter_tools/lib/src/android/android_device.dart
@@ -595,7 +595,8 @@
           androidBuildInfo: AndroidBuildInfo(
             debuggingOptions.buildInfo,
             targetArchs: <AndroidArch>[androidArch],
-            fastStart: debuggingOptions.fastStart
+            fastStart: debuggingOptions.fastStart,
+            multidexEnabled: platformArgs['multidex'] as bool,
           ),
       );
       // Package has been built, so we can get the updated application ID and
diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart
index be01307..fafe472 100644
--- a/packages/flutter_tools/lib/src/android/gradle.dart
+++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -28,6 +28,7 @@
 import 'android_studio.dart';
 import 'gradle_errors.dart';
 import 'gradle_utils.dart';
+import 'multidex.dart';
 
 /// The directory where the APK artifact is generated.
 Directory getApkDirectory(FlutterProject project) {
@@ -293,6 +294,22 @@
     if (target != null) {
       command.add('-Ptarget=$target');
     }
+    // Only attempt adding multidex support if all the flutter generated files exist.
+    // If the files do not exist and it was unintentional, the app will fail to build
+    // and prompt the developer if they wish Flutter to add the files again via gradle_error.dart.
+    if (androidBuildInfo.multidexEnabled &&
+        multiDexApplicationExists(project.directory) &&
+        androidManifestHasNameVariable(project.directory)) {
+      command.add('-Pmultidex-enabled=true');
+      ensureMultiDexApplicationExists(project.directory);
+      _logger.printStatus('Building with Flutter multidex support enabled.');
+    }
+    // If using v1 embedding, we want to use FlutterApplication as the base app.
+    final String baseApplicationName =
+        project.android.getEmbeddingVersion() == AndroidEmbeddingVersion.v2 ?
+          'android.app.Application' :
+          'io.flutter.app.FlutterApplication';
+    command.add('-Pbase-application-name=$baseApplicationName');
     final List<DeferredComponent>? deferredComponents = project.manifest.deferredComponents;
     if (deferredComponents != null) {
       if (deferredComponentsEnabled) {
@@ -389,6 +406,7 @@
           line: detectedGradleErrorLine!,
           project: project,
           usesAndroidX: usesAndroidX,
+          multidexEnabled: androidBuildInfo.multidexEnabled,
         );
 
         if (retries >= 1) {
diff --git a/packages/flutter_tools/lib/src/android/gradle_errors.dart b/packages/flutter_tools/lib/src/android/gradle_errors.dart
index 2b94ef9..3277527 100644
--- a/packages/flutter_tools/lib/src/android/gradle_errors.dart
+++ b/packages/flutter_tools/lib/src/android/gradle_errors.dart
@@ -7,10 +7,12 @@
 import '../base/error_handling_io.dart';
 import '../base/file_system.dart';
 import '../base/process.dart';
+import '../base/terminal.dart';
 import '../globals_null_migrated.dart' as globals;
 import '../project.dart';
 import '../reporting/reporting.dart';
 import 'android_studio.dart';
+import 'multidex.dart';
 
 typedef GradleErrorTest = bool Function(String);
 
@@ -31,6 +33,7 @@
     required String line,
     required FlutterProject project,
     required bool usesAndroidX,
+    required bool multidexEnabled,
   }) handler;
 
   /// The [BuildEvent] label is named gradle-[eventLabel].
@@ -71,8 +74,104 @@
   minSdkVersion,
   transformInputIssue,
   lockFileDepMissing,
+  multidexErrorHandler,
 ];
 
+// Multidex error message.
+@visibleForTesting
+final GradleHandledError multidexErrorHandler = GradleHandledError(
+  test: _lineMatcher(const <String>[
+    'com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives:',
+    'The number of method references in a .dex file cannot exceed 64K.',
+  ]),
+  handler: ({
+    required String line,
+    required FlutterProject project,
+    required bool usesAndroidX,
+    required bool multidexEnabled,
+  }) async {
+    globals.printStatus('${globals.logger.terminal.warningMark} App requires Multidex support', emphasis: true);
+    if (multidexEnabled) {
+      globals.printStatus(
+        'Multidex support is required for your android app to build since the number of methods has exceeded 64k. '
+        "You may pass the --no-multidex flag to skip Flutter's multidex support to use a manual solution.\n",
+        indent: 4,
+      );
+      if (!androidManifestHasNameVariable(project.directory)) {
+        globals.printStatus(
+          r'Your `android/app/src/main/AndroidManifest.xml` does not contain `android:name="${applicationName}"` '
+          'under the `application` element. This may be due to creating your project with an old version of Flutter. '
+          'Add the `android:name="\${applicationName}"` attribute to your AndroidManifest.xml to enable Flutter\'s multidex support:\n',
+          indent: 4,
+        );
+        globals.printStatus(r'''
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+  ...
+  <application
+    ...
+    android:name=''',
+          indent: 8,
+          newline: false,
+          color: TerminalColor.grey,
+        );
+        globals.printStatus(r'"${applicationName}"', color: TerminalColor.green, newline: true);
+        globals.printStatus(r'''
+    ...>
+''',
+          indent: 8,
+          color: TerminalColor.grey,
+        );
+
+        globals.printStatus(
+          'You may also roll your own multidex support by following the guide at: https://developer.android.com/studio/build/multidex\n',
+          indent: 4,
+        );
+        return GradleBuildStatus.exit;
+      }
+      if (!multiDexApplicationExists(project.directory)) {
+        globals.printStatus(
+          'Flutter tool can add multidex support. The following file will be added by flutter:\n',
+          indent: 4,
+        );
+        globals.printStatus(
+          'android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java\n',
+          indent: 8,
+        );
+        String selection = 'n';
+        // Default to 'no' if no interactive terminal.
+        try {
+          selection = await globals.terminal.promptForCharInput(
+            <String>['y', 'n'],
+            logger: globals.logger,
+            prompt: 'Do you want to continue with adding multidex support for Android?',
+            defaultChoiceIndex: 0,
+          );
+        } on StateError catch(e) {
+          globals.printError(
+            e.message,
+            indent: 0,
+          );
+        }
+        if (selection == 'y') {
+          ensureMultiDexApplicationExists(project.directory);
+          globals.printStatus(
+            'Multidex enabled. Retrying build.\n',
+            indent: 0,
+          );
+          return GradleBuildStatus.retry;
+        }
+      }
+    } else {
+      globals.printStatus(
+        'Flutter multidex handling is disabled. If you wish to let the tool configure multidex, use the --mutidex flag.',
+        indent: 4,
+      );
+    }
+    return GradleBuildStatus.exit;
+  },
+  eventLabel: 'multidex-error',
+);
+
 // Permission defined error message.
 @visibleForTesting
 final GradleHandledError permissionDeniedErrorHandler = GradleHandledError(
@@ -83,12 +182,13 @@
     required String line,
     required FlutterProject project,
     required bool usesAndroidX,
+    required bool multidexEnabled,
   }) async {
     globals.printStatus('${globals.logger.terminal.warningMark} Gradle does not have execution permission.', emphasis: true);
     globals.printStatus(
       'You should change the ownership of the project directory to your user, '
       'or move the project to a directory with execute permissions.',
-      indent: 4
+      indent: 4,
     );
     return GradleBuildStatus.exit;
   },
@@ -119,6 +219,7 @@
     required String line,
     required FlutterProject project,
     required bool usesAndroidX,
+    required bool multidexEnabled,
   }) async {
     globals.printError(
       '${globals.logger.terminal.warningMark} Gradle threw an error while downloading artifacts from the network. '
@@ -148,6 +249,7 @@
     required String line,
     required FlutterProject project,
     required bool usesAndroidX,
+    required bool multidexEnabled,
   }) async {
     globals.printStatus('${globals.logger.terminal.warningMark} The shrinker may have failed to optimize the Java bytecode.', emphasis: true);
     globals.printStatus('To disable the shrinker, pass the `--no-shrink` flag to this command.', indent: 4);
@@ -169,6 +271,7 @@
     required String line,
     required FlutterProject project,
     required bool usesAndroidX,
+    required bool multidexEnabled,
   }) async {
     const String licenseNotAcceptedMatcher =
       r'You have not accepted the license agreements of the following SDK components:\s*\[(.+)\]';
@@ -202,6 +305,7 @@
     required String line,
     required FlutterProject project,
     required bool usesAndroidX,
+    required bool multidexEnabled,
   }) async {
     final RunResult tasksRunResult = await globals.processUtils.run(
       <String>[
@@ -274,6 +378,7 @@
     required String line,
     required FlutterProject project,
     required bool usesAndroidX,
+    required bool multidexEnabled,
   }) async {
     final File gradleFile = project.directory
         .childDirectory('android')
@@ -314,6 +419,7 @@
     required String line,
     required FlutterProject project,
     required bool usesAndroidX,
+    required bool multidexEnabled,
   }) async {
     final File gradleFile = project.directory
         .childDirectory('android')
@@ -347,6 +453,7 @@
     required String line,
     required FlutterProject project,
     required bool usesAndroidX,
+    required bool multidexEnabled,
   }) async {
     final File gradleFile = project.directory
         .childDirectory('android')
diff --git a/packages/flutter_tools/lib/src/android/multidex.dart b/packages/flutter_tools/lib/src/android/multidex.dart
new file mode 100644
index 0000000..a016e0a
--- /dev/null
+++ b/packages/flutter_tools/lib/src/android/multidex.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 'package:xml/xml.dart';
+
+import '../base/file_system.dart';
+
+// These utility methods are used to generate the code for multidex support as
+// well as verifying the project is properly set up.
+
+File _getMultiDexApplicationFile(Directory projectDir) {
+  return projectDir.childDirectory('android')
+    .childDirectory('app')
+    .childDirectory('src')
+    .childDirectory('main')
+    .childDirectory('java')
+    .childDirectory('io')
+    .childDirectory('flutter')
+    .childDirectory('app')
+    .childFile('FlutterMultiDexApplication.java');
+}
+
+/// Creates the FlutterMultiDexApplication.java if it does not exist.
+void ensureMultiDexApplicationExists(final Directory projectDir) {
+  final File applicationFile = _getMultiDexApplicationFile(projectDir);
+  if (applicationFile.existsSync()) {
+    return;
+  }
+  applicationFile.createSync(recursive: true);
+
+  final StringBuffer buffer = StringBuffer();
+  buffer.write('''
+// Generated file.
+// If you wish to remove Flutter's multidex support, delete this entire file.
+
+package io.flutter.app;
+
+import android.content.Context;
+import androidx.annotation.CallSuper;
+import androidx.multidex.MultiDex;
+
+/**
+ * Extension of {@link io.flutter.app.FlutterApplication}, adding multidex support.
+ */
+public class FlutterMultiDexApplication extends FlutterApplication {
+  @Override
+  @CallSuper
+  protected void attachBaseContext(Context base) {
+    super.attachBaseContext(base);
+    MultiDex.install(this);
+  }
+}
+''');
+  applicationFile.writeAsStringSync(buffer.toString(), flush: true);
+}
+
+/// Returns true if FlutterMultiDexApplication.java exists.
+///
+/// This function does not verify the contents of the file.
+bool multiDexApplicationExists(final Directory projectDir) {
+  if (_getMultiDexApplicationFile(projectDir).existsSync()) {
+    return true;
+  }
+  return false;
+}
+
+File _getManifestFile(Directory projectDir) {
+  return projectDir.childDirectory('android')
+    .childDirectory('app')
+    .childDirectory('src')
+    .childDirectory('main')
+    .childFile('AndroidManifest.xml');
+}
+
+/// Returns true if the `app` module AndroidManifest.xml includes the
+/// <application android:name="${applicationName}"> attribute.
+bool androidManifestHasNameVariable(final Directory projectDir) {
+  final File manifestFile = _getManifestFile(projectDir);
+  if (!manifestFile.existsSync()) {
+    return false;
+  }
+  XmlDocument document;
+  try {
+    document = XmlDocument.parse(manifestFile.readAsStringSync());
+  } on XmlParserException {
+    return false;
+  } on FileSystemException {
+    return false;
+  }
+  // Check for the ${androidName} application attribute.
+  for (final XmlElement application in document.findAllElements('application')) {
+    final String? applicationName = application.getAttribute('android:name');
+    if (applicationName == r'${applicationName}') {
+      return true;
+    }
+  }
+  return false;
+}
diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart
index 9f62ce4..765e9f8 100644
--- a/packages/flutter_tools/lib/src/build_info.dart
+++ b/packages/flutter_tools/lib/src/build_info.dart
@@ -304,6 +304,7 @@
     ],
     this.splitPerAbi = false,
     this.fastStart = false,
+    this.multidexEnabled = false,
   });
 
   // The build info containing the mode and flavor.
@@ -321,6 +322,9 @@
 
   /// Whether to bootstrap an empty application.
   final bool fastStart;
+
+  /// Whether to enable multidex support for apps with more than 64k methods.
+  final bool multidexEnabled;
 }
 
 /// A summary of the compilation strategy used for Dart.
diff --git a/packages/flutter_tools/lib/src/commands/build_apk.dart b/packages/flutter_tools/lib/src/commands/build_apk.dart
index c335b6a..63caab5 100644
--- a/packages/flutter_tools/lib/src/commands/build_apk.dart
+++ b/packages/flutter_tools/lib/src/commands/build_apk.dart
@@ -35,6 +35,7 @@
     addNullSafetyModeOptions(hide: !verboseHelp);
     usesAnalyzeSizeFlag();
     addAndroidSpecificBuildOptions(hide: !verboseHelp);
+    addMultidexOption();
     argParser
       ..addFlag('split-per-abi',
         negatable: false,
@@ -99,9 +100,11 @@
       buildInfo,
       splitPerAbi: boolArg('split-per-abi'),
       targetArchs: stringsArg('target-platform').map<AndroidArch>(getAndroidArchForName),
+      multidexEnabled: boolArg('multidex'),
     );
     validateBuild(androidBuildInfo);
     displayNullSafetyMode(androidBuildInfo.buildInfo);
+    globals.terminal.usesTerminalUi = true;
     await androidBuilder.buildApk(
       project: FlutterProject.current(),
       target: targetFile,
diff --git a/packages/flutter_tools/lib/src/commands/build_appbundle.dart b/packages/flutter_tools/lib/src/commands/build_appbundle.dart
index 329267a..7d9a282 100644
--- a/packages/flutter_tools/lib/src/commands/build_appbundle.dart
+++ b/packages/flutter_tools/lib/src/commands/build_appbundle.dart
@@ -39,6 +39,7 @@
     addEnableExperimentation(hide: !verboseHelp);
     usesAnalyzeSizeFlag();
     addAndroidSpecificBuildOptions(hide: !verboseHelp);
+    addMultidexOption();
     argParser.addMultiOption('target-platform',
       splitCommas: true,
       defaultsTo: <String>['android-arm', 'android-arm64', 'android-x64'],
@@ -110,6 +111,7 @@
 
     final AndroidBuildInfo androidBuildInfo = AndroidBuildInfo(await getBuildInfo(),
       targetArchs: stringsArg('target-platform').map<AndroidArch>(getAndroidArchForName),
+      multidexEnabled: boolArg('multidex'),
     );
     // Do all setup verification that doesn't involve loading units. Checks that
     // require generated loading units are done after gen_snapshot in assemble.
@@ -144,6 +146,7 @@
 
     validateBuild(androidBuildInfo);
     displayNullSafetyMode(androidBuildInfo.buildInfo);
+    globals.terminal.usesTerminalUi = true;
     await androidBuilder.buildAab(
       project: FlutterProject.current(),
       target: targetFile,
diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart
index fdb30d0..2084b79 100644
--- a/packages/flutter_tools/lib/src/commands/drive.dart
+++ b/packages/flutter_tools/lib/src/commands/drive.dart
@@ -65,6 +65,7 @@
     // to prevent a local network permission dialog on iOS 14+,
     // which cannot be accepted or dismissed in a CI environment.
     addPublishPort(enabledByDefault: false, verboseHelp: verboseHelp);
+    addMultidexOption();
     argParser
       ..addFlag('keep-app-running',
         defaultsTo: null,
@@ -251,6 +252,8 @@
             'trace-startup': traceStartup,
           if (web)
             '--no-launch-chrome': true,
+          if (boolArg('multidex'))
+            'multidex': true,
         }
       );
     } else {
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index 3f1e025..0c65258 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -249,6 +249,7 @@
     // This will allow subsequent "flutter attach" commands to connect to the VM
     // without needing to know the port.
     addPublishPort(verboseHelp: verboseHelp);
+    addMultidexOption();
     argParser
       ..addFlag('enable-software-rendering',
         negatable: false,
@@ -500,6 +501,7 @@
         dillOutputPath: stringArg('output-dill'),
         stayResident: stayResident,
         ipv6: ipv6,
+        multidexEnabled: boolArg('multidex'),
       );
     } else if (webMode) {
       return webRunnerFactory.createWebRunner(
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index 686cd92..607500e 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -416,7 +416,9 @@
     }
     devFSWriter = device.createDevFSWriter(package, userIdentifier);
 
-    final Map<String, dynamic> platformArgs = <String, dynamic>{};
+    final Map<String, dynamic> platformArgs = <String, dynamic>{
+      'multidex': hotRunner.multidexEnabled,
+    };
 
     await startEchoingDeviceLog();
 
diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart
index e81bf73..5563473 100644
--- a/packages/flutter_tools/lib/src/run_hot.dart
+++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -93,6 +93,7 @@
     bool stayResident = true,
     bool ipv6 = false,
     bool machine = false,
+    this.multidexEnabled = false,
     ResidentDevtoolsHandlerFactory devtoolsHandler = createDefaultHandler,
     StopwatchFactory stopwatchFactory = const StopwatchFactory(),
     ReloadSourcesHelper reloadSourcesHelper = _defaultReloadSourcesHelper,
@@ -120,6 +121,7 @@
   final bool benchmarkMode;
   final File applicationBinary;
   final bool hostIsIde;
+  final bool multidexEnabled;
 
   /// When performing a hot restart, the tool needs to upload a new main.dart.dill to
   /// each attached device's devfs. Replacing the existing file is not safe and does
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index f95dda3..98d0b91 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -818,6 +818,16 @@
     );
   }
 
+  void addMultidexOption({ bool hide = false }) {
+    argParser.addFlag('multidex',
+      negatable: true,
+      defaultsTo: true,
+      help: 'When enabled, indicates that the app should be built with multidex support. This '
+            'flag adds the dependencies for multidex when the minimum android sdk is 20 or '
+            'below. For android sdk versions 21 and above, multidex support is native.',
+    );
+  }
+
   /// Adds build options common to all of the desktop build commands.
   void addCommonDesktopBuildOptions({ @required bool verboseHelp }) {
     addBuildModeFlags(verboseHelp: verboseHelp);
diff --git a/packages/flutter_tools/templates/app_shared/android.tmpl/app/src/main/AndroidManifest.xml.tmpl b/packages/flutter_tools/templates/app_shared/android.tmpl/app/src/main/AndroidManifest.xml.tmpl
index 4f1724e..e20e2be 100644
--- a/packages/flutter_tools/templates/app_shared/android.tmpl/app/src/main/AndroidManifest.xml.tmpl
+++ b/packages/flutter_tools/templates/app_shared/android.tmpl/app/src/main/AndroidManifest.xml.tmpl
@@ -2,6 +2,7 @@
     package="{{androidIdentifier}}">
    <application
         android:label="{{projectName}}"
+        android:name="${applicationName}"
         android:icon="@mipmap/ic_launcher">
         <activity
             android:name=".MainActivity"
diff --git a/packages/flutter_tools/test/commands.shard/permeable/build_apk_test.dart b/packages/flutter_tools/test/commands.shard/permeable/build_apk_test.dart
index 1112552..af937bf 100644
--- a/packages/flutter_tools/test/commands.shard/permeable/build_apk_test.dart
+++ b/packages/flutter_tools/test/commands.shard/permeable/build_apk_test.dart
@@ -157,6 +157,7 @@
           '-q',
           '-Ptarget-platform=android-arm,android-arm64,android-x64',
           '-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
+          '-Pbase-application-name=android.app.Application',
           '-Pdart-obfuscation=false',
           '-Ptrack-widget-creation=true',
           '-Ptree-shake-icons=true',
@@ -186,6 +187,7 @@
           '-q',
           '-Ptarget-platform=android-arm,android-arm64,android-x64',
           '-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
+          '-Pbase-application-name=android.app.Application',
           '-Pdart-obfuscation=false',
           '-Psplit-debug-info=${tempDir.path}',
           '-Ptrack-widget-creation=true',
@@ -216,6 +218,7 @@
           '-q',
           '-Ptarget-platform=android-arm,android-arm64,android-x64',
           '-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
+          '-Pbase-application-name=android.app.Application',
           '-Pdart-obfuscation=false',
           '-Pextra-front-end-options=foo,bar',
           '-Ptrack-widget-creation=true',
@@ -246,6 +249,7 @@
           '-q',
           '-Ptarget-platform=android-arm,android-arm64,android-x64',
           '-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
+          '-Pbase-application-name=android.app.Application',
           '-Pdart-obfuscation=false',
           '-Ptrack-widget-creation=true',
           '-Ptree-shake-icons=true',
@@ -281,6 +285,7 @@
           '-q',
           '-Ptarget-platform=android-arm,android-arm64,android-x64',
           '-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
+          '-Pbase-application-name=android.app.Application',
           '-Pdart-obfuscation=false',
           '-Ptrack-widget-creation=true',
           '-Ptree-shake-icons=true',
@@ -335,6 +340,7 @@
           '-q',
           '-Ptarget-platform=android-arm,android-arm64,android-x64',
           '-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
+          '-Pbase-application-name=android.app.Application',
           '-Pdart-obfuscation=false',
           '-Ptrack-widget-creation=true',
           '-Ptree-shake-icons=true',
@@ -381,6 +387,7 @@
           '-q',
           '-Ptarget-platform=android-arm,android-arm64,android-x64',
           '-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
+          '-Pbase-application-name=android.app.Application',
           '-Pdart-obfuscation=false',
           '-Ptrack-widget-creation=true',
           '-Ptree-shake-icons=true',
diff --git a/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart b/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart
index c0d50db..c18caf1 100644
--- a/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart
+++ b/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart
@@ -56,6 +56,7 @@
           '-q',
           '-Ptarget-platform=android-arm,android-arm64,android-x64',
           '-Ptarget=lib/main.dart',
+          '-Pbase-application-name=io.flutter.app.FlutterApplication',
           '-Pdart-obfuscation=false',
           '-Ptrack-widget-creation=false',
           '-Ptree-shake-icons=false',
@@ -101,6 +102,7 @@
                 String line,
                 FlutterProject project,
                 bool usesAndroidX,
+                bool multidexEnabled
               }) async {
                 handlerCalled = true;
                 return GradleBuildStatus.exit;
@@ -142,6 +144,7 @@
           '-q',
           '-Ptarget-platform=android-arm,android-arm64,android-x64',
           '-Ptarget=lib/main.dart',
+          '-Pbase-application-name=io.flutter.app.FlutterApplication',
           '-Pdart-obfuscation=false',
           '-Ptrack-widget-creation=false',
           '-Ptree-shake-icons=false',
@@ -156,6 +159,7 @@
           '-q',
           '-Ptarget-platform=android-arm,android-arm64,android-x64',
           '-Ptarget=lib/main.dart',
+          '-Pbase-application-name=io.flutter.app.FlutterApplication',
           '-Pdart-obfuscation=false',
           '-Ptrack-widget-creation=false',
           '-Ptree-shake-icons=false',
@@ -205,6 +209,7 @@
                 String line,
                 FlutterProject project,
                 bool usesAndroidX,
+                bool multidexEnabled
               }) async {
                 return GradleBuildStatus.retry;
               },
@@ -243,6 +248,7 @@
           '-q',
           '-Ptarget-platform=android-arm,android-arm64,android-x64',
           '-Ptarget=lib/main.dart',
+          '-Pbase-application-name=io.flutter.app.FlutterApplication',
           '-Pdart-obfuscation=false',
           '-Ptrack-widget-creation=false',
           '-Ptree-shake-icons=false',
@@ -288,6 +294,7 @@
                 String line,
                 FlutterProject project,
                 bool usesAndroidX,
+                bool multidexEnabled
               }) async {
                 handlerCalled = true;
                 return GradleBuildStatus.exit;
@@ -329,6 +336,7 @@
           '-q',
           '-Ptarget-platform=android-arm,android-arm64,android-x64',
           '-Ptarget=lib/main.dart',
+          '-Pbase-application-name=io.flutter.app.FlutterApplication',
           '-Pdart-obfuscation=false',
           '-Ptrack-widget-creation=false',
           '-Ptree-shake-icons=false',
@@ -388,6 +396,7 @@
           '-q',
           '-Ptarget-platform=android-arm,android-arm64,android-x64',
           '-Ptarget=lib/main.dart',
+          '-Pbase-application-name=io.flutter.app.FlutterApplication',
           '-Pdart-obfuscation=false',
           '-Ptrack-widget-creation=false',
           '-Ptree-shake-icons=false',
@@ -402,6 +411,7 @@
           '-q',
           '-Ptarget-platform=android-arm,android-arm64,android-x64',
           '-Ptarget=lib/main.dart',
+          '-Pbase-application-name=io.flutter.app.FlutterApplication',
           '-Pdart-obfuscation=false',
           '-Ptrack-widget-creation=false',
           '-Ptree-shake-icons=false',
@@ -450,6 +460,7 @@
               String line,
               FlutterProject project,
               bool usesAndroidX,
+                bool multidexEnabled
             }) async {
               return GradleBuildStatus.retry;
             },
@@ -488,6 +499,7 @@
           '-q',
           '-Ptarget-platform=android-arm64',
           '-Ptarget=lib/main.dart',
+          '-Pbase-application-name=io.flutter.app.FlutterApplication',
           '-Pdart-obfuscation=false',
           '-Ptrack-widget-creation=false',
           '-Ptree-shake-icons=false',
@@ -580,6 +592,7 @@
           '-q',
           '-Ptarget-platform=android-arm,android-arm64,android-x64',
           '-Ptarget=lib/main.dart',
+          '-Pbase-application-name=io.flutter.app.FlutterApplication',
           '-Pdart-obfuscation=false',
           '-Ptrack-widget-creation=false',
           '-Ptree-shake-icons=false',
@@ -765,6 +778,7 @@
           '-Plocal-engine-out=out/android_arm',
           '-Ptarget-platform=android-arm',
           '-Ptarget=lib/main.dart',
+          '-Pbase-application-name=io.flutter.app.FlutterApplication',
           '-Pdart-obfuscation=false',
           '-Ptrack-widget-creation=false',
           '-Ptree-shake-icons=false',
@@ -838,6 +852,7 @@
           '-Plocal-engine-out=out/android_arm64',
           '-Ptarget-platform=android-arm64',
           '-Ptarget=lib/main.dart',
+          '-Pbase-application-name=io.flutter.app.FlutterApplication',
           '-Pdart-obfuscation=false',
           '-Ptrack-widget-creation=false',
           '-Ptree-shake-icons=false',
@@ -911,6 +926,7 @@
           '-Plocal-engine-out=out/android_x86',
           '-Ptarget-platform=android-x86',
           '-Ptarget=lib/main.dart',
+          '-Pbase-application-name=io.flutter.app.FlutterApplication',
           '-Pdart-obfuscation=false',
           '-Ptrack-widget-creation=false',
           '-Ptree-shake-icons=false',
@@ -984,6 +1000,7 @@
           '-Plocal-engine-out=out/android_x64',
           '-Ptarget-platform=android-x64',
           '-Ptarget=lib/main.dart',
+          '-Pbase-application-name=io.flutter.app.FlutterApplication',
           '-Pdart-obfuscation=false',
           '-Ptrack-widget-creation=false',
           '-Ptree-shake-icons=false',
@@ -1056,6 +1073,7 @@
           '--no-daemon',
           '-Ptarget-platform=android-arm,android-arm64,android-x64',
           '-Ptarget=lib/main.dart',
+          '-Pbase-application-name=io.flutter.app.FlutterApplication',
           '-Pdart-obfuscation=false',
           '-Ptrack-widget-creation=false',
           '-Ptree-shake-icons=false',
diff --git a/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart
index eb820c7..a1ff236 100644
--- a/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart
+++ b/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart
@@ -9,7 +9,9 @@
 import 'package:flutter_tools/src/android/gradle_errors.dart';
 import 'package:flutter_tools/src/android/gradle_utils.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/logger.dart';
 import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/base/terminal.dart';
 import 'package:flutter_tools/src/globals_null_migrated.dart' as globals;
 import 'package:flutter_tools/src/project.dart';
 
@@ -30,6 +32,7 @@
           minSdkVersion,
           transformInputIssue,
           lockFileDepMissing,
+          multidexErrorHandler,
         ])
       );
     });
@@ -329,6 +332,213 @@
     });
   });
 
+  group('multidex errors', () {
+    testUsingContext('exits if multidex AndroidManifest not detected', () async {
+      const String errorMessage = r'''
+Caused by: com.android.tools.r8.utils.b: Cannot fit requested classes in a single dex file (# methods: 85091 > 65536)
+  at com.android.tools.r8.utils.T0.error(SourceFile:1)
+  at com.android.tools.r8.utils.T0.a(SourceFile:2)
+  at com.android.tools.r8.dex.P.a(SourceFile:740)
+  at com.android.tools.r8.dex.P$h.a(SourceFile:7)
+  at com.android.tools.r8.dex.b.a(SourceFile:14)
+  at com.android.tools.r8.dex.b.b(SourceFile:25)
+  at com.android.tools.r8.D8.d(D8.java:133)
+  at com.android.tools.r8.D8.b(D8.java:1)
+  at com.android.tools.r8.utils.Y.a(SourceFile:36)
+  ... 38 more
+
+
+FAILURE: Build failed with an exception.
+
+* What went wrong:
+Execution failed for task ':app:mergeDexDebug'.
+> A failure occurred while executing com.android.build.gradle.internal.tasks.Workers$ActionFacade
+   > com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives:
+     The number of method references in a .dex file cannot exceed 64K.
+     Learn how to resolve this issue at https://developer.android.com/tools/building/multidex.html''';
+
+      expect(formatTestErrorMessage(errorMessage, multidexErrorHandler), isTrue);
+      expect(await multidexErrorHandler.handler(project: FlutterProject.fromDirectory(globals.fs.currentDirectory), multidexEnabled: true), equals(GradleBuildStatus.exit));
+
+      expect(testLogger.statusText,
+        contains(
+          'Multidex support is required for your android app to build since the number of methods has exceeded 64k.'
+        )
+      );
+      expect(testLogger.statusText,
+        contains(
+          'Your `android/app/src/main/AndroidManifest.xml` does not contain'
+        )
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem.test(),
+      ProcessManager: () => FakeProcessManager.any(),
+    });
+    testUsingContext('retries if multidex support enabled', () async {
+      const String errorMessage = r'''
+Caused by: com.android.tools.r8.utils.b: Cannot fit requested classes in a single dex file (# methods: 85091 > 65536)
+  at com.android.tools.r8.utils.T0.error(SourceFile:1)
+  at com.android.tools.r8.utils.T0.a(SourceFile:2)
+  at com.android.tools.r8.dex.P.a(SourceFile:740)
+  at com.android.tools.r8.dex.P$h.a(SourceFile:7)
+  at com.android.tools.r8.dex.b.a(SourceFile:14)
+  at com.android.tools.r8.dex.b.b(SourceFile:25)
+  at com.android.tools.r8.D8.d(D8.java:133)
+  at com.android.tools.r8.D8.b(D8.java:1)
+  at com.android.tools.r8.utils.Y.a(SourceFile:36)
+  ... 38 more
+
+
+FAILURE: Build failed with an exception.
+
+* What went wrong:
+Execution failed for task ':app:mergeDexDebug'.
+> A failure occurred while executing com.android.build.gradle.internal.tasks.Workers$ActionFacade
+   > com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives:
+     The number of method references in a .dex file cannot exceed 64K.
+     Learn how to resolve this issue at https://developer.android.com/tools/building/multidex.html''';
+
+      final File manifest = globals.fs.currentDirectory
+          .childDirectory('android')
+          .childDirectory('app')
+          .childDirectory('src')
+          .childDirectory('main')
+          .childFile('AndroidManifest.xml');
+      manifest.createSync(recursive: true);
+      manifest.writeAsStringSync(r'''
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.multidexapp">
+   <application
+        android:label="multidextest2"
+        android:name="${applicationName}"
+        android:icon="@mipmap/ic_launcher">
+    </application>
+</manifest>
+''', flush: true);
+
+      expect(formatTestErrorMessage(errorMessage, multidexErrorHandler), isTrue);
+      expect(await multidexErrorHandler.handler(project: FlutterProject.fromDirectory(globals.fs.currentDirectory), multidexEnabled: true), equals(GradleBuildStatus.retry));
+
+      expect(testLogger.statusText,
+        contains(
+          'Multidex support is required for your android app to build since the number of methods has exceeded 64k.'
+        )
+      );
+      expect(testLogger.statusText,
+        contains(
+          'android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java'
+        )
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem.test(),
+      ProcessManager: () => FakeProcessManager.any(),
+      AnsiTerminal: () => _TestPromptTerminal('y')
+    });
+
+    testUsingContext('exits if multidex support skipped', () async {
+      const String errorMessage = r'''
+Caused by: com.android.tools.r8.utils.b: Cannot fit requested classes in a single dex file (# methods: 85091 > 65536)
+  at com.android.tools.r8.utils.T0.error(SourceFile:1)
+  at com.android.tools.r8.utils.T0.a(SourceFile:2)
+  at com.android.tools.r8.dex.P.a(SourceFile:740)
+  at com.android.tools.r8.dex.P$h.a(SourceFile:7)
+  at com.android.tools.r8.dex.b.a(SourceFile:14)
+  at com.android.tools.r8.dex.b.b(SourceFile:25)
+  at com.android.tools.r8.D8.d(D8.java:133)
+  at com.android.tools.r8.D8.b(D8.java:1)
+  at com.android.tools.r8.utils.Y.a(SourceFile:36)
+  ... 38 more
+
+
+FAILURE: Build failed with an exception.
+
+* What went wrong:
+Execution failed for task ':app:mergeDexDebug'.
+> A failure occurred while executing com.android.build.gradle.internal.tasks.Workers$ActionFacade
+   > com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives:
+     The number of method references in a .dex file cannot exceed 64K.
+     Learn how to resolve this issue at https://developer.android.com/tools/building/multidex.html''';
+
+      final File manifest = globals.fs.currentDirectory
+          .childDirectory('android')
+          .childDirectory('app')
+          .childDirectory('src')
+          .childDirectory('main')
+          .childFile('AndroidManifest.xml');
+      manifest.createSync(recursive: true);
+      manifest.writeAsStringSync(r'''
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.multidexapp">
+   <application
+        android:label="multidextest2"
+        android:name="${applicationName}"
+        android:icon="@mipmap/ic_launcher">
+    </application>
+</manifest>
+''', flush: true);
+
+      expect(formatTestErrorMessage(errorMessage, multidexErrorHandler), isTrue);
+      expect(await multidexErrorHandler.handler(project: FlutterProject.fromDirectory(globals.fs.currentDirectory), multidexEnabled: true), equals(GradleBuildStatus.exit));
+
+      expect(testLogger.statusText,
+        contains(
+          'Multidex support is required for your android app to build since the number of methods has exceeded 64k.'
+        )
+      );
+      expect(testLogger.statusText,
+        contains(
+          'Flutter tool can add multidex support. The following file will be added by flutter:'
+        )
+      );
+      expect(testLogger.statusText,
+        contains(
+          'android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java'
+        )
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem.test(),
+      ProcessManager: () => FakeProcessManager.any(),
+      AnsiTerminal: () => _TestPromptTerminal('n')
+    });
+
+    testUsingContext('exits if multidex support disabled', () async {
+      const String errorMessage = r'''
+Caused by: com.android.tools.r8.utils.b: Cannot fit requested classes in a single dex file (# methods: 85091 > 65536)
+  at com.android.tools.r8.utils.T0.error(SourceFile:1)
+  at com.android.tools.r8.utils.T0.a(SourceFile:2)
+  at com.android.tools.r8.dex.P.a(SourceFile:740)
+  at com.android.tools.r8.dex.P$h.a(SourceFile:7)
+  at com.android.tools.r8.dex.b.a(SourceFile:14)
+  at com.android.tools.r8.dex.b.b(SourceFile:25)
+  at com.android.tools.r8.D8.d(D8.java:133)
+  at com.android.tools.r8.D8.b(D8.java:1)
+  at com.android.tools.r8.utils.Y.a(SourceFile:36)
+  ... 38 more
+
+
+FAILURE: Build failed with an exception.
+
+* What went wrong:
+Execution failed for task ':app:mergeDexDebug'.
+> A failure occurred while executing com.android.build.gradle.internal.tasks.Workers$ActionFacade
+   > com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives:
+     The number of method references in a .dex file cannot exceed 64K.
+     Learn how to resolve this issue at https://developer.android.com/tools/building/multidex.html''';
+
+      expect(formatTestErrorMessage(errorMessage, multidexErrorHandler), isTrue);
+      expect(await multidexErrorHandler.handler(project: FlutterProject.fromDirectory(globals.fs.currentDirectory), multidexEnabled: false), equals(GradleBuildStatus.exit));
+
+      expect(testLogger.statusText,
+        contains(
+          'Flutter multidex handling is disabled.'
+        )
+      );
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem.test(),
+      ProcessManager: () => FakeProcessManager.any(),
+    });
+  });
+
   group('permission errors', () {
     testUsingContext('throws toolExit if gradle is missing execute permissions', () async {
       const String errorMessage = '''
@@ -667,3 +877,21 @@
     return 'gradlew';
   }
 }
+
+/// Simple terminal that returns the specified string when
+/// promptForCharInput is called.
+class _TestPromptTerminal extends AnsiTerminal {
+  _TestPromptTerminal(this.promptResult);
+
+  final String promptResult;
+
+  @override
+  Future<String> promptForCharInput(List<String> acceptedCharacters, {
+    Logger logger,
+    String prompt,
+    int defaultChoiceIndex,
+    bool displayAcceptedCharacters = true,
+  }) {
+    return Future<String>.value(promptResult);
+  }
+}
diff --git a/packages/flutter_tools/test/general.shard/android/multidex_test.dart b/packages/flutter_tools/test/general.shard/android/multidex_test.dart
new file mode 100644
index 0000000..a4cc8e8
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/android/multidex_test.dart
@@ -0,0 +1,189 @@
+// 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.
+
+// @dart = 2.8
+
+import 'package:file/file.dart';
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/android/multidex.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/globals_null_migrated.dart' as globals;
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  testUsingContext('ensureMultidexUtilsExists returns when exists', () async {
+    final Directory directory = globals.fs.currentDirectory;
+    final File applicationFile = directory.childDirectory('android')
+      .childDirectory('app')
+      .childDirectory('src')
+      .childDirectory('main')
+      .childDirectory('java')
+      .childDirectory('io')
+      .childDirectory('flutter')
+      .childDirectory('app')
+      .childFile('FlutterMultiDexApplication.java');
+    applicationFile.createSync(recursive: true);
+    applicationFile.writeAsStringSync('hello', flush: true);
+    expect(applicationFile.readAsStringSync(), 'hello');
+
+    ensureMultiDexApplicationExists(directory);
+
+    // File should remain untouched
+    expect(applicationFile.readAsStringSync(), 'hello');
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem.test(),
+    ProcessManager: () => FakeProcessManager.any(),
+  });
+
+  testUsingContext('ensureMultiDexApplicationExists generates when does not exist', () async {
+    final Directory directory = globals.fs.currentDirectory;
+    final File applicationFile = directory.childDirectory('android')
+      .childDirectory('app')
+      .childDirectory('src')
+      .childDirectory('main')
+      .childDirectory('java')
+      .childDirectory('io')
+      .childDirectory('flutter')
+      .childDirectory('app')
+      .childFile('FlutterMultiDexApplication.java');
+
+    ensureMultiDexApplicationExists(directory);
+
+    final String contents = applicationFile.readAsStringSync();
+    expect(contents.contains('FlutterMultiDexApplication'), true);
+    expect(contents.contains('MultiDex.install(this);'), true);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem.test(),
+    ProcessManager: () => FakeProcessManager.any(),
+  });
+
+  testUsingContext('multiDexApplicationExists false when does not exist', () async {
+    final Directory directory = globals.fs.currentDirectory;
+    expect(multiDexApplicationExists(directory), false);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem.test(),
+    ProcessManager: () => FakeProcessManager.any(),
+  });
+
+  testUsingContext('multiDexApplicationExists true when does exist', () async {
+    final Directory directory = globals.fs.currentDirectory;
+    final File utilsFile = directory.childDirectory('android')
+      .childDirectory('app')
+      .childDirectory('src')
+      .childDirectory('main')
+      .childDirectory('java')
+      .childDirectory('io')
+      .childDirectory('flutter')
+      .childDirectory('app')
+      .childFile('FlutterMultiDexApplication.java');
+    utilsFile.createSync(recursive: true);
+
+    expect(multiDexApplicationExists(directory), true);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem.test(),
+    ProcessManager: () => FakeProcessManager.any(),
+  });
+
+  testUsingContext('androidManifestHasNameVariable true with valid manifest', () async {
+    final Directory directory = globals.fs.currentDirectory;
+    final File applicationFile = directory.childDirectory('android')
+      .childDirectory('app')
+      .childDirectory('src')
+      .childDirectory('main')
+      .childFile('AndroidManifest.xml');
+    applicationFile.createSync(recursive: true);
+    applicationFile.writeAsStringSync(r'''
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.multidexapp">
+   <application
+        android:label="multidextest2"
+        android:name="${applicationName}"
+        android:icon="@mipmap/ic_launcher">
+    </application>
+</manifest>
+''', flush: true);
+    expect(androidManifestHasNameVariable(directory), true);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem.test(),
+    ProcessManager: () => FakeProcessManager.any(),
+  });
+
+  testUsingContext('androidManifestHasNameVariable false with no android:name attribute', () async {
+    final Directory directory = globals.fs.currentDirectory;
+    final File applicationFile = directory.childDirectory('android')
+      .childDirectory('app')
+      .childDirectory('src')
+      .childDirectory('main')
+      .childFile('AndroidManifest.xml');
+    applicationFile.createSync(recursive: true);
+    applicationFile.writeAsStringSync(r'''
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.multidexapp">
+   <application
+        android:label="multidextest2"
+        android:icon="@mipmap/ic_launcher">
+    </application>
+''', flush: true);
+    expect(androidManifestHasNameVariable(directory), false);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem.test(),
+    ProcessManager: () => FakeProcessManager.any(),
+  });
+
+  testUsingContext('androidManifestHasNameVariable false with incorrect android:name attribute', () async {
+    final Directory directory = globals.fs.currentDirectory;
+    final File applicationFile = directory.childDirectory('android')
+      .childDirectory('app')
+      .childDirectory('src')
+      .childDirectory('main')
+      .childFile('AndroidManifest.xml');
+    applicationFile.createSync(recursive: true);
+    applicationFile.writeAsStringSync(r'''
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.multidexapp">
+   <application
+        android:label="multidextest2"
+        android:name="io.flutter.app.FlutterApplication"
+        android:icon="@mipmap/ic_launcher">
+    </application>
+''', flush: true);
+    expect(androidManifestHasNameVariable(directory), false);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem.test(),
+    ProcessManager: () => FakeProcessManager.any(),
+  });
+
+  testUsingContext('androidManifestHasNameVariable false with invalid xml manifest', () async {
+    final Directory directory = globals.fs.currentDirectory;
+    final File applicationFile = directory.childDirectory('android')
+      .childDirectory('app')
+      .childDirectory('src')
+      .childDirectory('main')
+      .childFile('AndroidManifest.xml');
+    applicationFile.createSync(recursive: true);
+    applicationFile.writeAsStringSync(r'''
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.multidexapp">
+   <application
+        android:label="multidextest2"
+        android:name="${applicationName}"
+        android:icon="@mipmap/ic_launcher">
+    </application>
+''', flush: true);
+    expect(androidManifestHasNameVariable(directory), false);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem.test(),
+    ProcessManager: () => FakeProcessManager.any(),
+  });
+
+  testUsingContext('androidManifestHasNameVariable false with no manifest file', () async {
+    final Directory directory = globals.fs.currentDirectory;
+    expect(androidManifestHasNameVariable(directory), false);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => MemoryFileSystem.test(),
+    ProcessManager: () => FakeProcessManager.any(),
+  });
+}
diff --git a/packages/flutter_tools/test/integration.shard/multidex_build_test.dart b/packages/flutter_tools/test/integration.shard/multidex_build_test.dart
new file mode 100644
index 0000000..21c43e1
--- /dev/null
+++ b/packages/flutter_tools/test/integration.shard/multidex_build_test.dart
@@ -0,0 +1,61 @@
+// 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.
+
+// @dart = 2.8
+
+import 'package:file/file.dart';
+import 'package:flutter_tools/src/base/io.dart';
+
+import '../src/common.dart';
+import 'test_data/multidex_project.dart';
+import 'test_driver.dart';
+import 'test_utils.dart';
+
+void main() {
+  Directory tempDir;
+  FlutterRunTestDriver _flutter;
+
+  setUp(() async {
+    tempDir = createResolvedTempDirectorySync('run_test.');
+    _flutter = FlutterRunTestDriver(tempDir);
+  });
+
+  tearDown(() async {
+    await _flutter.stop();
+    tryToDelete(tempDir);
+  });
+
+  testWithoutContext('simple build apk succeeds', () async {
+    final MultidexProject project = MultidexProject(true);
+    await project.setUpIn(tempDir);
+    final String flutterBin = fileSystem.path.join(getFlutterRoot(), 'bin', 'flutter');
+    final ProcessResult result = await processManager.run(<String>[
+      flutterBin,
+      ...getLocalEngineArguments(),
+      'build',
+      'apk',
+      '--debug',
+    ], workingDirectory: tempDir.path);
+
+    expect(result.exitCode, 0);
+    expect(result.stdout.toString(), contains('app-debug.apk'));
+  });
+
+  testWithoutContext('simple build apk without FlutterMultiDexApplication fails', () async {
+    final MultidexProject project = MultidexProject(false);
+    await project.setUpIn(tempDir);
+    final String flutterBin = fileSystem.path.join(getFlutterRoot(), 'bin', 'flutter');
+    final ProcessResult result = await processManager.run(<String>[
+      flutterBin,
+      ...getLocalEngineArguments(),
+      'build',
+      'apk',
+      '--debug',
+    ], workingDirectory: tempDir.path);
+
+    expect(result.stderr.toString(), contains('Cannot fit requested classes in a single dex file'));
+    expect(result.stderr.toString(), contains('The number of method references in a .dex file cannot exceed 64K.'));
+    expect(result.exitCode, 1);
+  });
+}
diff --git a/packages/flutter_tools/test/integration.shard/test_data/multidex_project.dart b/packages/flutter_tools/test/integration.shard/test_data/multidex_project.dart
new file mode 100644
index 0000000..70fdf0d
--- /dev/null
+++ b/packages/flutter_tools/test/integration.shard/test_data/multidex_project.dart
@@ -0,0 +1,322 @@
+// 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.
+
+// @dart = 2.8
+
+import 'package:file/file.dart';
+
+import '../../src/common.dart';
+import '../test_utils.dart';
+import 'project.dart';
+
+class MultidexProject extends Project {
+  MultidexProject(this.includeFlutterMultiDexApplication);
+
+  @override
+  Future<void> setUpIn(Directory dir, {
+    bool useDeferredLoading = false,
+    bool useSyntheticPackage = false,
+  }) {
+    this.dir = dir;
+    if (androidSettings != null) {
+      writeFile(fileSystem.path.join(dir.path, 'android', 'settings.gradle'), androidSettings);
+    }
+    if (androidBuild != null) {
+      writeFile(fileSystem.path.join(dir.path, 'android', 'build.gradle'), androidBuild);
+    }
+    if (androidLocalProperties != null) {
+      writeFile(fileSystem.path.join(dir.path, 'android', 'local.properties'), androidLocalProperties);
+    }
+    if (androidGradleProperties != null) {
+      writeFile(fileSystem.path.join(dir.path, 'android', 'gradle.properties'), androidGradleProperties);
+    }
+    if (appBuild != null) {
+      writeFile(fileSystem.path.join(dir.path, 'android', 'app', 'build.gradle'), appBuild);
+    }
+    if (appManifest != null) {
+      writeFile(fileSystem.path.join(dir.path, 'android', 'app', 'src', 'main', 'AndroidManifest.xml'), appManifest);
+    }
+    if (appStrings != null) {
+      writeFile(fileSystem.path.join(dir.path, 'android', 'app', 'src', 'main', 'res', 'values', 'strings.xml'), appStrings);
+    }
+    if (appStyles != null) {
+      writeFile(fileSystem.path.join(dir.path, 'android', 'app', 'src', 'main', 'res', 'values', 'styles.xml'), appStyles);
+    }
+    if (appLaunchBackground != null) {
+      writeFile(fileSystem.path.join(dir.path, 'android', 'app', 'src', 'main', 'res', 'drawable', 'launch_background.xml'), appLaunchBackground);
+    }
+    if (includeFlutterMultiDexApplication && appMultidexApplication != null) {
+      writeFile(fileSystem.path.join(dir.path, 'android', 'app', 'src', 'main', 'java', fileSystem.path.join('io', 'flutter', 'app', 'FlutterMultiDexApplication.java')), appMultidexApplication);
+    }
+    return super.setUpIn(dir);
+  }
+
+  final bool includeFlutterMultiDexApplication;
+
+  @override
+  final String pubspec = '''
+  name: test
+  environment:
+    sdk: ">=2.12.0-0 <3.0.0"
+
+  dependencies:
+    flutter:
+      sdk: flutter
+    cloud_firestore: ^2.5.3
+    firebase_core: ^1.6.0
+  ''';
+
+  @override
+  final String main = r'''
+  import 'package:flutter/material.dart';
+
+  void main() {
+    runApp(MyApp());
+  }
+
+  class MyApp extends StatelessWidget {
+    @override
+    Widget build(BuildContext context) {
+      return new MaterialApp(
+        title: 'Flutter Demo',
+        home: new Container(),
+      );
+    }
+  }
+  ''';
+
+  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"
+  ''';
+
+  String get androidBuild => r'''
+  buildscript {
+      ext.kotlin_version = '1.3.50'
+      repositories {
+          google()
+          mavenCentral()
+      }
+
+      dependencies {
+          classpath 'com.android.tools.build:gradle:4.1.0'
+          classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+      }
+  }
+
+  allprojects {
+      repositories {
+          google()
+          mavenCentral()
+      }
+  }
+
+  rootProject.buildDir = '../build'
+  subprojects {
+      project.buildDir = "${rootProject.buildDir}/${project.name}"
+  }
+  subprojects {
+      project.evaluationDependsOn(':app')
+  }
+
+  task clean(type: Delete) {
+      delete rootProject.buildDir
+  }
+  ''';
+
+  String get appBuild => r'''
+  def localProperties = new Properties()
+  def localPropertiesFile = rootProject.file('local.properties')
+  if (localPropertiesFile.exists()) {
+      localPropertiesFile.withReader('UTF-8') { reader ->
+          localProperties.load(reader)
+      }
+  }
+
+  def flutterRoot = localProperties.getProperty('flutter.sdk')
+  if (flutterRoot == null) {
+      throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+  }
+
+  def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+  if (flutterVersionCode == null) {
+      flutterVersionCode = '1'
+  }
+
+  def flutterVersionName = localProperties.getProperty('flutter.versionName')
+  if (flutterVersionName == null) {
+      flutterVersionName = '1.0'
+  }
+
+  apply plugin: 'com.android.application'
+  apply plugin: 'kotlin-android'
+  apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+  android {
+      compileSdkVersion 30
+
+      compileOptions {
+          sourceCompatibility JavaVersion.VERSION_1_8
+          targetCompatibility JavaVersion.VERSION_1_8
+      }
+
+      kotlinOptions {
+          jvmTarget = '1.8'
+      }
+
+      sourceSets {
+          main.java.srcDirs += 'src/main/kotlin'
+      }
+
+      defaultConfig {
+          // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+          applicationId "com.example.multidextest2"
+          minSdkVersion 16
+          targetSdkVersion 30
+          versionCode flutterVersionCode.toInteger()
+          versionName flutterVersionName
+      }
+
+      buildTypes {
+          release {
+              // TODO: Add your own signing config for the release build.
+              // Signing with the debug keys for now, so `flutter run --release` works.
+              signingConfig signingConfigs.debug
+          }
+      }
+  }
+
+  flutter {
+      source '../..'
+  }
+
+  dependencies {
+      implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+  }
+  ''';
+
+  String get androidLocalProperties => '''
+  flutter.sdk=${getFlutterRoot()}
+  flutter.buildMode=debug
+  flutter.versionName=1.0.0
+  flutter.versionCode=22
+  ''';
+
+  String get androidGradleProperties => '''
+  org.gradle.jvmargs=-Xmx1536M
+  android.useAndroidX=true
+  android.enableJetifier=true
+  android.enableR8=true
+  android.experimental.enableNewResourceShrinker=true
+  ''';
+
+  String get appManifest => r'''
+  <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+      package="com.example.multidextest">
+     <application
+          android:label="multidextest"
+          android:name="${applicationName}">
+          <activity
+              android:name=".MainActivity"
+              android:launchMode="singleTop"
+              android:theme="@style/LaunchTheme"
+              android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+              android:hardwareAccelerated="true"
+              android:windowSoftInputMode="adjustResize">
+              <!-- Specifies an Android theme to apply to this Activity as soon as
+                   the Android process has started. This theme is visible to the user
+                   while the Flutter UI initializes. After that, this theme continues
+                   to determine the Window background behind the Flutter UI. -->
+              <meta-data
+                android:name="io.flutter.embedding.android.NormalTheme"
+                android:resource="@style/NormalTheme"
+                />
+              <intent-filter>
+                  <action android:name="android.intent.action.MAIN"/>
+                  <category android:name="android.intent.category.LAUNCHER"/>
+              </intent-filter>
+          </activity>
+          <!-- Don't delete the meta-data below.
+               This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
+          <meta-data
+              android:name="flutterEmbedding"
+              android:value="2" />
+      </application>
+  </manifest>
+  ''';
+
+  String get appStrings => r'''
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>
+  ''';
+
+  String get appStyles => r'''
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!-- Theme applied to the Android Window while the process is starting -->
+    <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <!-- Show a splash screen on the activity. Automatically removed when
+             Flutter draws its first frame -->
+        <item name="android:windowBackground">@drawable/launch_background</item>
+    </style>
+    <!-- Theme applied to the Android Window as soon as the process has started.
+         This theme determines the color of the Android Window while your
+         Flutter UI initializes, as well as behind your Flutter UI while its
+         running.
+
+         This Theme is only used starting with V2 of Flutter's Android embedding. -->
+    <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <item name="android:windowBackground">@android:color/white</item>
+    </style>
+</resources>
+  ''';
+
+  String get appLaunchBackground => r'''
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@android:color/white" />
+
+    <!-- You can insert your own image assets here -->
+    <!-- <item>
+        <bitmap
+            android:gravity="center"
+            android:src="@mipmap/launch_image" />
+    </item> -->
+</layer-list>
+  ''';
+
+  String get appMultidexApplication => r'''
+  // Generated file.
+  // If you wish to remove Flutter's multidex support, delete this entire file.
+
+  package io.flutter.app;
+
+  import android.content.Context;
+  import androidx.annotation.CallSuper;
+  import androidx.multidex.MultiDex;
+
+  /**
+   * Extension of {@link io.flutter.app.FlutterApplication}, adding multidex support.
+   */
+  public class FlutterMultiDexApplication extends FlutterApplication {
+    @Override
+    @CallSuper
+    protected void attachBaseContext(Context base) {
+      super.attachBaseContext(base);
+      MultiDex.install(this);
+    }
+  }
+  ''';
+}