Materialize Flutter module, Android (#20520)
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.';