Add module template for Android (#18697)

diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart
index 1cefa1f..833ae58 100644
--- a/packages/flutter_tools/lib/src/android/gradle.dart
+++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -141,16 +141,13 @@
   }
 }
 
-String _locateProjectGradlew({ bool ensureExecutable = true }) {
-  final String path = fs.path.join(
-    'android',
+String _locateGradlewExecutable(Directory directory) {
+  final File gradle = directory.childFile(
     platform.isWindows ? 'gradlew.bat' : 'gradlew',
   );
 
-  if (fs.isFileSync(path)) {
-    final File gradle = fs.file(path);
-    if (ensureExecutable)
-      os.makeExecutable(gradle);
+  if (gradle.existsSync()) {
+    os.makeExecutable(gradle);
     return gradle.absolute.path;
   } else {
     return null;
@@ -165,11 +162,12 @@
 // 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() async {
+  final Directory android = fs.directory('android');
   final Status status = logger.startProgress('Initializing gradle...', expectSlowOperation: true);
-  String gradle = _locateProjectGradlew();
+  String gradle = _locateGradlewExecutable(android);
   if (gradle == null) {
-    _injectGradleWrapper();
-    gradle = _locateProjectGradlew();
+    injectGradleWrapper(android);
+    gradle = _locateGradlewExecutable(android);
   }
   if (gradle == null)
     throwToolExit('Unable to locate gradlew script');
@@ -181,11 +179,13 @@
   return gradle;
 }
 
-void _injectGradleWrapper() {
-  copyDirectorySync(cache.getArtifactDirectory('gradle_wrapper'), fs.directory('android'));
-  final String propertiesPath = fs.path.join('android', 'gradle', 'wrapper', 'gradle-wrapper.properties');
-  if (!fs.file(propertiesPath).existsSync()) {
-    fs.file(propertiesPath).writeAsStringSync('''
+/// Injects the Gradle wrapper into the specified directory.
+void injectGradleWrapper(Directory directory) {
+  copyDirectorySync(cache.getArtifactDirectory('gradle_wrapper'), directory);
+  _locateGradlewExecutable(directory);
+  final File propertiesFile = directory.childFile(fs.path.join('gradle', 'wrapper', 'gradle-wrapper.properties'));
+  if (!propertiesFile.existsSync()) {
+    propertiesFile.writeAsStringSync('''
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
@@ -196,14 +196,31 @@
   }
 }
 
-/// Create android/local.properties if needed, and update Flutter settings.
+/// Overwrite android/local.properties in the specified Flutter project, if needed.
+///
+/// Throws, if `pubspec.yaml` or Android SDK cannot be located.
 Future<void> updateLocalProperties({String projectPath, BuildInfo buildInfo}) async {
-  final File localProperties = (projectPath == null)
-      ? fs.file(fs.path.join('android', 'local.properties'))
-      : fs.file(fs.path.join(projectPath, 'android', 'local.properties'));
+  final Directory android = (projectPath == null)
+      ? fs.directory('android')
+      : fs.directory(fs.path.join(projectPath, 'android'));
   final String flutterManifest = (projectPath == null)
       ? fs.path.join(bundle.defaultManifestPath)
       : fs.path.join(projectPath, bundle.defaultManifestPath);
+  if (androidSdk == null) {
+    throwToolExit('Unable to locate Android SDK. Please run `flutter doctor` for more details.');
+  }
+  FlutterManifest manifest;
+  try {
+    manifest = await FlutterManifest.createFromPath(flutterManifest);
+  } catch (error) {
+    throwToolExit('Failed to load pubspec.yaml: $error');
+  }
+  updateLocalPropertiesSync(android, manifest, buildInfo);
+}
+
+/// Overwrite local.properties in the specified directory, if needed.
+void updateLocalPropertiesSync(Directory android, FlutterManifest manifest, [BuildInfo buildInfo]) {
+  final File localProperties = android.childFile('local.properties');
   bool changed = false;
 
   SettingsFile settings;
@@ -211,40 +228,27 @@
     settings = new SettingsFile.parseFromFile(localProperties);
   } else {
     settings = new SettingsFile();
-    if (androidSdk == null) {
-      throwToolExit('Unable to locate Android SDK. Please run `flutter doctor` for more details.');
+    changed = true;
+  }
+
+  void changeIfNecessary(String key, String value) {
+    if (settings.values[key] != value) {
+      settings.values[key] = value;
+      changed = true;
     }
-    settings.values['sdk.dir'] = escapePath(androidSdk.directory);
-    changed = true;
-  }
-  final String escapedRoot = escapePath(Cache.flutterRoot);
-  if (changed || settings.values['flutter.sdk'] != escapedRoot) {
-    settings.values['flutter.sdk'] = escapedRoot;
-    changed = true;
-  }
-  if (buildInfo != null && settings.values['flutter.buildMode'] != buildInfo.modeName) {
-    settings.values['flutter.buildMode'] = buildInfo.modeName;
-    changed = true;
   }
 
-  FlutterManifest manifest;
-  try {
-    manifest = await FlutterManifest.createFromPath(flutterManifest);
-  } catch (error) {
-    throwToolExit('Failed to load pubspec.yaml: $error');
-  }
-
+  if (androidSdk != null)
+    changeIfNecessary('sdk.dir', escapePath(androidSdk.directory));
+  changeIfNecessary('flutter.sdk', escapePath(Cache.flutterRoot));
+  if (buildInfo != null)
+    changeIfNecessary('flutter.buildMode', buildInfo.modeName);
   final String buildName = buildInfo?.buildName ?? manifest.buildName;
-  if (buildName != null) {
-    settings.values['flutter.versionName'] = buildName;
-    changed = true;
-  }
-
+  if (buildName != null)
+    changeIfNecessary('flutter.versionName', buildName);
   final int buildNumber = buildInfo?.buildNumber ?? manifest.buildNumber;
-  if (buildNumber != null) {
-    settings.values['flutter.versionCode'] = '$buildNumber';
-    changed = true;
-  }
+  if (buildNumber != null)
+    changeIfNecessary('flutter.versionCode', '$buildNumber');
 
   if (changed)
     settings.writeContents(localProperties);
diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart
index 04e0000..3b3aefb 100644
--- a/packages/flutter_tools/lib/src/commands/create.dart
+++ b/packages/flutter_tools/lib/src/commands/create.dart
@@ -44,7 +44,7 @@
     argParser.addOption(
       'template',
       abbr: 't',
-      allowed: <String>['app', 'package', 'plugin'],
+      allowed: <String>['app', 'module', 'package', 'plugin'],
       help: 'Specify the type of project to create.',
       valueHelp: 'type',
       allowedHelp: <String, String>{
@@ -124,6 +124,7 @@
       throwToolExit('Unable to find package:flutter_driver in $flutterDriverPackagePath', exitCode: 2);
 
     final String template = argResults['template'];
+    final bool generateModule = template == 'module';
     final bool generatePlugin = template == 'plugin';
     final bool generatePackage = template == 'package';
 
@@ -172,6 +173,9 @@
       case 'app':
         generatedFileCount += await _generateApp(dirPath, templateContext);
         break;
+      case 'module':
+        generatedFileCount += await _generateModule(dirPath, templateContext);
+        break;
       case 'package':
         generatedFileCount += await _generatePackage(dirPath, templateContext);
         break;
@@ -185,6 +189,9 @@
     if (generatePackage) {
       final String relativePath = fs.path.relative(dirPath);
       printStatus('Your package code is in lib/${templateContext['projectName']}.dart in the $relativePath directory.');
+    } else if (generateModule) {
+      final String relativePath = fs.path.relative(dirPath);
+      printStatus('Your module code is in lib/main.dart in the $relativePath directory.');
     } else {
       // Run doctor; tell the user the next steps.
       final String relativeAppPath = fs.path.relative(appPath);
@@ -226,6 +233,25 @@
     }
   }
 
+  Future<int> _generateModule(String dirPath, Map<String, dynamic> templateContext) async {
+    int generatedCount = 0;
+    final String description = argResults.wasParsed('description')
+        ? argResults['description']
+        : 'A new flutter module project.';
+    templateContext['description'] = description;
+    generatedCount += _renderTemplate(fs.path.join('module', 'common'), dirPath, templateContext);
+    if (argResults['pub']) {
+      await pubGet(
+        context: PubContext.create,
+        directory: dirPath,
+        offline: argResults['offline'],
+      );
+      final FlutterProject project = new FlutterProject(fs.directory(dirPath));
+      await project.ensureReadyForPlatformSpecificTooling();
+    }
+    return generatedCount;
+  }
+
   Future<int> _generatePackage(String dirPath, Map<String, dynamic> templateContext) async {
     int generatedCount = 0;
     final String description = argResults.wasParsed('description')
diff --git a/packages/flutter_tools/lib/src/flutter_manifest.dart b/packages/flutter_tools/lib/src/flutter_manifest.dart
index c151839..80fef7a 100644
--- a/packages/flutter_tools/lib/src/flutter_manifest.dart
+++ b/packages/flutter_tools/lib/src/flutter_manifest.dart
@@ -87,6 +87,23 @@
     return _flutterDescriptor['uses-material-design'] ?? false;
   }
 
+  /// Properties defining how to expose this Flutter project as a module
+  /// for integration into an unspecified host app.
+  Map<String, dynamic> get moduleDescriptor {
+    return _flutterDescriptor.containsKey('module')
+        ? _flutterDescriptor['module'] ?? const <String, dynamic>{}
+        : null;
+  }
+
+  /// True if this manifest declares a Flutter module project.
+  ///
+  /// A Flutter project is considered a module when it has a `module:`
+  /// descriptor. A Flutter module project supports integration into an
+  /// existing host app.
+  ///
+  /// Such a project can be created using `flutter create -t module`.
+  bool get isModule => moduleDescriptor != null;
+
   List<Map<String, dynamic>> get fontsDescriptor {
    return _flutterDescriptor['fonts'] ?? const <Map<String, dynamic>>[];
   }
diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
index 4dfd917..8099b89 100644
--- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart
+++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
@@ -51,6 +51,8 @@
 ///
 /// targetOverride: Optional parameter, if null or unspecified the default value
 /// from xcode_backend.sh is used 'lib/main.dart'.
+///
+/// Returns the number of files written.
 Future<void> updateGeneratedXcodeProperties({
   @required String projectPath,
   @required BuildInfo buildInfo,
diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart
index 51a7f0d..f72463c 100644
--- a/packages/flutter_tools/lib/src/plugins.dart
+++ b/packages/flutter_tools/lib/src/plugins.dart
@@ -148,7 +148,7 @@
 
   final String pluginRegistry =
       new mustache.Template(_androidPluginRegistryTemplate).renderString(context);
-  final String javaSourcePath = fs.path.join(directory, 'android', 'app', 'src', 'main', 'java');
+  final String javaSourcePath = fs.path.join(directory, 'src', 'main', 'java');
   final Directory registryDirectory =
       fs.directory(fs.path.join(javaSourcePath, 'io', 'flutter', 'plugins'));
   registryDirectory.createSync(recursive: true);
@@ -233,9 +233,11 @@
   directory ??= fs.currentDirectory.path;
   final List<Plugin> plugins = _findPlugins(directory);
   final bool changed = _writeFlutterPluginsList(directory, plugins);
-
-  if (fs.isDirectorySync(fs.path.join(directory, 'android')))
-    _writeAndroidPluginRegistrant(directory, plugins);
+  if (fs.isDirectorySync(fs.path.join(directory, '.android', 'Flutter'))) {
+    _writeAndroidPluginRegistrant(fs.path.join(directory, '.android', 'Flutter'), plugins);
+  } else if (fs.isDirectorySync(fs.path.join(directory, 'android', 'app'))) {
+    _writeAndroidPluginRegistrant(fs.path.join(directory, 'android', 'app'), plugins);
+  }
   if (fs.isDirectorySync(fs.path.join(directory, 'ios'))) {
     _writeIOSPluginRegistrant(directory, plugins);
     final CocoaPods cocoaPods = new CocoaPods();
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
index f1d2b94..dae7772 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -5,10 +5,14 @@
 import 'dart:async';
 import 'dart:convert';
 
-import 'base/file_system.dart';
-import 'ios/xcodeproj.dart';
-import 'plugins.dart';
 
+import 'android/gradle.dart' as gradle;
+import 'base/file_system.dart';
+import 'cache.dart';
+import 'flutter_manifest.dart';
+import 'ios/xcodeproj.dart' as xcode;
+import 'plugins.dart';
+import 'template.dart';
 
 /// Represents the contents of a Flutter project at the specified [directory].
 class FlutterProject {
@@ -47,6 +51,9 @@
   /// The Android sub project of this project.
   AndroidProject get android => new AndroidProject(directory.childDirectory('android'));
 
+  /// The generated AndroidModule sub project of this module project.
+  AndroidModuleProject get androidModule => new AndroidModuleProject(directory.childDirectory('.android'));
+
   /// Returns true if this project has an example application
   bool get hasExampleApp => directory.childDirectory('example').childFile('pubspec.yaml').existsSync();
 
@@ -54,13 +61,19 @@
   FlutterProject get example => new FlutterProject(directory.childDirectory('example'));
 
   /// Generates project files necessary to make Gradle builds work on Android
-  /// and CocoaPods+Xcode work on iOS, for app projects only
+  /// and CocoaPods+Xcode work on iOS, for app and module projects only.
+  ///
+  /// Returns the number of files written.
   Future<void> ensureReadyForPlatformSpecificTooling() async {
     if (!directory.existsSync() || hasExampleApp) {
-      return;
+      return 0;
+    }
+    final FlutterManifest manifest = await FlutterManifest.createFromPath(directory.childFile('pubspec.yaml').path);
+    if (manifest.isModule) {
+      await androidModule.ensureReadyForPlatformSpecificTooling(manifest);
     }
     injectPlugins(directory: directory.path);
-    await generateXcodeProperties(directory.path);
+    await xcode.generateXcodeProperties(directory.path);
   }
 }
 
@@ -97,6 +110,36 @@
   }
 }
 
+/// Represents the contents of the .android-generated/ folder of a Flutter module
+/// project.
+class AndroidModuleProject {
+  AndroidModuleProject(this.directory);
+
+  final Directory directory;
+
+  Future<void> ensureReadyForPlatformSpecificTooling(FlutterManifest manifest) async {
+    if (_shouldRegenerate()) {
+      final Template template = new Template.fromName(fs.path.join('module', 'android'));
+      template.render(directory, <String, dynamic>{
+        'androidIdentifier': manifest.moduleDescriptor['androidPackage'],
+      }, printStatusWhenWriting: false);
+      gradle.injectGradleWrapper(directory);
+    }
+    gradle.updateLocalPropertiesSync(directory, manifest);
+  }
+
+  bool _shouldRegenerate() {
+    final File flutterToolsStamp = Cache.instance.getStampFileFor('flutter_tools');
+    final File buildDotGradleFile = directory.childFile('build.gradle');
+    if (!buildDotGradleFile.existsSync())
+      return true;
+    return flutterToolsStamp.existsSync() &&
+        flutterToolsStamp
+            .lastModifiedSync()
+            .isAfter(buildDotGradleFile.lastModifiedSync());
+  }
+}
+
 /// Asynchronously returns the first line-based match for [regExp] in [file].
 ///
 /// Assumes UTF8 encoding.
diff --git a/packages/flutter_tools/lib/src/template.dart b/packages/flutter_tools/lib/src/template.dart
index 71c47ba..3f22211 100644
--- a/packages/flutter_tools/lib/src/template.dart
+++ b/packages/flutter_tools/lib/src/template.dart
@@ -66,6 +66,7 @@
     Directory destination,
     Map<String, dynamic> context, {
     bool overwriteExisting = true,
+    bool printStatusWhenWriting = true,
   }) {
     destination.createSync(recursive: true);
     int fileCount = 0;
@@ -117,14 +118,17 @@
       if (finalDestinationFile.existsSync()) {
         if (overwriteExisting) {
           finalDestinationFile.deleteSync(recursive: true);
-          printStatus('  $relativePathForLogging (overwritten)');
+          if (printStatusWhenWriting)
+            printStatus('  $relativePathForLogging (overwritten)');
         } else {
           // The file exists but we cannot overwrite it, move on.
-          printTrace('  $relativePathForLogging (existing - skipped)');
+          if (printStatusWhenWriting)
+            printTrace('  $relativePathForLogging (existing - skipped)');
           return;
         }
       } else {
-        printStatus('  $relativePathForLogging (created)');
+        if (printStatusWhenWriting)
+          printStatus('  $relativePathForLogging (created)');
       }
 
       fileCount++;
diff --git a/packages/flutter_tools/schema/pubspec_yaml.json b/packages/flutter_tools/schema/pubspec_yaml.json
index 0045bf4..deafe91 100644
--- a/packages/flutter_tools/schema/pubspec_yaml.json
+++ b/packages/flutter_tools/schema/pubspec_yaml.json
@@ -43,6 +43,13 @@
                         }
                     }
                 },
+                "module": {
+                    "type": "object",
+                    "additionalProperties": false,
+                    "properties": {
+                        "androidPackage": { "type": "string" }
+                    }
+                },
                 "plugin": {
                     "type": "object",
                     "additionalProperties": false,
diff --git a/packages/flutter_tools/templates/module/android/Flutter.tmpl/build.gradle.tmpl b/packages/flutter_tools/templates/module/android/Flutter.tmpl/build.gradle.tmpl
new file mode 100644
index 0000000..c156a87
--- /dev/null
+++ b/packages/flutter_tools/templates/module/android/Flutter.tmpl/build.gradle.tmpl
@@ -0,0 +1,48 @@
+// Generated file. Do not edit.
+
+def localProperties = new Properties()
+def localPropertiesFile = new File(buildscript.sourceFile.parentFile.parentFile, '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) {
+    throw new GradleException("versionCode not found. Define flutter.versionCode in the local.properties file.")
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+    throw new GradleException("versionName not found. Define flutter.versionName in the local.properties file.")
+}
+
+apply plugin: 'com.android.library'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+    compileSdkVersion 27
+
+    defaultConfig {
+        minSdkVersion 16
+        targetSdkVersion 27
+        versionCode flutterVersionCode.toInteger()
+        versionName flutterVersionName
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+    }
+}
+
+flutter {
+    source '../..'
+}
+
+dependencies {
+    testImplementation 'junit:junit:4.12'
+    implementation 'com.android.support:support-v13:27.1.1'
+}
diff --git a/packages/flutter_tools/templates/module/android/Flutter.tmpl/src/main/AndroidManifest.xml.tmpl b/packages/flutter_tools/templates/module/android/Flutter.tmpl/src/main/AndroidManifest.xml.tmpl
new file mode 100644
index 0000000..7466643
--- /dev/null
+++ b/packages/flutter_tools/templates/module/android/Flutter.tmpl/src/main/AndroidManifest.xml.tmpl
@@ -0,0 +1,5 @@
+<!-- Generated file. Do not edit. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="{{androidIdentifier}}">
+  <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>
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/Flutter.tmpl/src/main/java/io/flutter/facade/Flutter.java
new file mode 100644
index 0000000..a52fd9f
--- /dev/null
+++ b/packages/flutter_tools/templates/module/android/Flutter.tmpl/src/main/java/io/flutter/facade/Flutter.java
@@ -0,0 +1,98 @@
+package io.flutter.facade;
+
+import android.app.Activity;
+import android.arch.lifecycle.Lifecycle;
+import android.arch.lifecycle.LifecycleObserver;
+import android.arch.lifecycle.OnLifecycleEvent;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.View;
+
+import io.flutter.app.FlutterActivityDelegate;
+import io.flutter.view.FlutterMain;
+import io.flutter.view.FlutterNativeView;
+import io.flutter.view.FlutterView;
+import io.flutter.plugins.GeneratedPluginRegistrant;
+
+/**
+ * Main entry point for using Flutter in Android applications.
+ *
+ * <p><strong>Warning:</strong> This file is auto-generated by Flutter tooling. Do not edit.
+ * It may be moved into flutter.jar or another library dependency of the Flutter module project
+ * at a later point.</p>
+ */
+public final class Flutter {
+  private Flutter() {
+    // to prevent instantiation
+  }
+
+  public static void startInitialization(Context applicationContext) {
+    FlutterMain.startInitialization(applicationContext, null);
+  }
+
+  public static Fragment createFragment(String route) {
+    final FlutterFragment fragment = new FlutterFragment();
+    final Bundle args = new Bundle();
+    args.putString(FlutterFragment.ARG_ROUTE, route);
+    fragment.setArguments(args);
+    return fragment;
+  }
+
+  public static View createView(final Activity activity, final Lifecycle lifecycle, final String route) {
+    FlutterMain.startInitialization(activity.getApplicationContext());
+    FlutterMain.ensureInitializationComplete(activity.getApplicationContext(), null);
+    final FlutterActivityDelegate delegate = new FlutterActivityDelegate(activity, new FlutterActivityDelegate.ViewFactory() {
+      @Override
+      public FlutterView createFlutterView(Context context) {
+        final FlutterNativeView nativeView = new FlutterNativeView(context);
+        final FlutterView flutterView = new FlutterView(activity, null, nativeView);
+        flutterView.setInitialRoute(route);
+        return flutterView;
+      }
+
+      @Override
+      public boolean retainFlutterNativeView() {
+        return false;
+      }
+
+      @Override
+      public FlutterNativeView createFlutterNativeView() {
+        throw new UnsupportedOperationException();
+      }
+    });
+    lifecycle.addObserver(new LifecycleObserver() {
+      @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
+      public void onCreate() {
+        delegate.onCreate(null);
+        GeneratedPluginRegistrant.registerWith(delegate);
+      }
+
+      @OnLifecycleEvent(Lifecycle.Event.ON_START)
+      public void onStart() {
+        delegate.onStart();
+      }
+
+      @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
+      public void onResume() {
+        delegate.onResume();
+      }
+
+      @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
+      public void onPause() {
+        delegate.onPause();
+      }
+
+      @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
+      public void onStop() {
+        delegate.onStop();
+      }
+
+      @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
+      public void onDestroy() {
+        delegate.onDestroy();
+      }
+    });
+    return delegate.getFlutterView();
+  }
+}
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/Flutter.tmpl/src/main/java/io/flutter/facade/FlutterFragment.java
new file mode 100644
index 0000000..1b082de
--- /dev/null
+++ b/packages/flutter_tools/templates/module/android/Flutter.tmpl/src/main/java/io/flutter/facade/FlutterFragment.java
@@ -0,0 +1,40 @@
+package io.flutter.facade;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A {@link Fragment} managing a Flutter view.
+ *
+ * <p><strong>Warning:</strong> This file is auto-generated by Flutter tooling. Do not edit.
+ * It may be moved into flutter.jar or another library dependency of the Flutter module project
+ * at a later point.</p>
+ */
+public class FlutterFragment extends Fragment {
+  public static final String ARG_ROUTE = "route";
+  private String mRoute = "/";
+
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    if (getArguments() != null) {
+      mRoute = getArguments().getString(ARG_ROUTE);
+    }
+  }
+
+  @Override
+  public void onInflate(Context context, AttributeSet attrs, Bundle savedInstanceState) {
+    super.onInflate(context, attrs, savedInstanceState);
+  }
+
+  @Override
+  public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                           Bundle savedInstanceState) {
+    return Flutter.createView(getActivity(), getLifecycle(), mRoute);
+  }
+}
diff --git a/packages/flutter_tools/templates/module/android/build.gradle.tmpl b/packages/flutter_tools/templates/module/android/build.gradle.tmpl
new file mode 100644
index 0000000..fdb7698
--- /dev/null
+++ b/packages/flutter_tools/templates/module/android/build.gradle.tmpl
@@ -0,0 +1,31 @@
+// Generated file. Do not edit.
+
+buildscript {
+    repositories {
+        google()
+        jcenter()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.1.3'
+    }
+}
+
+allprojects {
+    repositories {
+        google()
+        jcenter()
+    }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+    project.buildDir = "${rootProject.buildDir}/android_gen"
+}
+subprojects {
+    project.evaluationDependsOn(':flutter')
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}
diff --git a/packages/flutter_tools/templates/module/android/gradle.properties.tmpl b/packages/flutter_tools/templates/module/android/gradle.properties.tmpl
new file mode 100644
index 0000000..8bd86f6
--- /dev/null
+++ b/packages/flutter_tools/templates/module/android/gradle.properties.tmpl
@@ -0,0 +1 @@
+org.gradle.jvmargs=-Xmx1536M
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
new file mode 100644
index 0000000..9372d0f
--- /dev/null
+++ b/packages/flutter_tools/templates/module/android/gradle.tmpl/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#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/include_flutter.groovy.tmpl b/packages/flutter_tools/templates/module/android/include_flutter.groovy.tmpl
new file mode 100644
index 0000000..e3a0dea
--- /dev/null
+++ b/packages/flutter_tools/templates/module/android/include_flutter.groovy.tmpl
@@ -0,0 +1,19 @@
+// Generated file. Do not edit.
+
+def scriptFile = getClass().protectionDomain.codeSource.location.path
+def flutterProjectRoot = new File(scriptFile).parentFile.parentFile
+
+gradle.include ':flutter'
+gradle.project(':flutter').projectDir = new File(flutterProjectRoot, '.android/Flutter')
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot, '.flutter-plugins')
+if (pluginsFile.exists()) {
+    pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
+}
+
+plugins.each { name, path ->
+    def pluginDirectory = flutterProjectRoot.toPath().resolve(path).resolve('android').toFile()
+    gradle.include ":$name"
+    gradle.project(":$name").projectDir = pluginDirectory
+}
diff --git a/packages/flutter_tools/templates/module/android/settings.gradle.tmpl b/packages/flutter_tools/templates/module/android/settings.gradle.tmpl
new file mode 100644
index 0000000..49eb997
--- /dev/null
+++ b/packages/flutter_tools/templates/module/android/settings.gradle.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/templates/module/common/.gitignore.tmpl b/packages/flutter_tools/templates/module/common/.gitignore.tmpl
new file mode 100644
index 0000000..c15d674
--- /dev/null
+++ b/packages/flutter_tools/templates/module/common/.gitignore.tmpl
@@ -0,0 +1,40 @@
+.DS_Store
+.dart_tool/
+
+.packages
+.pub/
+
+.idea/
+.vagrant/
+.sconsign.dblite
+.svn/
+
+*.swp
+profile
+
+DerivedData/
+
+.generated/
+
+*.pbxuser
+*.mode1v3
+*.mode2v3
+*.perspectivev3
+
+!default.pbxuser
+!default.mode1v3
+!default.mode2v3
+!default.perspectivev3
+
+xcuserdata
+
+*.moved-aside
+
+*.pyc
+*sync/
+Icon?
+.tags*
+
+build/
+android_gen/
+.flutter-plugins
diff --git a/packages/flutter_tools/templates/module/common/.metadata.tmpl b/packages/flutter_tools/templates/module/common/.metadata.tmpl
new file mode 100644
index 0000000..accfecc
--- /dev/null
+++ b/packages/flutter_tools/templates/module/common/.metadata.tmpl
@@ -0,0 +1,8 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: {{flutterRevision}}
+  channel: {{flutterChannel}}
diff --git a/packages/flutter_tools/templates/module/common/README.md.tmpl b/packages/flutter_tools/templates/module/common/README.md.tmpl
new file mode 100644
index 0000000..f140e12
--- /dev/null
+++ b/packages/flutter_tools/templates/module/common/README.md.tmpl
@@ -0,0 +1,8 @@
+# {{projectName}}
+
+{{description}}
+
+## Getting Started
+
+For help getting started with Flutter, view our online
+[documentation](https://flutter.io/).
diff --git a/packages/flutter_tools/templates/module/common/lib/main.dart.tmpl b/packages/flutter_tools/templates/module/common/lib/main.dart.tmpl
new file mode 100644
index 0000000..1b325f6
--- /dev/null
+++ b/packages/flutter_tools/templates/module/common/lib/main.dart.tmpl
@@ -0,0 +1,38 @@
+import 'dart:async';
+import 'dart:ui';
+
+import 'package:flutter/material.dart';
+import 'package:package_info/package_info.dart';
+
+Future<void> main() async {
+  final PackageInfo packageInfo = await PackageInfo.fromPlatform();
+  runApp(Directionality(textDirection: TextDirection.ltr, child: Router(packageInfo)));
+}
+
+class Router extends StatelessWidget {
+  Router(this.packageInfo);
+
+  final PackageInfo packageInfo;
+
+  @override
+  Widget build(BuildContext context) {
+    final String route = window.defaultRouteName;
+    switch (route) {
+      case 'route1':
+        return Container(
+          child: Center(child: Text('Route 1\n${packageInfo.appName}')),
+          color: Colors.green,
+        );
+      case 'route2':
+        return Container(
+          child: Center(child: Text('Route 2\n${packageInfo.packageName}')),
+          color: Colors.blue,
+        );
+      default:
+        return Container(
+          child: Center(child: Text('Unknown route: $route')),
+          color: Colors.red,
+        );
+    }
+  }
+}
diff --git a/packages/flutter_tools/templates/module/common/pubspec.yaml.tmpl b/packages/flutter_tools/templates/module/common/pubspec.yaml.tmpl
new file mode 100644
index 0000000..dc6bb57
--- /dev/null
+++ b/packages/flutter_tools/templates/module/common/pubspec.yaml.tmpl
@@ -0,0 +1,17 @@
+name: {{projectName}}
+description: {{description}}
+version: 1.0.0+1
+
+dependencies:
+  package_info:
+  flutter:
+    sdk: flutter
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+
+flutter:
+  uses-material-design: true
+  module:
+    androidPackage: {{androidIdentifier}}
diff --git a/packages/flutter_tools/test/commands/create_test.dart b/packages/flutter_tools/test/commands/create_test.dart
index 4666080..ad27491 100644
--- a/packages/flutter_tools/test/commands/create_test.dart
+++ b/packages/flutter_tools/test/commands/create_test.dart
@@ -186,6 +186,59 @@
       );
     }, timeout: allowForRemotePubInvocation);
 
