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