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');
}
}