Add module template for Android (#18697)
diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index 1cefa1f..833ae58 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -141,16 +141,13 @@ } } -String _locateProjectGradlew({ bool ensureExecutable = true }) { - final String path = fs.path.join( - 'android', +String _locateGradlewExecutable(Directory directory) { + final File gradle = directory.childFile( platform.isWindows ? 'gradlew.bat' : 'gradlew', ); - if (fs.isFileSync(path)) { - final File gradle = fs.file(path); - if (ensureExecutable) - os.makeExecutable(gradle); + if (gradle.existsSync()) { + os.makeExecutable(gradle); return gradle.absolute.path; } else { return null; @@ -165,11 +162,12 @@ // Note: Gradle may be bootstrapped and possibly downloaded as a side-effect // of validating the Gradle executable. This may take several seconds. Future<String> _initializeGradle() async { + final Directory android = fs.directory('android'); final Status status = logger.startProgress('Initializing gradle...', expectSlowOperation: true); - String gradle = _locateProjectGradlew(); + String gradle = _locateGradlewExecutable(android); if (gradle == null) { - _injectGradleWrapper(); - gradle = _locateProjectGradlew(); + injectGradleWrapper(android); + gradle = _locateGradlewExecutable(android); } if (gradle == null) throwToolExit('Unable to locate gradlew script'); @@ -181,11 +179,13 @@ return gradle; } -void _injectGradleWrapper() { - copyDirectorySync(cache.getArtifactDirectory('gradle_wrapper'), fs.directory('android')); - final String propertiesPath = fs.path.join('android', 'gradle', 'wrapper', 'gradle-wrapper.properties'); - if (!fs.file(propertiesPath).existsSync()) { - fs.file(propertiesPath).writeAsStringSync(''' +/// Injects the Gradle wrapper into the specified directory. +void injectGradleWrapper(Directory directory) { + copyDirectorySync(cache.getArtifactDirectory('gradle_wrapper'), directory); + _locateGradlewExecutable(directory); + final File propertiesFile = directory.childFile(fs.path.join('gradle', 'wrapper', 'gradle-wrapper.properties')); + if (!propertiesFile.existsSync()) { + propertiesFile.writeAsStringSync(''' distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME @@ -196,14 +196,31 @@ } } -/// Create android/local.properties if needed, and update Flutter settings. +/// Overwrite android/local.properties in the specified Flutter project, if needed. +/// +/// Throws, if `pubspec.yaml` or Android SDK cannot be located. Future<void> updateLocalProperties({String projectPath, BuildInfo buildInfo}) async { - final File localProperties = (projectPath == null) - ? fs.file(fs.path.join('android', 'local.properties')) - : fs.file(fs.path.join(projectPath, 'android', 'local.properties')); + final Directory android = (projectPath == null) + ? fs.directory('android') + : fs.directory(fs.path.join(projectPath, 'android')); final String flutterManifest = (projectPath == null) ? fs.path.join(bundle.defaultManifestPath) : fs.path.join(projectPath, bundle.defaultManifestPath); + if (androidSdk == null) { + throwToolExit('Unable to locate Android SDK. Please run `flutter doctor` for more details.'); + } + FlutterManifest manifest; + try { + manifest = await FlutterManifest.createFromPath(flutterManifest); + } catch (error) { + throwToolExit('Failed to load pubspec.yaml: $error'); + } + updateLocalPropertiesSync(android, manifest, buildInfo); +} + +/// Overwrite local.properties in the specified directory, if needed. +void updateLocalPropertiesSync(Directory android, FlutterManifest manifest, [BuildInfo buildInfo]) { + final File localProperties = android.childFile('local.properties'); bool changed = false; SettingsFile settings; @@ -211,40 +228,27 @@ settings = new SettingsFile.parseFromFile(localProperties); } else { settings = new SettingsFile(); - if (androidSdk == null) { - throwToolExit('Unable to locate Android SDK. Please run `flutter doctor` for more details.'); + changed = true; + } + + void changeIfNecessary(String key, String value) { + if (settings.values[key] != value) { + settings.values[key] = value; + changed = true; } - settings.values['sdk.dir'] = escapePath(androidSdk.directory); - changed = true; - } - final String escapedRoot = escapePath(Cache.flutterRoot); - if (changed || settings.values['flutter.sdk'] != escapedRoot) { - settings.values['flutter.sdk'] = escapedRoot; - changed = true; - } - if (buildInfo != null && settings.values['flutter.buildMode'] != buildInfo.modeName) { - settings.values['flutter.buildMode'] = buildInfo.modeName; - changed = true; } - FlutterManifest manifest; - try { - manifest = await FlutterManifest.createFromPath(flutterManifest); - } catch (error) { - throwToolExit('Failed to load pubspec.yaml: $error'); - } - + if (androidSdk != null) + changeIfNecessary('sdk.dir', escapePath(androidSdk.directory)); + changeIfNecessary('flutter.sdk', escapePath(Cache.flutterRoot)); + if (buildInfo != null) + changeIfNecessary('flutter.buildMode', buildInfo.modeName); final String buildName = buildInfo?.buildName ?? manifest.buildName; - if (buildName != null) { - settings.values['flutter.versionName'] = buildName; - changed = true; - } - + if (buildName != null) + changeIfNecessary('flutter.versionName', buildName); final int buildNumber = buildInfo?.buildNumber ?? manifest.buildNumber; - if (buildNumber != null) { - settings.values['flutter.versionCode'] = '$buildNumber'; - changed = true; - } + if (buildNumber != null) + changeIfNecessary('flutter.versionCode', '$buildNumber'); if (changed) settings.writeContents(localProperties);
diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart index 04e0000..3b3aefb 100644 --- a/packages/flutter_tools/lib/src/commands/create.dart +++ b/packages/flutter_tools/lib/src/commands/create.dart
@@ -44,7 +44,7 @@ argParser.addOption( 'template', abbr: 't', - allowed: <String>['app', 'package', 'plugin'], + allowed: <String>['app', 'module', 'package', 'plugin'], help: 'Specify the type of project to create.', valueHelp: 'type', allowedHelp: <String, String>{ @@ -124,6 +124,7 @@ throwToolExit('Unable to find package:flutter_driver in $flutterDriverPackagePath', exitCode: 2); final String template = argResults['template']; + final bool generateModule = template == 'module'; final bool generatePlugin = template == 'plugin'; final bool generatePackage = template == 'package'; @@ -172,6 +173,9 @@ case 'app': generatedFileCount += await _generateApp(dirPath, templateContext); break; + case 'module': + generatedFileCount += await _generateModule(dirPath, templateContext); + break; case 'package': generatedFileCount += await _generatePackage(dirPath, templateContext); break; @@ -185,6 +189,9 @@ if (generatePackage) { final String relativePath = fs.path.relative(dirPath); printStatus('Your package code is in lib/${templateContext['projectName']}.dart in the $relativePath directory.'); + } else if (generateModule) { + final String relativePath = fs.path.relative(dirPath); + printStatus('Your module code is in lib/main.dart in the $relativePath directory.'); } else { // Run doctor; tell the user the next steps. final String relativeAppPath = fs.path.relative(appPath); @@ -226,6 +233,25 @@ } } + Future<int> _generateModule(String dirPath, Map<String, dynamic> templateContext) async { + int generatedCount = 0; + final String description = argResults.wasParsed('description') + ? argResults['description'] + : 'A new flutter module project.'; + templateContext['description'] = description; + generatedCount += _renderTemplate(fs.path.join('module', 'common'), dirPath, templateContext); + if (argResults['pub']) { + await pubGet( + context: PubContext.create, + directory: dirPath, + offline: argResults['offline'], + ); + final FlutterProject project = new FlutterProject(fs.directory(dirPath)); + await project.ensureReadyForPlatformSpecificTooling(); + } + return generatedCount; + } + Future<int> _generatePackage(String dirPath, Map<String, dynamic> templateContext) async { int generatedCount = 0; final String description = argResults.wasParsed('description')
diff --git a/packages/flutter_tools/lib/src/flutter_manifest.dart b/packages/flutter_tools/lib/src/flutter_manifest.dart index c151839..80fef7a 100644 --- a/packages/flutter_tools/lib/src/flutter_manifest.dart +++ b/packages/flutter_tools/lib/src/flutter_manifest.dart
@@ -87,6 +87,23 @@ return _flutterDescriptor['uses-material-design'] ?? false; } + /// Properties defining how to expose this Flutter project as a module + /// for integration into an unspecified host app. + Map<String, dynamic> get moduleDescriptor { + return _flutterDescriptor.containsKey('module') + ? _flutterDescriptor['module'] ?? const <String, dynamic>{} + : null; + } + + /// True if this manifest declares a Flutter module project. + /// + /// A Flutter project is considered a module when it has a `module:` + /// descriptor. A Flutter module project supports integration into an + /// existing host app. + /// + /// Such a project can be created using `flutter create -t module`. + bool get isModule => moduleDescriptor != null; + List<Map<String, dynamic>> get fontsDescriptor { return _flutterDescriptor['fonts'] ?? const <Map<String, dynamic>>[]; }
diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart index 4dfd917..8099b89 100644 --- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart +++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
@@ -51,6 +51,8 @@ /// /// targetOverride: Optional parameter, if null or unspecified the default value /// from xcode_backend.sh is used 'lib/main.dart'. +/// +/// Returns the number of files written. Future<void> updateGeneratedXcodeProperties({ @required String projectPath, @required BuildInfo buildInfo,
diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart index 51a7f0d..f72463c 100644 --- a/packages/flutter_tools/lib/src/plugins.dart +++ b/packages/flutter_tools/lib/src/plugins.dart
@@ -148,7 +148,7 @@ final String pluginRegistry = new mustache.Template(_androidPluginRegistryTemplate).renderString(context); - final String javaSourcePath = fs.path.join(directory, 'android', 'app', 'src', 'main', 'java'); + final String javaSourcePath = fs.path.join(directory, 'src', 'main', 'java'); final Directory registryDirectory = fs.directory(fs.path.join(javaSourcePath, 'io', 'flutter', 'plugins')); registryDirectory.createSync(recursive: true); @@ -233,9 +233,11 @@ directory ??= fs.currentDirectory.path; final List<Plugin> plugins = _findPlugins(directory); final bool changed = _writeFlutterPluginsList(directory, plugins); - - if (fs.isDirectorySync(fs.path.join(directory, 'android'))) - _writeAndroidPluginRegistrant(directory, plugins); + if (fs.isDirectorySync(fs.path.join(directory, '.android', 'Flutter'))) { + _writeAndroidPluginRegistrant(fs.path.join(directory, '.android', 'Flutter'), plugins); + } else if (fs.isDirectorySync(fs.path.join(directory, 'android', 'app'))) { + _writeAndroidPluginRegistrant(fs.path.join(directory, 'android', 'app'), plugins); + } if (fs.isDirectorySync(fs.path.join(directory, 'ios'))) { _writeIOSPluginRegistrant(directory, plugins); final CocoaPods cocoaPods = new CocoaPods();
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index f1d2b94..dae7772 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart
@@ -5,10 +5,14 @@ import 'dart:async'; import 'dart:convert'; -import 'base/file_system.dart'; -import 'ios/xcodeproj.dart'; -import 'plugins.dart'; +import 'android/gradle.dart' as gradle; +import 'base/file_system.dart'; +import 'cache.dart'; +import 'flutter_manifest.dart'; +import 'ios/xcodeproj.dart' as xcode; +import 'plugins.dart'; +import 'template.dart'; /// Represents the contents of a Flutter project at the specified [directory]. class FlutterProject { @@ -47,6 +51,9 @@ /// The Android sub project of this project. AndroidProject get android => new AndroidProject(directory.childDirectory('android')); + /// The generated AndroidModule sub project of this module project. + AndroidModuleProject get androidModule => new AndroidModuleProject(directory.childDirectory('.android')); + /// Returns true if this project has an example application bool get hasExampleApp => directory.childDirectory('example').childFile('pubspec.yaml').existsSync(); @@ -54,13 +61,19 @@ FlutterProject get example => new FlutterProject(directory.childDirectory('example')); /// Generates project files necessary to make Gradle builds work on Android - /// and CocoaPods+Xcode work on iOS, for app projects only + /// and CocoaPods+Xcode work on iOS, for app and module projects only. + /// + /// Returns the number of files written. Future<void> ensureReadyForPlatformSpecificTooling() async { if (!directory.existsSync() || hasExampleApp) { - return; + return 0; + } + final FlutterManifest manifest = await FlutterManifest.createFromPath(directory.childFile('pubspec.yaml').path); + if (manifest.isModule) { + await androidModule.ensureReadyForPlatformSpecificTooling(manifest); } injectPlugins(directory: directory.path); - await generateXcodeProperties(directory.path); + await xcode.generateXcodeProperties(directory.path); } } @@ -97,6 +110,36 @@ } } +/// Represents the contents of the .android-generated/ folder of a Flutter module +/// project. +class AndroidModuleProject { + AndroidModuleProject(this.directory); + + final Directory directory; + + Future<void> ensureReadyForPlatformSpecificTooling(FlutterManifest manifest) async { + if (_shouldRegenerate()) { + final Template template = new Template.fromName(fs.path.join('module', 'android')); + template.render(directory, <String, dynamic>{ + 'androidIdentifier': manifest.moduleDescriptor['androidPackage'], + }, printStatusWhenWriting: false); + gradle.injectGradleWrapper(directory); + } + gradle.updateLocalPropertiesSync(directory, manifest); + } + + bool _shouldRegenerate() { + final File flutterToolsStamp = Cache.instance.getStampFileFor('flutter_tools'); + final File buildDotGradleFile = directory.childFile('build.gradle'); + if (!buildDotGradleFile.existsSync()) + return true; + return flutterToolsStamp.existsSync() && + flutterToolsStamp + .lastModifiedSync() + .isAfter(buildDotGradleFile.lastModifiedSync()); + } +} + /// Asynchronously returns the first line-based match for [regExp] in [file]. /// /// Assumes UTF8 encoding.
diff --git a/packages/flutter_tools/lib/src/template.dart b/packages/flutter_tools/lib/src/template.dart index 71c47ba..3f22211 100644 --- a/packages/flutter_tools/lib/src/template.dart +++ b/packages/flutter_tools/lib/src/template.dart
@@ -66,6 +66,7 @@ Directory destination, Map<String, dynamic> context, { bool overwriteExisting = true, + bool printStatusWhenWriting = true, }) { destination.createSync(recursive: true); int fileCount = 0; @@ -117,14 +118,17 @@ if (finalDestinationFile.existsSync()) { if (overwriteExisting) { finalDestinationFile.deleteSync(recursive: true); - printStatus(' $relativePathForLogging (overwritten)'); + if (printStatusWhenWriting) + printStatus(' $relativePathForLogging (overwritten)'); } else { // The file exists but we cannot overwrite it, move on. - printTrace(' $relativePathForLogging (existing - skipped)'); + if (printStatusWhenWriting) + printTrace(' $relativePathForLogging (existing - skipped)'); return; } } else { - printStatus(' $relativePathForLogging (created)'); + if (printStatusWhenWriting) + printStatus(' $relativePathForLogging (created)'); } fileCount++;