FlutterProject refactoring and test coverage (#20296)
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
index 196924c..b10192c 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -5,8 +5,11 @@
import 'dart:async';
import 'dart:convert';
+import 'package:meta/meta.dart';
+
import 'android/gradle.dart' as gradle;
import 'base/file_system.dart';
+import 'build_info.dart';
import 'bundle.dart' as bundle;
import 'cache.dart';
import 'flutter_manifest.dart';
@@ -16,21 +19,27 @@
/// Represents the contents of a Flutter project at the specified [directory].
///
-/// Instances should be treated as immutable snapshots, to be replaced by new
-/// instances on changes to `pubspec.yaml` files.
+/// [FlutterManifest] information is read from `pubspec.yaml` and
+/// `example/pubspec.yaml` files on construction of a [FlutterProject] instance.
+/// The constructed instance carries an immutable snapshot representation of the
+/// presence and content of those files. Accordingly, [FlutterProject] instances
+/// should be discarded upon changes to the `pubspec.yaml` files, but can be
+/// used across changes to other files, as no other file-level information is
+/// cached.
class FlutterProject {
- FlutterProject._(this.directory, this.manifest, this._exampleManifest);
+ @visibleForTesting
+ FlutterProject(this.directory, this.manifest, this._exampleManifest);
/// Returns a future that completes with a FlutterProject view of the given directory.
static Future<FlutterProject> fromDirectory(Directory directory) async {
final FlutterManifest manifest = await FlutterManifest.createFromPath(
directory.childFile(bundle.defaultManifestPath).path,
);
- final Directory exampleDirectory = directory.childDirectory('example');
+ final Directory exampleDirectory = _exampleDirectory(directory);
final FlutterManifest exampleManifest = await FlutterManifest.createFromPath(
exampleDirectory.childFile(bundle.defaultManifestPath).path,
);
- return new FlutterProject._(directory, manifest, exampleManifest);
+ return new FlutterProject(directory, manifest, exampleManifest);
}
/// Returns a future that completes with a FlutterProject view of the current directory.
@@ -73,83 +82,56 @@
}
/// The iOS sub project of this project.
- IosProject get ios => new IosProject(directory.childDirectory('ios'));
+ IosProject get ios => new IosProject._(this);
/// The Android sub project of this project.
- AndroidProject get android {
- if (manifest.isModule) {
- return new AndroidProject(directory.childDirectory('.android'));
- }
- return new AndroidProject(directory.childDirectory('android'));
- }
-
- /// The generated AndroidModule sub project of this module project.
- AndroidModuleProject get androidModule => new AndroidModuleProject(directory.childDirectory('.android'));
-
- /// The generated IosModule sub project of this module project.
- IosModuleProject get iosModule => new IosModuleProject(directory.childDirectory('.ios'));
-
- File get androidLocalPropertiesFile {
- return directory
- .childDirectory(manifest.isModule ? '.android' : 'android')
- .childFile('local.properties');
- }
-
- File get generatedXcodePropertiesFile {
- return directory
- .childDirectory(manifest.isModule ? '.ios' : 'ios')
- .childDirectory('Flutter')
- .childFile('Generated.xcconfig');
- }
+ AndroidProject get android => new AndroidProject._(this);
File get flutterPluginsFile => directory.childFile('.flutter-plugins');
- Directory get androidPluginRegistrantHost {
- return manifest.isModule
- ? directory.childDirectory('.android').childDirectory('Flutter')
- : directory.childDirectory('android').childDirectory('app');
- }
-
- Directory get iosPluginRegistrantHost {
- // In a module create the GeneratedPluginRegistrant as a pod to be included
- // from a hosting app.
- // For a non-module create the GeneratedPluginRegistrant as source files
- // directly in the iOS project.
- return manifest.isModule
- ? directory.childDirectory('.ios').childDirectory('Flutter').childDirectory('FlutterPluginRegistrant')
- : directory.childDirectory('ios').childDirectory('Runner');
- }
-
/// The example sub-project of this project.
- FlutterProject get example => new FlutterProject._(_exampleDirectory, _exampleManifest, FlutterManifest.empty());
+ FlutterProject get example => new FlutterProject(
+ _exampleDirectory(directory),
+ _exampleManifest,
+ FlutterManifest.empty(),
+ );
- /// True, if this project has an example application
- bool get hasExampleApp => _exampleDirectory.childFile('pubspec.yaml').existsSync();
+ bool get isModule => manifest != null && manifest.isModule;
+
+ /// True, if this project has an example application.
+ bool get hasExampleApp => _exampleDirectory(directory).existsSync();
/// The directory that will contain the example if an example exists.
- Directory get _exampleDirectory => directory.childDirectory('example');
+ static Directory _exampleDirectory(Directory directory) => directory.childDirectory('example');
/// Generates project files necessary to make Gradle builds work on Android
/// and CocoaPods+Xcode work on iOS, for app and module projects only.
Future<void> ensureReadyForPlatformSpecificTooling() async {
- if (!directory.existsSync() || hasExampleApp) {
+ if (!directory.existsSync() || hasExampleApp)
return;
- }
- if (manifest.isModule) {
- await androidModule.ensureReadyForPlatformSpecificTooling(this);
- await iosModule.ensureReadyForPlatformSpecificTooling();
- }
- await xcode.generateXcodeProperties(project: this);
+ await android.ensureReadyForPlatformSpecificTooling();
+ await ios.ensureReadyForPlatformSpecificTooling();
await injectPlugins(this);
}
}
-/// Represents the contents of the ios/ folder of a Flutter project.
+/// Represents the iOS sub-project of a Flutter project.
+///
+/// Instances will reflect the contents of the `ios/` sub-folder of
+/// Flutter applications and the `.ios/` sub-folder of Flutter modules.
class IosProject {
static final RegExp _productBundleIdPattern = new RegExp(r'^\s*PRODUCT_BUNDLE_IDENTIFIER\s*=\s*(.*);\s*$');
- IosProject(this.directory);
- final Directory directory;
+ IosProject._(this.parent);
+
+ /// The parent of this project.
+ final FlutterProject parent;
+
+ /// The directory of this project.
+ Directory get directory => parent.directory.childDirectory(isModule ? '.ios' : 'ios');
+
+ /// True, if the parent Flutter project is a module.
+ bool get isModule => parent.isModule;
/// The xcode config file for [mode].
File xcodeConfigFor(String mode) => directory.childDirectory('Flutter').childFile('$mode.xcconfig');
@@ -167,33 +149,55 @@
final File projectFile = directory.childDirectory('Runner.xcodeproj').childFile('project.pbxproj');
return _firstMatchInFile(projectFile, _productBundleIdPattern).then((Match match) => match?.group(1));
}
-}
-
-/// Represents the contents of the .ios/ folder of a Flutter module
-/// project.
-class IosModuleProject {
- IosModuleProject(this.directory);
-
- final Directory directory;
Future<void> ensureReadyForPlatformSpecificTooling() async {
- if (_shouldRegenerate()) {
+ if (isModule && _shouldRegenerateFromTemplate()) {
final Template template = new Template.fromName(fs.path.join('module', 'ios'));
template.render(directory, <String, dynamic>{}, printStatusWhenWriting: false);
}
+ if (!directory.existsSync())
+ return;
+ if (Cache.instance.fileOlderThanToolsStamp(generatedXcodePropertiesFile)) {
+ await xcode.updateGeneratedXcodeProperties(
+ project: parent,
+ buildInfo: BuildInfo.debug,
+ targetOverride: bundle.defaultMainPath,
+ previewDart2: true,
+ );
+ }
}
- bool _shouldRegenerate() {
+ bool _shouldRegenerateFromTemplate() {
return Cache.instance.fileOlderThanToolsStamp(directory.childFile('podhelper.rb'));
}
+
+ File get generatedXcodePropertiesFile => directory.childDirectory('Flutter').childFile('Generated.xcconfig');
+
+ Directory get pluginRegistrantHost {
+ return isModule
+ ? directory.childDirectory('Flutter').childDirectory('FlutterPluginRegistrant')
+ : directory.childDirectory('Runner');
+ }
}
-/// Represents the contents of the android/ folder of a Flutter project.
+/// Represents the Android sub-project of a Flutter project.
+///
+/// Instances will reflect the contents of the `android/` sub-folder of
+/// Flutter applications and the `.android/` sub-folder of Flutter modules.
class AndroidProject {
static final RegExp _applicationIdPattern = new RegExp('^\\s*applicationId\\s+[\'\"](.*)[\'\"]\\s*\$');
static final RegExp _groupPattern = new RegExp('^\\s*group\\s+[\'\"](.*)[\'\"]\\s*\$');
- AndroidProject(this.directory);
+ AndroidProject._(this.parent);
+
+ /// The parent of this project.
+ final FlutterProject parent;
+
+ /// The directory of this project.
+ Directory get directory => parent.directory.childDirectory(isModule ? '.android' : 'android');
+
+ /// True, if the parent Flutter project is a module.
+ bool get isModule => parent.isModule;
File get gradleManifestFile {
return isUsingGradle()
@@ -211,8 +215,6 @@
return directory.childFile('build.gradle').existsSync();
}
- final Directory directory;
-
Future<String> applicationId() {
final File gradleFile = directory.childDirectory('app').childFile('build.gradle');
return _firstMatchInFile(gradleFile, _applicationIdPattern).then((Match match) => match?.group(1));
@@ -222,32 +224,31 @@
final File gradleFile = directory.childFile('build.gradle');
return _firstMatchInFile(gradleFile, _groupPattern).then((Match match) => match?.group(1));
}
-}
-/// Represents the contents of the .android/ folder of a Flutter module project.
-class AndroidModuleProject {
- AndroidModuleProject(this.directory);
-
- final Directory directory;
-
- Future<void> ensureReadyForPlatformSpecificTooling(FlutterProject project) async {
- if (_shouldRegenerate()) {
+ Future<void> ensureReadyForPlatformSpecificTooling() async {
+ if (isModule && _shouldRegenerateFromTemplate()) {
final Template template = new Template.fromName(fs.path.join('module', 'android'));
template.render(
directory,
<String, dynamic>{
- 'androidIdentifier': project.manifest.androidPackage,
+ 'androidIdentifier': parent.manifest.androidPackage,
},
printStatusWhenWriting: false,
);
gradle.injectGradleWrapper(directory);
}
- await gradle.updateLocalProperties(project: project, requireAndroidSdk: false);
+ if (!directory.existsSync())
+ return;
+ await gradle.updateLocalProperties(project: parent, requireAndroidSdk: false);
}
- bool _shouldRegenerate() {
+ bool _shouldRegenerateFromTemplate() {
return Cache.instance.fileOlderThanToolsStamp(directory.childFile('build.gradle'));
}
+
+ File get localPropertiesFile => directory.childFile('local.properties');
+
+ Directory get pluginRegistrantHost => directory.childDirectory(isModule ? 'Flutter' : 'app');
}
/// Asynchronously returns the first line-based match for [regExp] in [file].