Materialize Flutter module, Android (#20520)

diff --git a/dev/devicelab/bin/tasks/module_test.dart b/dev/devicelab/bin/tasks/module_test.dart
index 6d31c95..75ee57a 100644
--- a/dev/devicelab/bin/tasks/module_test.dart
+++ b/dev/devicelab/bin/tasks/module_test.dart
@@ -40,7 +40,12 @@
         '\ndependencies:\n  battery:\n  package_info:\n',
       );
       await pubspec.writeAsString(content, flush: true);
-
+      await inDirectory(new Directory(path.join(directory.path, 'hello')), () async {
+        await flutter(
+          'packages',
+          options: <String>['get'],
+        );
+      });
 
       section('Build Flutter module library archive');
 
@@ -76,7 +81,7 @@
         );
       });
 
-      final bool apkBuilt = exists(new File(path.join(
+      final bool ephemeralHostApkBuilt = exists(new File(path.join(
         directory.path,
         'hello',
         'build',
@@ -87,10 +92,49 @@
         'app-release.apk',
       )));
 
-      if (!apkBuilt) {
+      if (!ephemeralHostApkBuilt) {
         return new TaskResult.failure('Failed to build ephemeral host .apk');
       }
 
+      section('Clean build');
+
+      await inDirectory(new Directory(path.join(directory.path, 'hello')), () async {
+        await flutter('clean');
+      });
+
+      section('Materialize host app');
+
+      await inDirectory(new Directory(path.join(directory.path, 'hello')), () async {
+        await flutter(
+          'materialize',
+          options: <String>['android'],
+        );
+      });
+
+      section('Build materialized host app');
+
+      await inDirectory(new Directory(path.join(directory.path, 'hello')), () async {
+        await flutter(
+          'build',
+          options: <String>['apk'],
+        );
+      });
+
+      final bool materializedHostApkBuilt = exists(new File(path.join(
+        directory.path,
+        'hello',
+        'build',
+        'host',
+        'outputs',
+        'apk',
+        'release',
+        'app-release.apk',
+      )));
+
+      if (!materializedHostApkBuilt) {
+        return new TaskResult.failure('Failed to build materialized host .apk');
+      }
+
       section('Add to Android app');
 
       final Directory hostApp = new Directory(path.join(directory.path, 'hello_host_app'));
diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart
index b0f326f..04ed92c 100644
--- a/packages/flutter_tools/lib/executable.dart
+++ b/packages/flutter_tools/lib/executable.dart
@@ -23,6 +23,7 @@
 import 'src/commands/inject_plugins.dart';
 import 'src/commands/install.dart';
 import 'src/commands/logs.dart';
+import 'src/commands/materialize.dart';
 import 'src/commands/packages.dart';
 import 'src/commands/precache.dart';
 import 'src/commands/run.dart';
@@ -67,6 +68,7 @@
     new InjectPluginsCommand(hidden: !verboseHelp),
     new InstallCommand(),
     new LogsCommand(),
+    new MaterializeCommand(),
     new PackagesCommand(),
     new PrecacheCommand(),
     new RunCommand(verboseHelp: verboseHelp),
diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart
index 2473479..a473164 100644
--- a/packages/flutter_tools/lib/src/android/gradle.dart
+++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -23,7 +23,7 @@
 import 'android_sdk.dart';
 import 'android_studio.dart';
 
-const String gradleVersion = '4.1';
+const String gradleVersion = '4.4';
 final RegExp _assembleTaskPattern = new RegExp(r'assemble([^:]+): task ');
 
 GradleProject _cachedGradleProject;
@@ -44,7 +44,7 @@
   r'|If you are using NDK, verify the ndk.dir is set to a valid NDK directory.  It is currently set to .*)');
 
 FlutterPluginVersion getFlutterPluginVersion(AndroidProject project) {
-  final File plugin = project.directory.childFile(
+  final File plugin = project.hostAppGradleRoot.childFile(
       fs.path.join('buildSrc', 'src', 'main', 'groovy', 'FlutterPlugin.groovy'));
   if (plugin.existsSync()) {
     final String packageLine = plugin.readAsLinesSync().skip(4).first;
@@ -53,8 +53,8 @@
     }
     return FlutterPluginVersion.v1;
   }
-  final File appGradle = project.directory.childFile(
-      fs.path.join('app','build.gradle'));
+  final File appGradle = project.hostAppGradleRoot.childFile(
+      fs.path.join('app', 'build.gradle'));
   if (appGradle.existsSync()) {
     for (String line in appGradle.readAsLinesSync()) {
       if (line.contains(new RegExp(r'apply from: .*/flutter.gradle'))) {
@@ -93,13 +93,13 @@
 Future<GradleProject> _readGradleProject() async {
   final FlutterProject flutterProject = await FlutterProject.current();
   final String gradle = await _ensureGradle(flutterProject);
-  await updateLocalProperties(project: flutterProject);
+  updateLocalProperties(project: flutterProject);
   final Status status = logger.startProgress('Resolving dependencies...', expectSlowOperation: true);
   GradleProject project;
   try {
     final RunResult runResult = await runCheckedAsync(
       <String>[gradle, 'app:properties'],
-      workingDirectory: flutterProject.android.directory.path,
+      workingDirectory: flutterProject.android.hostAppGradleRoot.path,
       environment: _gradleEnv,
     );
     final String properties = runResult.stdout.trim();
@@ -166,7 +166,7 @@
 // Note: Gradle may be bootstrapped and possibly downloaded as a side-effect
 // of validating the Gradle executable. This may take several seconds.
 Future<String> _initializeGradle(FlutterProject project) async {
-  final Directory android = project.android.directory;
+  final Directory android = project.android.hostAppGradleRoot;
   final Status status = logger.startProgress('Initializing gradle...', expectSlowOperation: true);
   String gradle = _locateGradlewExecutable(android);
   if (gradle == null) {
@@ -205,13 +205,13 @@
 ///
 /// If [requireAndroidSdk] is true (the default) and no Android SDK is found,
 /// this will fail with a [ToolExit].
-Future<void> updateLocalProperties({
+void updateLocalProperties({
   @required FlutterProject project,
   BuildInfo buildInfo,
   bool requireAndroidSdk = true,
-}) async {
-  if (requireAndroidSdk && androidSdk == null) {
-    throwToolExit('Unable to locate Android SDK. Please run `flutter doctor` for more details.');
+}) {
+  if (requireAndroidSdk) {
+    _exitIfNoAndroidSdk();
   }
 
   final File localProperties = project.android.localPropertiesFile;
@@ -250,6 +250,24 @@
     settings.writeContents(localProperties);
 }
 
+/// Writes standard Android local properties to the specified [properties] file.
+///
+/// Writes the path to the Android SDK, if known.
+void writeLocalProperties(File properties) {
+  final SettingsFile settings = new SettingsFile();
+  if (androidSdk != null) {
+    settings.values['sdk.dir'] = escapePath(androidSdk.directory);
+  }
+  settings.writeContents(properties);
+}
+
+/// Throws a ToolExit, if the path to the Android SDK is not known.
+void _exitIfNoAndroidSdk() {
+  if (androidSdk == null) {
+    throwToolExit('Unable to locate Android SDK. Please run `flutter doctor` for more details.');
+  }
+}
+
 Future<Null> buildGradleProject({
   @required FlutterProject project,
   @required BuildInfo buildInfo,
@@ -263,7 +281,7 @@
   // and can be overwritten with flutter build command.
   // The default Gradle script reads the version name and number
   // from the local.properties file.
-  await updateLocalProperties(project: project, buildInfo: buildInfo);
+  updateLocalProperties(project: project, buildInfo: buildInfo);
 
   final String gradle = await _ensureGradle(project);
 
@@ -284,7 +302,7 @@
   final Status status = logger.startProgress('Running \'gradlew build\'...', expectSlowOperation: true);
   final int exitCode = await runCommandAndStreamOutput(
     <String>[fs.file(gradle).absolute.path, 'build'],
-    workingDirectory: project.android.directory.path,
+    workingDirectory: project.android.hostAppGradleRoot.path,
     allowReentrantFlutter: true,
     environment: _gradleEnv,
   );
@@ -361,7 +379,7 @@
   command.add(assembleTask);
   final int exitCode = await runCommandAndStreamOutput(
     command,
-    workingDirectory: flutterProject.android.directory.path,
+    workingDirectory: flutterProject.android.hostAppGradleRoot.path,
     allowReentrantFlutter: true,
     environment: _gradleEnv,
     filter: logger.isVerbose ? null : ndkMessageFilter,
diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart
index 27e99bf..ac7faf5 100644
--- a/packages/flutter_tools/lib/src/application_package.dart
+++ b/packages/flutter_tools/lib/src/application_package.dart
@@ -107,7 +107,7 @@
       apkFile = fs.file(fs.path.join(getAndroidBuildDirectory(), 'app.apk'));
     }
 
-    final File manifest = androidProject.gradleManifestFile;
+    final File manifest = androidProject.appManifestFile;
 
     if (!manifest.existsSync())
       return null;
diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart
index b5f6cca..fb1a2c0 100644
--- a/packages/flutter_tools/lib/src/commands/create.dart
+++ b/packages/flutter_tools/lib/src/commands/create.dart
@@ -286,8 +286,7 @@
       );
     }
     final FlutterProject project = await FlutterProject.fromDirectory(directory);
-    if (android_sdk.androidSdk != null)
-      await gradle.updateLocalProperties(project: project);
+    gradle.updateLocalProperties(project: project, requireAndroidSdk: false);
 
     final String projectName = templateContext['projectName'];
     final String organization = templateContext['organization'];
@@ -320,8 +319,7 @@
       await project.ensureReadyForPlatformSpecificTooling();
     }
 
-    if (android_sdk.androidSdk != null)
-      await gradle.updateLocalProperties(project: project);
+    gradle.updateLocalProperties(project: project, requireAndroidSdk: false);
 
     return generatedCount;
   }
@@ -373,7 +371,7 @@
     int filesCreated = 0;
     copyDirectorySync(
       cache.getArtifactDirectory('gradle_wrapper'),
-      project.android.directory,
+      project.android.hostAppGradleRoot,
       (File sourceFile, File destinationFile) {
         filesCreated++;
         final String modes = sourceFile.statSync().modeString();
diff --git a/packages/flutter_tools/lib/src/commands/materialize.dart b/packages/flutter_tools/lib/src/commands/materialize.dart
new file mode 100644
index 0000000..0c0f27d
--- /dev/null
+++ b/packages/flutter_tools/lib/src/commands/materialize.dart
@@ -0,0 +1,78 @@
+// Copyright 2018 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 'dart:async';
+import 'package:meta/meta.dart';
+import '../base/common.dart';
+import '../project.dart';
+import '../runner/flutter_command.dart';
+
+class MaterializeCommand extends FlutterCommand {
+  MaterializeCommand() {
+    addSubcommand(new MaterializeAndroidCommand());
+    addSubcommand(new MaterializeIosCommand());
+  }
+
+  @override
+  final String name = 'materialize';
+
+  @override
+  final String description = 'Commands for materializing host apps for a Flutter Module';
+
+  @override
+  bool get hidden => true;
+
+  @override
+  Future<Null> runCommand() async { }
+}
+
+abstract class MaterializeSubCommand extends FlutterCommand {
+  MaterializeSubCommand() {
+    requiresPubspecYaml();
+  }
+
+  FlutterProject _project;
+
+  @override
+  @mustCallSuper
+  Future<Null> runCommand() async {
+    await _project.ensureReadyForPlatformSpecificTooling();
+  }
+
+  @override
+  Future<Null> validateCommand() async {
+    await super.validateCommand();
+    _project = await FlutterProject.current();
+    if (!_project.isModule)
+      throw new ToolExit("Only projects created using 'flutter create -t module' can be materialized.");
+  }
+}
+
+class MaterializeAndroidCommand extends MaterializeSubCommand {
+  @override
+  String get name => 'android';
+
+  @override
+  String get description => 'Materialize an Android host app';
+
+  @override
+  Future<Null> runCommand() async {
+    await super.runCommand();
+    await _project.android.materialize();
+  }
+}
+
+class MaterializeIosCommand extends MaterializeSubCommand {
+  @override
+  String get name => 'ios';
+
+  @override
+  String get description => 'Materialize an iOS host app';
+
+  @override
+  Future<Null> runCommand() async {
+    await super.runCommand();
+    await _project.ios.materialize();
+  }
+}
diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart
index 1ec19a7..3d31b83 100644
--- a/packages/flutter_tools/lib/src/devfs.dart
+++ b/packages/flutter_tools/lib/src/devfs.dart
@@ -329,7 +329,7 @@
 }
 
 class DevFS {
-  /// Create a [DevFS] named [fsName] for the local files in [directory].
+  /// Create a [DevFS] named [fsName] for the local files in [rootDirectory].
   DevFS(VMService serviceProtocol,
         this.fsName,
         this.rootDirectory, {
diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart
index 8943720..9a6afe8 100644
--- a/packages/flutter_tools/lib/src/plugins.dart
+++ b/packages/flutter_tools/lib/src/plugins.dart
@@ -105,7 +105,7 @@
   return oldContents != newContents;
 }
 
-/// Returns the contents of the `.flutter-plugins` file in [directory], or
+/// Returns the contents of the `.flutter-plugins` file in [project], or
 /// null if that file does not exist.
 String _readFlutterPluginsList(FlutterProject project) {
   return project.flutterPluginsFile.existsSync()
@@ -295,8 +295,7 @@
   }
 }
 
-/// Returns whether the Flutter project at the specified [directory]
-/// has any plugin dependencies.
+/// Returns whether the specified Flutter [project] has any plugin dependencies.
 bool hasPlugins(FlutterProject project) {
   return _readFlutterPluginsList(project) != null;
 }
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
index 3c8743e..d54d8e9 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -186,6 +186,10 @@
     }
   }
 
+  Future<void> materialize() async {
+    throwToolExit('flutter materialize has not yet been implemented for iOS');
+  }
+
   bool _shouldRegenerateFromTemplate() {
     return Cache.instance.fileOlderThanToolsStamp(directory.childFile('podhelper.rb'));
   }
@@ -212,62 +216,112 @@
   /// The parent of this project.
   final FlutterProject parent;
 
-  /// The directory of this project.
-  Directory get directory => parent.directory.childDirectory(isModule ? '.android' : 'android');
+  /// The Gradle root directory of the Android host app. This is the directory
+  /// containing the `app/` subdirectory and the `settings.gradle` file that
+  /// includes it in the overall Gradle project.
+  Directory get hostAppGradleRoot {
+    if (!isModule || _materializedDirectory.existsSync())
+      return _materializedDirectory;
+    return _ephemeralDirectory;
+  }
+
+  /// The Gradle root directory of the Android wrapping of Flutter and plugins.
+  /// This is the same as [hostAppGradleRoot] except when the project is
+  /// a Flutter module with a materialized host app.
+  Directory get _flutterLibGradleRoot => isModule ? _ephemeralDirectory : _materializedDirectory;
+
+  Directory get _ephemeralDirectory => parent.directory.childDirectory('.android');
+  Directory get _materializedDirectory => parent.directory.childDirectory('android');
 
   /// True, if the parent Flutter project is a module.
   bool get isModule => parent.isModule;
 
-  File get gradleManifestFile {
+  File get appManifestFile {
     return isUsingGradle()
-        ? fs.file(fs.path.join(directory.path, 'app', 'src', 'main', 'AndroidManifest.xml'))
-        : directory.childFile('AndroidManifest.xml');
+        ? fs.file(fs.path.join(hostAppGradleRoot.path, 'app', 'src', 'main', 'AndroidManifest.xml'))
+        : hostAppGradleRoot.childFile('AndroidManifest.xml');
   }
 
   File get gradleAppOutV1File => gradleAppOutV1Directory.childFile('app-debug.apk');
 
   Directory get gradleAppOutV1Directory {
-    return fs.directory(fs.path.join(directory.path, 'app', 'build', 'outputs', 'apk'));
+    return fs.directory(fs.path.join(hostAppGradleRoot.path, 'app', 'build', 'outputs', 'apk'));
   }
 
   bool isUsingGradle() {
-    return directory.childFile('build.gradle').existsSync();
+    return hostAppGradleRoot.childFile('build.gradle').existsSync();
   }
 
   Future<String> applicationId() {
-    final File gradleFile = directory.childDirectory('app').childFile('build.gradle');
+    final File gradleFile = hostAppGradleRoot.childDirectory('app').childFile('build.gradle');
     return _firstMatchInFile(gradleFile, _applicationIdPattern).then((Match match) => match?.group(1));
   }
 
   Future<String> group() {
-    final File gradleFile = directory.childFile('build.gradle');
+    final File gradleFile = hostAppGradleRoot.childFile('build.gradle');
     return _firstMatchInFile(gradleFile, _groupPattern).then((Match match) => match?.group(1));
   }
 
   Future<void> ensureReadyForPlatformSpecificTooling() async {
     if (isModule && _shouldRegenerateFromTemplate()) {
-      final Template template = new Template.fromName(fs.path.join('module', 'android'));
-      template.render(
-        directory,
-        <String, dynamic>{
-          'androidIdentifier': parent.manifest.androidPackage,
-        },
-        printStatusWhenWriting: false,
-      );
-      gradle.injectGradleWrapper(directory);
+      _regenerateLibrary();
+      // Add ephemeral host app, if a materialized host app does not already exist.
+      if (!_materializedDirectory.existsSync()) {
+        _overwriteFromTemplate(fs.path.join('module', 'android', 'host_app_common'), _ephemeralDirectory);
+        _overwriteFromTemplate(fs.path.join('module', 'android', 'host_app_ephemeral'), _ephemeralDirectory);
+      }
     }
-    if (!directory.existsSync())
+    if (!hostAppGradleRoot.existsSync()) {
       return;
-    await gradle.updateLocalProperties(project: parent, requireAndroidSdk: false);
+    }
+    gradle.updateLocalProperties(project: parent, requireAndroidSdk: false);
   }
 
   bool _shouldRegenerateFromTemplate() {
-    return Cache.instance.fileOlderThanToolsStamp(directory.childFile('build.gradle'));
+    return Cache.instance.fileOlderThanToolsStamp(_ephemeralDirectory.childFile('build.gradle'));
   }
 
-  File get localPropertiesFile => directory.childFile('local.properties');
+  Future<void> materialize() async {
+    assert(isModule);
+    if (_materializedDirectory.existsSync())
+      throwToolExit('Android host app already materialized. To redo materialization, delete the android/ folder.');
+    _regenerateLibrary();
+    _overwriteFromTemplate(fs.path.join('module', 'android', 'host_app_common'), _materializedDirectory);
+    _overwriteFromTemplate(fs.path.join('module', 'android', 'host_app_materialized'), _materializedDirectory);
+    _overwriteFromTemplate(fs.path.join('module', 'android', 'gradle'), _materializedDirectory);
+    gradle.injectGradleWrapper(_materializedDirectory);
+    gradle.writeLocalProperties(_materializedDirectory.childFile('local.properties'));
+    await injectPlugins(parent);
+  }
 
-  Directory get pluginRegistrantHost => directory.childDirectory(isModule ? 'Flutter' : 'app');
+  File get localPropertiesFile => _flutterLibGradleRoot.childFile('local.properties');
+
+  Directory get pluginRegistrantHost => _flutterLibGradleRoot.childDirectory(isModule ? 'Flutter' : 'app');
+
+  void _regenerateLibrary() {
+    _deleteIfExistsSync(_ephemeralDirectory);
+    _overwriteFromTemplate(fs.path.join('module', 'android', 'library'), _ephemeralDirectory);
+    _overwriteFromTemplate(fs.path.join('module', 'android', 'gradle'), _ephemeralDirectory);
+    gradle.injectGradleWrapper(_ephemeralDirectory);
+  }
+
+  void _deleteIfExistsSync(Directory directory) {
+    if (directory.existsSync())
+      directory.deleteSync(recursive: true);
+  }
+
+  void _overwriteFromTemplate(String path, Directory target) {
+    final Template template = new Template.fromName(path);
+    template.render(
+      target,
+      <String, dynamic>{
+        'projectName': parent.manifest.appName,
+        'androidIdentifier': parent.manifest.androidPackage,
+      },
+      printStatusWhenWriting: false,
+      overwriteExisting: true,
+    );
+  }
 }
 
 /// Asynchronously returns the first line-based match for [regExp] in [file].
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index 03ba79b..b5f93b3 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -905,7 +905,7 @@
     case TargetPlatform.android_x64:
     case TargetPlatform.android_x86:
       final FlutterProject project = await FlutterProject.current();
-      final String manifestPath = fs.path.relative(project.android.gradleManifestFile.path);
+      final String manifestPath = fs.path.relative(project.android.appManifestFile.path);
       return 'Is your project missing an $manifestPath?\nConsider running "flutter create ." to create one.';
     case TargetPlatform.ios:
       return 'Is your project missing an ios/Runner/Info.plist?\nConsider running "flutter create ." to create one.';
diff --git a/packages/flutter_tools/templates/module/README.md b/packages/flutter_tools/templates/module/README.md
new file mode 100644
index 0000000..f5dc3b0
--- /dev/null
+++ b/packages/flutter_tools/templates/module/README.md
@@ -0,0 +1,67 @@
+# Templates for Flutter Module
+
+## common
+
+Written to root of Flutter module.
+
+Adds Dart project files including `pubspec.yaml`.
+
+## android
+
+#### library
+
+Written to the `.android/` hidden folder.
+
+Contents wraps Flutter/Dart code as a Gradle project that defines an
+Android library.
+
+Executing `./gradlew flutter:assembleDebug` in that folder produces
+a `.aar` archive.
+
+Android host apps can set up a dependency to this project to consume
+Flutter views.
+
+#### gradle
+
+Written to `.android/` or `android/`.
+
+Mixin for adding Gradle boilerplate to Android projects. The `build.gradle`
+file is a template file so that it is created, not copied, on instantiation.
+That way, its timestamp reflects template instantiation time.
+
+#### host_app_common
+
+Written to either `.android/` or `android/`.
+
+Contents define a single-Activity, single-View Android host app
+with a dependency on the `.android/Flutter` library.
+
+Executing `./gradlew app:assembleDebug` in the target folder produces
+an `.apk` archive.
+
+Used with either `android_host_ephemeral` or `android_host_materialized`.
+
+#### host_app_ephemeral
+
+Written to `.android/` on top of `android_host_common`.
+
+Combined contents define an *ephemeral* (hidden, auto-generated,
+under Flutter tooling control) Android host app with a dependency on the
+`.android/Flutter` library.
+
+#### host_app_materialized
+
+Written to `android/` on top of `android_host_common`.
+
+Combined contents define a *materialized* (visible, one-time generated,
+under app author control) Android host app with a dependency on the
+`.android/Flutter` library.
+
+## ios
+
+Written to the `.ios/` hidden folder.
+
+Contents wraps Flutter/Dart code as a CocoaPods pod.
+
+iOS host apps can set up a dependency to this project to consume
+Flutter views.
diff --git a/packages/flutter_tools/templates/module/android/gradle.tmpl/wrapper/gradle-wrapper.properties b/packages/flutter_tools/templates/module/android/gradle.tmpl/wrapper/gradle-wrapper.properties
deleted file mode 100644
index 9372d0f..0000000
--- a/packages/flutter_tools/templates/module/android/gradle.tmpl/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-#Fri Jun 23 08:50:38 CEST 2017
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
diff --git a/packages/flutter_tools/templates/module/android/build.gradle.tmpl b/packages/flutter_tools/templates/module/android/gradle/build.gradle.tmpl
similarity index 82%
rename from packages/flutter_tools/templates/module/android/build.gradle.tmpl
rename to packages/flutter_tools/templates/module/android/gradle/build.gradle.tmpl
index 99a1611..b6810f1 100644
--- a/packages/flutter_tools/templates/module/android/build.gradle.tmpl
+++ b/packages/flutter_tools/templates/module/android/gradle/build.gradle.tmpl
@@ -7,7 +7,7 @@
     }
 
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.1.3'
+        classpath 'com.android.tools.build:gradle:3.1.4'
     }
 }
 
diff --git a/packages/flutter_tools/templates/module/android/gradle.properties.tmpl b/packages/flutter_tools/templates/module/android/gradle/gradle.properties.copy.tmpl
similarity index 100%
rename from packages/flutter_tools/templates/module/android/gradle.properties.tmpl
rename to packages/flutter_tools/templates/module/android/gradle/gradle.properties.copy.tmpl
diff --git a/packages/flutter_tools/templates/module/android/app.tmpl/build.gradle.tmpl b/packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/build.gradle.tmpl
similarity index 100%
rename from packages/flutter_tools/templates/module/android/app.tmpl/build.gradle.tmpl
rename to packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/build.gradle.tmpl
diff --git a/packages/flutter_tools/templates/module/android/app.tmpl/src/main/AndroidManifest.xml.tmpl b/packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/src/main/AndroidManifest.xml.tmpl
similarity index 97%
rename from packages/flutter_tools/templates/module/android/app.tmpl/src/main/AndroidManifest.xml.tmpl
rename to packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/src/main/AndroidManifest.xml.tmpl
index e9f4d8e..70eca97 100644
--- a/packages/flutter_tools/templates/module/android/app.tmpl/src/main/AndroidManifest.xml.tmpl
+++ b/packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/src/main/AndroidManifest.xml.tmpl
@@ -15,6 +15,7 @@
          FlutterApplication and put your custom class here. -->
     <application
         android:name="io.flutter.app.FlutterApplication"
+        android:label="{{projectName}}"
         android:icon="@mipmap/ic_launcher">
         <activity
             android:name=".MainActivity"
diff --git a/packages/flutter_tools/templates/module/android/app.tmpl/src/main/java/androidIdentifier/host/MainActivity.java.tmpl b/packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/src/main/java/androidIdentifier/host/MainActivity.java.tmpl
similarity index 100%
rename from packages/flutter_tools/templates/module/android/app.tmpl/src/main/java/androidIdentifier/host/MainActivity.java.tmpl
rename to packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/src/main/java/androidIdentifier/host/MainActivity.java.tmpl
diff --git a/packages/flutter_tools/templates/module/android/app.tmpl/src/main/res/drawable/launch_background.xml b/packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/src/main/res/drawable/launch_background.xml
similarity index 100%
rename from packages/flutter_tools/templates/module/android/app.tmpl/src/main/res/drawable/launch_background.xml
rename to packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/src/main/res/drawable/launch_background.xml
diff --git a/packages/flutter_tools/templates/module/android/app.tmpl/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/src/main/res/mipmap-hdpi/ic_launcher.png
similarity index 100%
rename from packages/flutter_tools/templates/module/android/app.tmpl/src/main/res/mipmap-hdpi/ic_launcher.png
rename to packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/flutter_tools/templates/module/android/app.tmpl/src/main/res/values/styles.xml b/packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/src/main/res/values/styles.xml
similarity index 100%
rename from packages/flutter_tools/templates/module/android/app.tmpl/src/main/res/values/styles.xml
rename to packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/src/main/res/values/styles.xml
diff --git a/packages/flutter_tools/templates/module/android/settings.gradle.tmpl b/packages/flutter_tools/templates/module/android/host_app_ephemeral/settings.gradle.copy.tmpl
similarity index 100%
rename from packages/flutter_tools/templates/module/android/settings.gradle.tmpl
rename to packages/flutter_tools/templates/module/android/host_app_ephemeral/settings.gradle.copy.tmpl
diff --git a/packages/flutter_tools/templates/module/android/host_app_materialized/settings.gradle.copy.tmpl b/packages/flutter_tools/templates/module/android/host_app_materialized/settings.gradle.copy.tmpl
new file mode 100644
index 0000000..3f3ac34
--- /dev/null
+++ b/packages/flutter_tools/templates/module/android/host_app_materialized/settings.gradle.copy.tmpl
@@ -0,0 +1,5 @@
+// Generated file. Do not edit.
+include ':app'
+
+setBinding(new Binding([gradle: this]))
+evaluate(new File('../.android/include_flutter.groovy'))
diff --git a/packages/flutter_tools/templates/module/android/Flutter.tmpl/build.gradle.tmpl b/packages/flutter_tools/templates/module/android/library/Flutter.tmpl/build.gradle.tmpl
similarity index 100%
rename from packages/flutter_tools/templates/module/android/Flutter.tmpl/build.gradle.tmpl
rename to packages/flutter_tools/templates/module/android/library/Flutter.tmpl/build.gradle.tmpl
diff --git a/packages/flutter_tools/templates/module/android/Flutter.tmpl/src/main/AndroidManifest.xml.tmpl b/packages/flutter_tools/templates/module/android/library/Flutter.tmpl/src/main/AndroidManifest.xml.tmpl
similarity index 100%
rename from packages/flutter_tools/templates/module/android/Flutter.tmpl/src/main/AndroidManifest.xml.tmpl
rename to packages/flutter_tools/templates/module/android/library/Flutter.tmpl/src/main/AndroidManifest.xml.tmpl
diff --git a/packages/flutter_tools/templates/module/android/Flutter.tmpl/src/main/java/io/flutter/facade/Flutter.java b/packages/flutter_tools/templates/module/android/library/Flutter.tmpl/src/main/java/io/flutter/facade/Flutter.java
similarity index 100%
rename from packages/flutter_tools/templates/module/android/Flutter.tmpl/src/main/java/io/flutter/facade/Flutter.java
rename to packages/flutter_tools/templates/module/android/library/Flutter.tmpl/src/main/java/io/flutter/facade/Flutter.java
diff --git a/packages/flutter_tools/templates/module/android/Flutter.tmpl/src/main/java/io/flutter/facade/FlutterFragment.java b/packages/flutter_tools/templates/module/android/library/Flutter.tmpl/src/main/java/io/flutter/facade/FlutterFragment.java
similarity index 100%
rename from packages/flutter_tools/templates/module/android/Flutter.tmpl/src/main/java/io/flutter/facade/FlutterFragment.java
rename to packages/flutter_tools/templates/module/android/library/Flutter.tmpl/src/main/java/io/flutter/facade/FlutterFragment.java
diff --git a/packages/flutter_tools/templates/module/android/include_flutter.groovy.tmpl b/packages/flutter_tools/templates/module/android/library/include_flutter.groovy.copy.tmpl
similarity index 100%
rename from packages/flutter_tools/templates/module/android/include_flutter.groovy.tmpl
rename to packages/flutter_tools/templates/module/android/library/include_flutter.groovy.copy.tmpl
diff --git a/packages/flutter_tools/templates/module/android/library/settings.gradle.copy.tmpl b/packages/flutter_tools/templates/module/android/library/settings.gradle.copy.tmpl
new file mode 100644
index 0000000..49eb997
--- /dev/null
+++ b/packages/flutter_tools/templates/module/android/library/settings.gradle.copy.tmpl
@@ -0,0 +1,5 @@
+// Generated file. Do not edit.
+
+rootProject.name = 'android_generated'
+setBinding(new Binding([gradle: this]))
+evaluate(new File('include_flutter.groovy'))
diff --git a/packages/flutter_tools/test/android/gradle_test.dart b/packages/flutter_tools/test/android/gradle_test.dart
index ed8741e..e8a2419 100644
--- a/packages/flutter_tools/test/android/gradle_test.dart
+++ b/packages/flutter_tools/test/android/gradle_test.dart
@@ -33,7 +33,7 @@
         // This test is written to fail if our bots get Android SDKs in the future: shouldBeToolExit
         // will be null and our expectation would fail. That would remind us to make these tests
         // hermetic before adding Android SDKs to the bots.
-        await updateLocalProperties(project: await FlutterProject.current());
+        updateLocalProperties(project: await FlutterProject.current());
       } on Exception catch (e) {
         shouldBeToolExit = e;
       }
@@ -190,7 +190,7 @@
       writeSchemaFile(fs, schemaData);
 
       try {
-        await updateLocalProperties(
+        updateLocalProperties(
           project: await FlutterProject.fromPath('path/to/project'),
           buildInfo: buildInfo,
         );
diff --git a/packages/flutter_tools/test/project_test.dart b/packages/flutter_tools/test/project_test.dart
index a8b7388..7a4775d 100644
--- a/packages/flutter_tools/test/project_test.dart
+++ b/packages/flutter_tools/test/project_test.dart
@@ -27,17 +27,6 @@
         );
       });
 
-      Future<Null> expectToolExitLater(Future<dynamic> future, Matcher messageMatcher) async {
-        try {
-          await future;
-          fail('ToolExit expected, but nothing thrown');
-        } on ToolExit catch(e) {
-          expect(e.message, messageMatcher);
-        } catch(e, trace) {
-          fail('ToolExit expected, got $e\n$trace');
-        }
-      }
-
       testInMemory('fails on invalid pubspec.yaml', () async {
         final Directory directory = fs.directory('myproject');
         directory.childFile('pubspec.yaml')
@@ -95,16 +84,55 @@
           fs.currentDirectory.absolute.path,
         );
       });
+    });
 
+    group('materialize Android', () {
+      testInMemory('fails on non-module', () async {
+        final FlutterProject project = await someProject();
+        await expectLater(
+          project.android.materialize(),
+          throwsA(const isInstanceOf<AssertionError>()),
+        );
+      });
+      testInMemory('exits on already materialized module', () async {
+        final FlutterProject project = await aModuleProject();
+        await project.android.materialize();
+        expectToolExitLater(project.android.materialize(), contains('already materialized'));
+      });
+      testInMemory('creates android/app folder in place of .android/app', () async {
+        final FlutterProject project = await aModuleProject();
+        await project.android.materialize();
+        expectNotExists(project.directory.childDirectory('.android').childDirectory('app'));
+        expect(
+          project.directory.childDirectory('.android').childFile('settings.gradle').readAsStringSync(),
+          isNot(contains("include ':app'")),
+        );
+        expectExists(project.directory.childDirectory('android').childDirectory('app'));
+        expectExists(project.directory.childDirectory('android').childFile('local.properties'));
+        expect(
+          project.directory.childDirectory('android').childFile('settings.gradle').readAsStringSync(),
+          contains("include ':app'"),
+        );
+      });
+      testInMemory('retains .android/Flutter folder and references it', () async {
+        final FlutterProject project = await aModuleProject();
+        await project.android.materialize();
+        expectExists(project.directory.childDirectory('.android').childDirectory('Flutter'));
+        expect(
+          project.directory.childDirectory('android').childFile('settings.gradle').readAsStringSync(),
+          contains('../.android/include_flutter.groovy'),
+        );
+      });
+      testInMemory('can be redone after deletion', () async {
+        final FlutterProject project = await aModuleProject();
+        await project.android.materialize();
+        project.directory.childDirectory('android').deleteSync(recursive: true);
+        await project.android.materialize();
+        expectExists(project.directory.childDirectory('android').childDirectory('app'));
+      });
     });
 
     group('ensure ready for platform-specific tooling', () {
-      void expectExists(FileSystemEntity entity) {
-        expect(entity.existsSync(), isTrue);
-      }
-      void expectNotExists(FileSystemEntity entity) {
-        expect(entity.existsSync(), isFalse);
-      }
       testInMemory('does nothing, if project is not created', () async {
         final FlutterProject project = new FlutterProject(
           fs.directory('not_created'),
@@ -118,9 +146,9 @@
         final FlutterProject project = await aPluginProject();
         await project.ensureReadyForPlatformSpecificTooling();
         expectNotExists(project.ios.directory.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h'));
-        expectNotExists(androidPluginRegistrant(project.android.directory.childDirectory('app')));
+        expectNotExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')));
         expectNotExists(project.ios.directory.childDirectory('Flutter').childFile('Generated.xcconfig'));
-        expectNotExists(project.android.directory.childFile('local.properties'));
+        expectNotExists(project.android.hostAppGradleRoot.childFile('local.properties'));
       });
       testInMemory('injects plugins for iOS', () async {
         final FlutterProject project = await someProject();
@@ -135,19 +163,19 @@
       testInMemory('injects plugins for Android', () async {
         final FlutterProject project = await someProject();
         await project.ensureReadyForPlatformSpecificTooling();
-        expectExists(androidPluginRegistrant(project.android.directory.childDirectory('app')));
+        expectExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')));
       });
       testInMemory('updates local properties for Android', () async {
         final FlutterProject project = await someProject();
         await project.ensureReadyForPlatformSpecificTooling();
-        expectExists(project.android.directory.childFile('local.properties'));
+        expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
       });
       testInMemory('creates Android library in module', () async {
         final FlutterProject project = await aModuleProject();
         await project.ensureReadyForPlatformSpecificTooling();
-        expectExists(project.android.directory.childFile('settings.gradle'));
-        expectExists(project.android.directory.childFile('local.properties'));
-        expectExists(androidPluginRegistrant(project.android.directory.childDirectory('Flutter')));
+        expectExists(project.android.hostAppGradleRoot.childFile('settings.gradle'));
+        expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
+        expectExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('Flutter')));
       });
       testInMemory('creates iOS pod in module', () async {
         final FlutterProject project = await aModuleProject();
@@ -169,7 +197,7 @@
         expect(project.isModule, isTrue);
         expect(project.android.isModule, isTrue);
         expect(project.ios.isModule, isTrue);
-        expect(project.android.directory.basename, '.android');
+        expect(project.android.hostAppGradleRoot.basename, '.android');
         expect(project.ios.directory.basename, '.ios');
       });
       testInMemory('is known for non-module', () async {
@@ -177,7 +205,7 @@
         expect(project.isModule, isFalse);
         expect(project.android.isModule, isFalse);
         expect(project.ios.isModule, isFalse);
-        expect(project.android.directory.basename, 'android');
+        expect(project.android.hostAppGradleRoot.basename, 'android');
         expect(project.ios.directory.basename, 'ios');
       });
     });
@@ -326,6 +354,25 @@
   }
 }
 
+Future<Null> expectToolExitLater(Future<dynamic> future, Matcher messageMatcher) async {
+  try {
+    await future;
+    fail('ToolExit expected, but nothing thrown');
+  } on ToolExit catch(e) {
+    expect(e.message, messageMatcher);
+  } catch(e, trace) {
+    fail('ToolExit expected, got $e\n$trace');
+  }
+}
+
+void expectExists(FileSystemEntity entity) {
+  expect(entity.existsSync(), isTrue);
+}
+
+void expectNotExists(FileSystemEntity entity) {
+  expect(entity.existsSync(), isFalse);
+}
+
 void addIosWithBundleId(Directory directory, String id) {
   directory
       .childDirectory('ios')