Start abstracting platform logic builds behind a shared interface (#31889)

diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart
index b1f5ef7..360eaf2 100644
--- a/packages/flutter_tools/lib/src/android/android_device.dart
+++ b/packages/flutter_tools/lib/src/android/android_device.dart
@@ -8,7 +8,6 @@
 
 import '../android/android_sdk.dart';
 import '../android/android_workflow.dart';
-import '../android/apk.dart';
 import '../application_package.dart';
 import '../base/common.dart' show throwToolExit;
 import '../base/file_system.dart';
@@ -20,6 +19,7 @@
 import '../convert.dart';
 import '../device.dart';
 import '../globals.dart';
+import '../platform_step.dart';
 import '../project.dart';
 import '../protocol_discovery.dart';
 
@@ -374,10 +374,11 @@
     if (!prebuiltApplication || androidSdk.licensesAvailable && androidSdk.latestVersion == null) {
       printTrace('Building APK');
       final FlutterProject project = FlutterProject.current();
-      await buildApk(
-          project: project,
-          target: mainPath,
-          buildInfo: buildInfo,
+      final PlatformBuildStep platformStep = platformBuilders.selectPlatform(buildInfo: buildInfo);
+      await platformStep.build(
+        project: project,
+        target: mainPath,
+        buildInfo: buildInfo,
       );
       // Package has been built, so we can get the updated application ID and
       // activity name from the .apk.
diff --git a/packages/flutter_tools/lib/src/android/apk.dart b/packages/flutter_tools/lib/src/android/apk.dart
index 8b5b8d9..5158177 100644
--- a/packages/flutter_tools/lib/src/android/apk.dart
+++ b/packages/flutter_tools/lib/src/android/apk.dart
@@ -4,38 +4,67 @@
 
 import 'dart:async';
 
-import 'package:meta/meta.dart';
-
 import '../base/common.dart';
 import '../build_info.dart';
+import '../cache.dart';
+import '../platform_step.dart';
 import '../project.dart';
 
 import 'android_sdk.dart';
 import 'gradle.dart';
 
-Future<void> buildApk({
-  @required FlutterProject project,
-  @required String target,
-  BuildInfo buildInfo = BuildInfo.debug,
-}) async {
-  if (!project.android.isUsingGradle) {
-    throwToolExit(
+/// The Android Gradle build step.
+class AndroidPlatformBuildStep extends PlatformBuildStep {
+  const AndroidPlatformBuildStep();
+
+  @override
+  Future<void> build({
+    FlutterProject project,
+    BuildInfo buildInfo,
+    String target,
+  }) async {
+    if (!project.android.isUsingGradle) {
+      throwToolExit(
         'The build process for Android has changed, and the current project configuration\n'
-            'is no longer valid. Please consult\n\n'
-            '  https://github.com/flutter/flutter/wiki/Upgrading-Flutter-projects-to-build-with-gradle\n\n'
-            'for details on how to upgrade the project.'
+        'is no longer valid. Please consult\n\n'
+        '  https://github.com/flutter/flutter/wiki/Upgrading-Flutter-projects-to-build-with-gradle\n\n'
+        'for details on how to upgrade the project.'
+      );
+    }
+
+    // Validate that we can find an android sdk.
+    if (androidSdk == null) {
+      throwToolExit('No Android SDK found. Try setting the ANDROID_SDK_ROOT environment variable.');
+    }
+    await buildGradleProject(
+      project: project,
+      buildInfo: buildInfo,
+      target: target,
+      isBuildingBundle: false,
     );
+    androidSdk.reinitialize();
   }
 
-  // Validate that we can find an android sdk.
-  if (androidSdk == null)
-    throwToolExit('No Android SDK found. Try setting the ANDROID_SDK_ROOT environment variable.');
+  @override
+  Set<TargetPlatform> get targetPlatforms => const <TargetPlatform>{
+    TargetPlatform.android_arm,
+    TargetPlatform.android_arm64,
+    TargetPlatform.android_x64,
+    TargetPlatform.android_x86,
+  };
 
-  await buildGradleProject(
-    project: project,
-    buildInfo: buildInfo,
-    target: target,
-    isBuildingBundle: false,
-  );
-  androidSdk.reinitialize();
+  @override
+  Set<BuildMode> get buildModes => const <BuildMode>{
+    BuildMode.debug,
+    BuildMode.dynamicProfile,
+    BuildMode.dynamicRelease,
+    BuildMode.profile,
+    BuildMode.release,
+  };
+
+  @override
+  Set<DevelopmentArtifact> get developmentArtifacts => const <DevelopmentArtifact>{
+    DevelopmentArtifact.universal,
+    DevelopmentArtifact.android,
+  };
 }
diff --git a/packages/flutter_tools/lib/src/commands/build_apk.dart b/packages/flutter_tools/lib/src/commands/build_apk.dart
index 92c7ef0..1fdb3a8 100644
--- a/packages/flutter_tools/lib/src/commands/build_apk.dart
+++ b/packages/flutter_tools/lib/src/commands/build_apk.dart
@@ -4,7 +4,8 @@
 
 import 'dart:async';
 
-import '../android/apk.dart';
+import '../build_info.dart';
+import '../platform_step.dart';
 import '../project.dart';
 import '../runner/flutter_command.dart' show DevelopmentArtifact, FlutterCommandResult;
 import 'build.dart';
@@ -47,10 +48,12 @@
 
   @override
   Future<FlutterCommandResult> runCommand() async {
-    await buildApk(
+    final BuildInfo buildInfo = getBuildInfo();
+    final PlatformBuildStep platformStep = platformBuilders.selectPlatform(buildInfo: buildInfo);
+    await platformStep.build(
       project: FlutterProject.current(),
       target: targetFile,
-      buildInfo: getBuildInfo(),
+      buildInfo: buildInfo,
     );
     return null;
   }
diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart
index eb7dea5..f5f6730 100644
--- a/packages/flutter_tools/lib/src/context_runner.dart
+++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -36,6 +36,7 @@
 import 'ios/xcodeproj.dart';
 import 'linux/linux_workflow.dart';
 import 'macos/macos_workflow.dart';
+import 'platform_step.dart';
 import 'run_hot.dart';
 import 'usage.dart';
 import 'version.dart';
@@ -87,6 +88,7 @@
       Logger: () => platform.isWindows ? WindowsStdoutLogger() : StdoutLogger(),
       MacOSWorkflow: () => const MacOSWorkflow(),
       OperatingSystemUtils: () => OperatingSystemUtils(),
+      PlatformBuilders: () => const PlatformBuilders(),
       PlistBuddy: () => const PlistBuddy(),
       SimControl: () => SimControl(),
       SystemClock: () => const SystemClock(),
diff --git a/packages/flutter_tools/lib/src/platform_step.dart b/packages/flutter_tools/lib/src/platform_step.dart
new file mode 100644
index 0000000..4203340
--- /dev/null
+++ b/packages/flutter_tools/lib/src/platform_step.dart
@@ -0,0 +1,81 @@
+// Copyright 2019 The Chromium 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 'android/apk.dart';
+import 'base/common.dart';
+import 'base/context.dart';
+import 'build_info.dart';
+import 'cache.dart';
+import 'project.dart';
+
+/// The [PlatformBuilders] instance.
+PlatformBuilders get platformBuilders => context.get<PlatformBuilders>();
+
+/// A registry that selects the correct [PlatformBuilder] based on the provided
+/// build info.
+class PlatformBuilders {
+  /// Create a new [PlatformBuilder] with `_platformSteps` as platform specific
+  /// implementations.
+  const PlatformBuilders([this._platformSteps = const <PlatformBuildStep>[
+    AndroidPlatformBuildStep(),
+  ]]);
+
+  final List<PlatformBuildStep> _platformSteps;
+
+  /// Build the platform specific bundle for [flutterProject].
+  ///
+  /// Selects the [PlatformBuildStep] that supports the required [TargetPlatform]
+  /// and [BuildMode] requested.
+  ///
+  /// Throws a [ToolExit] if zero steps or more than one step match.
+  PlatformBuildStep selectPlatform({
+    BuildInfo buildInfo,
+  }) {
+    PlatformBuildStep selected;
+    for (PlatformBuildStep platformStep in _platformSteps) {
+      if (!platformStep.targetPlatforms.contains(buildInfo.targetPlatform)) {
+        continue;
+      }
+      if (!platformStep.buildModes.contains(buildInfo.mode)) {
+        continue;
+      }
+      if (selected != null) {
+        throwToolExit(
+          'Multiple platform steps registered for targetPlatform: ${buildInfo.targetPlatform} '
+          'and build mode: ${buildInfo.mode}.'
+        );
+      }
+      selected = platformStep;
+    }
+    if (selected == null) {
+      throwToolExit(
+        'No platform steps registered for targetPlatform: ${buildInfo.targetPlatform} '
+        'and build mode: ${buildInfo.mode}.'
+      );
+    }
+    return selected;
+  }
+}
+
+/// The workflow by which a flutter application in packaged into a platform
+/// specific application.
+abstract class PlatformBuildStep {
+  const PlatformBuildStep();
+
+  /// Build the platform specific bundle for [flutterProject].
+  Future<void> build({
+    FlutterProject project,
+    BuildInfo buildInfo,
+    String target,
+  });
+
+  /// The targets this platform step supports.
+  Set<TargetPlatform> get targetPlatforms;
+
+  /// The build modes this platform step supports.
+  Set<BuildMode> get buildModes;
+
+  /// Development artifacts required by this platform step.
+  Set<DevelopmentArtifact> get developmentArtifacts;
+}
diff --git a/packages/flutter_tools/test/platform_step_test.dart b/packages/flutter_tools/test/platform_step_test.dart
new file mode 100644
index 0000000..adaa433
--- /dev/null
+++ b/packages/flutter_tools/test/platform_step_test.dart
@@ -0,0 +1,101 @@
+// Copyright 2019 The Chromium 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:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/platform_step.dart';
+import 'package:flutter_tools/src/project.dart';
+
+import 'src/common.dart';
+import 'src/testbed.dart';
+
+void main() {
+  group(PlatformBuilders, () {
+    Testbed testbed;
+
+    setUp(() {
+      testbed = Testbed();
+    });
+
+    test('Fails if more than one builder matches', () => testbed.run(() {
+      const PlatformBuilders platformBuilders = PlatformBuilders(<PlatformBuildStep>[
+        FakePlatformStep(
+          buildModes: <BuildMode>{ BuildMode.debug, },
+          targetPlatforms: <TargetPlatform>{ TargetPlatform.android_arm }
+        ),
+        FakePlatformStep(
+          buildModes: <BuildMode>{ BuildMode.debug, },
+          targetPlatforms: <TargetPlatform>{ TargetPlatform.android_arm }
+        ),
+      ]);
+      const BuildInfo buildInfo = BuildInfo(
+        BuildMode.debug,
+        '',
+        targetPlatform: TargetPlatform.android_arm,
+      );
+
+      expect(() => platformBuilders.selectPlatform(buildInfo: buildInfo), throwsA(isA<ToolExit>()));
+    }));
+
+    test('Fails if no builders match', () => testbed.run(() {
+      const PlatformBuilders platformBuilders = PlatformBuilders(<PlatformBuildStep>[
+        FakePlatformStep(
+          buildModes: <BuildMode>{ BuildMode.debug, },
+          targetPlatforms: <TargetPlatform>{ TargetPlatform.android_arm }
+        ),
+      ]);
+      const BuildInfo buildInfo = BuildInfo(
+        BuildMode.debug,
+        '',
+        targetPlatform: TargetPlatform.ios,
+      );
+
+      expect(() => platformBuilders.selectPlatform(buildInfo: buildInfo), throwsA(isA<ToolExit>()));
+    }));
+
+    test('Finds a matching step', () => testbed.run(() {
+      const PlatformBuildStep iosStep = FakePlatformStep(
+        buildModes: <BuildMode>{ BuildMode.debug, },
+        targetPlatforms: <TargetPlatform>{ TargetPlatform.ios }
+      );
+      const PlatformBuilders platformBuilders = PlatformBuilders(<PlatformBuildStep>[
+        FakePlatformStep(
+          buildModes: <BuildMode>{ BuildMode.debug, },
+          targetPlatforms: <TargetPlatform>{ TargetPlatform.android_arm }
+        ),
+        iosStep,
+      ]);
+      const BuildInfo buildInfo = BuildInfo(
+        BuildMode.debug,
+        '',
+        targetPlatform: TargetPlatform.ios,
+      );
+
+      expect(platformBuilders.selectPlatform(buildInfo: buildInfo), iosStep);
+    }));
+  });
+}
+
+class FakePlatformStep extends PlatformBuildStep {
+  const FakePlatformStep({
+    this.buildModes,
+    this.developmentArtifacts,
+    this.targetPlatforms,
+  });
+
+  @override
+  Future<void> build({FlutterProject project, BuildInfo buildInfo, String target}) {
+    return null;
+  }
+
+  @override
+  final Set<BuildMode> buildModes;
+
+  @override
+  final Set<DevelopmentArtifact> developmentArtifacts;
+
+  @override
+  final Set<TargetPlatform> targetPlatforms;
+}