+    testUsingContext('module', () async {
+      return _createProject(
+        projectDir,
+        <String>['--no-pub', '--template=module'],
+        <String>[
+          '.gitignore',
+          '.metadata',
+          'lib/main.dart',
+          'pubspec.yaml',
+          'README.md',
+        ],
+        unexpectedPaths: <String>[
+          '.android/',
+          'android/',
+          'ios/',
+        ]
+      );
+    }, timeout: allowForCreateFlutterProject);
+
+    testUsingContext('module with pub', () async {
+      return _createProject(
+          projectDir,
+          <String>['-t', 'module'],
+          <String>[
+            '.gitignore',
+            '.metadata',
+            'lib/main.dart',
+            'pubspec.lock',
+            'pubspec.yaml',
+            'README.md',
+            '.packages',
+            '.android/build.gradle',
+            '.android/Flutter/build.gradle',
+            '.android/Flutter/src/main/java/io/flutter/facade/Flutter.java',
+            '.android/Flutter/src/main/java/io/flutter/facade/FlutterFragment.java',
+            '.android/Flutter/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java',
+            '.android/Flutter/src/main/AndroidManifest.xml',
+            '.android/gradle.properties',
+            '.android/gradle/wrapper/gradle-wrapper.jar',
+            '.android/gradle/wrapper/gradle-wrapper.properties',
+            '.android/gradlew',
+            '.android/gradlew.bat',
+            '.android/local.properties',
+            '.android/include_flutter.groovy',
+            '.android/settings.gradle',
+          ],
+          unexpectedPaths: <String>[
+            'android/',
+            'ios/',
+          ]
+      );
+    }, timeout: allowForRemotePubInvocation);
+
     // Verify content and formatting
     testUsingContext('content', () async {
       Cache.flutterRoot = '../..';
@@ -423,11 +476,16 @@
   args.add(dir.path);
   await runner.run(args);
 
+  bool pathExists(String path) {
+    final String fullPath = fs.path.join(dir.path, path);
+    return fs.typeSync(fullPath) != FileSystemEntityType.notFound;
+  }
+
   for (String path in expectedPaths) {
-    expect(fs.file(fs.path.join(dir.path, path)).existsSync(), true, reason: '$path does not exist');
+    expect(pathExists(path), true, reason: '$path does not exist');
   }
   for (String path in unexpectedPaths) {
-    expect(fs.file(fs.path.join(dir.path, path)).existsSync(), false, reason: '$path exists');
+    expect(pathExists(path), false, reason: '$path exists');
   }
 }