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