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++;