[flutter_tools] Lazily initialize AndroidSdk platform and build-tools discovery (#191972)
## Description
This PR optimizes `AndroidSdk` initialization by deferring expensive
synchronous filesystem operations out of the constructor.
### Why this is needed now
As part of the modular dependency injection migration
([#190724](https://github.com/flutter/flutter/pull/190724)), context
objects like `AndroidContext` are created upfront during
`ToolDependencies.bootstrap` / runner initialization rather than via the
old global ambient context (`AppContext`).
Previously, `AndroidSdk` was initialized lazily only when a command or
service queried `globals.androidSdk` or `context.get<AndroidSdk>()`.
With upfront `AndroidContext` creation, `AndroidSdk.locateAndroidSdk()`
runs on tool startup for every command. Because the `AndroidSdk`
constructor eagerly executed `reinitialize()`, it performed synchronous
directory listings across `build-tools` and `platforms` directories and
read `build.prop` files on disk. This introduced unnecessary disk I/O
and latency to tool startup, including for commands completely unrelated
to Android (such as `flutter config` or iOS-specific workflows).
### Changes
- Defers synchronous scanning of build-tools and platform directories
(`reinitialize()`) out of the `AndroidSdk` constructor so that object
instantiation is fast and side-effect free.
- `sdkVersions` and `latestVersion` are evaluated lazily on first
access.
- Retains `_fileSystem` on the instance so that explicit calls to
`reinitialize()` continue to refresh platform and version state with the
expected filesystem.
## Related Issues
Part of [#47161](https://github.com/flutter/flutter/issues/47161)
(flutter_tools modular dependency injection migration).
## Tests
- Updated existing test in
`packages/flutter_tools/test/general.shard/android/android_sdk_test.dart`
(`constructing an AndroidSdk handles no matching lines in build.prop`)
to access `sdk.latestVersion` so that `build.prop` parsing
error-handling is executed during lazy initialization.
- Added unit tests verifying:
- Constructor instantiation does not initialize `sdkVersions` or
`latestVersion` or scan platform directories.
- `sdkVersions` and `latestVersion` evaluate lazily on first access
across independent instances.
- Calling `reinitialize()` updates `sdkVersions` and `latestVersion` and
correctly falls back to stored `_fileSystem`.
diff --git a/packages/flutter_tools/lib/src/android/android_sdk.dart b/packages/flutter_tools/lib/src/android/android_sdk.dart
index c89a695..ab72c1e 100644
--- a/packages/flutter_tools/lib/src/android/android_sdk.dart
+++ b/packages/flutter_tools/lib/src/android/android_sdk.dart
@@ -44,17 +44,24 @@
// $ANDROID_HOME/platforms/android-23/android.jar
// $ANDROID_HOME/platforms/android-N/android.jar
class AndroidSdk {
- AndroidSdk(this.directory, {this._java, FileSystem? fileSystem}) {
- reinitialize(fileSystem: fileSystem);
- }
+ AndroidSdk(this.directory, {this._java});
/// The Android SDK root directory.
final Directory directory;
final Java? _java;
- var _sdkVersions = <AndroidSdkVersion>[];
+ List<AndroidSdkVersion> _sdkVersions = <AndroidSdkVersion>[];
AndroidSdkVersion? _latestVersion;
+ bool _reinitialized = false;
+
+ void _ensureInitialized() {
+ if (_reinitialized) {
+ return;
+ }
+ _reinitialized = true;
+ reinitialize();
+ }
/// Whether the `cmdline-tools` directory exists in the Android SDK.
///
@@ -170,9 +177,15 @@
return globals.fs.isDirectorySync(globals.fs.path.join(dir, 'licenses'));
}
- List<AndroidSdkVersion> get sdkVersions => _sdkVersions;
+ List<AndroidSdkVersion> get sdkVersions {
+ _ensureInitialized();
+ return _sdkVersions;
+ }
- AndroidSdkVersion? get latestVersion => _latestVersion;
+ AndroidSdkVersion? get latestVersion {
+ _ensureInitialized();
+ return _latestVersion;
+ }
late final String? adbPath = getPlatformToolsPath(globals.platform.isWindows ? 'adb.exe' : 'adb');
@@ -451,7 +464,8 @@
///
/// This method should be called in a case where the tooling may have updated
/// SDK artifacts, such as after running a gradle build.
- void reinitialize({FileSystem? fileSystem}) {
+ void reinitialize() {
+ _reinitialized = true;
var buildTools = <Version>[]; // 19.1.0, 22.0.1, ...
final Directory buildToolsDir = directory.childDirectory('build-tools');
@@ -514,10 +528,10 @@
return AndroidSdkVersion._(
this,
- sdkLevel: platformVersion,
- platformName: platformName,
buildToolsVersion: buildToolsVersion,
- fileSystem: fileSystem ?? globals.fs,
+ fileSystem: directory.fileSystem,
+ platformName: platformName,
+ sdkLevel: platformVersion,
);
})
.whereType<AndroidSdkVersion>()
diff --git a/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart b/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart
index 42e9c94..f3dc2a9 100644
--- a/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart
+++ b/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart
@@ -117,7 +117,6 @@
AndroidSdk(
(fileSystem ?? globals.fs).directory(missingSdkPath()),
java: FakeJava(),
- fileSystem: fileSystem ?? globals.fs,
),
),
analytics: analytics ?? fakeAnalytics,
@@ -133,11 +132,7 @@
description,
body,
overrides: <Type, Generator>{
- AndroidSdk: () => AndroidSdk(
- fileSystem.directory(missingSdkPath()),
- java: FakeJava(),
- fileSystem: fileSystem,
- ),
+ AndroidSdk: () => AndroidSdk(fileSystem.directory(missingSdkPath()), java: FakeJava()),
ProcessManager: () => processManager,
...overrides,
},
@@ -230,11 +225,7 @@
.childFile('source.properties')
.createSync(recursive: true);
fileSystem.directory(ndkPath('29.0.13846066-bad')).createSync(recursive: true);
- return AndroidSdk(
- fileSystem.directory(sdkPath()),
- java: FakeJava(),
- fileSystem: fileSystem,
- );
+ return AndroidSdk(fileSystem.directory(sdkPath()), java: FakeJava());
},
AndroidStudio: () => FakeAndroidStudio(),
},
@@ -321,11 +312,7 @@
.directory(fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin'))
.childFile(globals.platform.isWindows ? 'sdkmanager.bat' : 'sdkmanager')
.createSync(recursive: true);
- return AndroidSdk(
- fileSystem.directory(sdkPath()),
- java: FakeJava(),
- fileSystem: fileSystem,
- );
+ return AndroidSdk(fileSystem.directory(sdkPath()), java: FakeJava());
},
AndroidStudio: () => FakeAndroidStudio(),
},
@@ -416,11 +403,7 @@
.directory(fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin'))
.childFile(globals.platform.isWindows ? 'sdkmanager.bat' : 'sdkmanager')
.createSync(recursive: true);
- return AndroidSdk(
- fileSystem.directory(sdkPath()),
- java: FakeJava(),
- fileSystem: fileSystem,
- );
+ return AndroidSdk(fileSystem.directory(sdkPath()), java: FakeJava());
},
AndroidStudio: () => FakeAndroidStudio(),
},
@@ -507,11 +490,7 @@
.directory(fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin'))
.childFile(globals.platform.isWindows ? 'sdkmanager.bat' : 'sdkmanager')
.createSync(recursive: true);
- return AndroidSdk(
- fileSystem.directory(sdkPath()),
- java: FakeJava(),
- fileSystem: fileSystem,
- );
+ return AndroidSdk(fileSystem.directory(sdkPath()), java: FakeJava());
},
AndroidStudio: () => FakeAndroidStudio(),
},
@@ -1486,11 +1465,7 @@
.directory(fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin'))
.childFile(apkAnalyzerBinaryName)
.createSync(recursive: true);
- return AndroidSdk(
- fileSystem.directory(sdkPath()),
- java: FakeJava(),
- fileSystem: fileSystem,
- );
+ return AndroidSdk(fileSystem.directory(sdkPath()), java: FakeJava());
},
AndroidStudio: () => FakeAndroidStudio(),
ProcessManager: () => processManager,
@@ -1557,11 +1532,7 @@
.directory(fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin'))
.childFile(apkAnalyzerBinaryName)
.createSync(recursive: true);
- return AndroidSdk(
- fileSystem.directory(sdkPath()),
- java: FakeJava(),
- fileSystem: fileSystem,
- );
+ return AndroidSdk(fileSystem.directory(sdkPath()), java: FakeJava());
},
AndroidStudio: () => FakeAndroidStudio(),
ProcessManager: () => processManager,
@@ -1621,11 +1592,7 @@
.directory(fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin'))
.childFile(apkAnalyzerBinaryName)
.createSync(recursive: true);
- return AndroidSdk(
- fileSystem.directory(sdkPath()),
- java: FakeJava(),
- fileSystem: fileSystem,
- );
+ return AndroidSdk(fileSystem.directory(sdkPath()), java: FakeJava());
},
AndroidStudio: () => FakeAndroidStudio(),
},
@@ -1639,11 +1606,7 @@
.directory(fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin'))
.childFile(apkAnalyzerBinaryName)
.createSync(recursive: true);
- final sdk = AndroidSdk(
- fileSystem.directory(sdkPath()),
- java: FakeJava(),
- fileSystem: fileSystem,
- );
+ final sdk = AndroidSdk(fileSystem.directory(sdkPath()), java: FakeJava());
final AndroidGradleBuilder builder = createBuilder(
java: FakeJava(),
logger: logger,
@@ -1709,11 +1672,7 @@
.directory(fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin'))
.childFile(apkAnalyzerBinaryName)
.createSync(recursive: true);
- return AndroidSdk(
- fileSystem.directory(sdkPath()),
- java: FakeJava(),
- fileSystem: fileSystem,
- );
+ return AndroidSdk(fileSystem.directory(sdkPath()), java: FakeJava());
},
AndroidStudio: () => FakeAndroidStudio(),
},
@@ -1727,11 +1686,7 @@
.directory(fileSystem.path.join(sdkPath(), 'cmdline-tools', 'latest', 'bin'))
.childFile(apkAnalyzerBinaryName)
.createSync(recursive: true);
- final sdk = AndroidSdk(
- fileSystem.directory(sdkPath()),
- java: FakeJava(),
- fileSystem: fileSystem,
- );
+ final sdk = AndroidSdk(fileSystem.directory(sdkPath()), java: FakeJava());
final AndroidGradleBuilder builder = createBuilder(
java: FakeJava(),
logger: logger,
@@ -2223,11 +2178,7 @@
.childFile('source.properties')
.createSync(recursive: true);
fileSystem.directory(ndkPath('29.0.13846066-bad')).createSync(recursive: true);
- return AndroidSdk(
- fileSystem.directory(sdkPath()),
- java: FakeJava(),
- fileSystem: fileSystem,
- );
+ return AndroidSdk(fileSystem.directory(sdkPath()), java: FakeJava());
},
AndroidStudio: () => FakeAndroidStudio(),
},
@@ -3293,7 +3244,6 @@
return AndroidSdk(
fileSystem.directory(sdkPath()),
java: FakeJava(version: const Version.withText(21, 0, 0, '21.0.0')),
- fileSystem: fileSystem,
);
},
AndroidStudio: () => FakeAndroidStudio(),
@@ -3397,7 +3347,6 @@
return AndroidSdk(
fileSystem.directory(sdkPath()),
java: FakeJava(version: const Version.withText(21, 0, 0, '21.0.0')),
- fileSystem: fileSystem,
);
},
AndroidStudio: () => FakeAndroidStudio(),
@@ -3504,7 +3453,6 @@
return AndroidSdk(
fileSystem.directory(sdkPath()),
java: FakeJava(version: const Version.withText(21, 0, 0, '21.0.0')),
- fileSystem: fileSystem,
);
},
AndroidStudio: () => FakeAndroidStudio(),
diff --git a/packages/flutter_tools/test/general.shard/android/android_sdk_test.dart b/packages/flutter_tools/test/general.shard/android/android_sdk_test.dart
index 01f8df9..01dfece 100644
--- a/packages/flutter_tools/test/general.shard/android/android_sdk_test.dart
+++ b/packages/flutter_tools/test/general.shard/android/android_sdk_test.dart
@@ -36,15 +36,16 @@
config.setValue('android-sdk', sdkDir.path);
try {
- AndroidSdk.locateAndroidSdk()!;
+ final AndroidSdk sdk = AndroidSdk.locateAndroidSdk()!;
+ sdk.latestVersion;
} on StateError catch (err) {
fail('sdk.reinitialize() threw a StateError:\n$err');
}
},
overrides: <Type, Generator>{
+ Config: () => config,
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
- Config: () => config,
},
);
@@ -510,6 +511,79 @@
Config: () => config,
},
);
+
+ testUsingContext(
+ 'does not initialize sdkVersions or latestVersion during constructor instantiation',
+ () {
+ final Directory sdkDir = createSdkDirectory(fileSystem: fileSystem);
+ final sdk = AndroidSdk(sdkDir);
+
+ // Constructor did not scan build-tools or platforms.
+ // We verify by modifying the directory before first access.
+ fileSystem
+ .directory(fileSystem.path.join(sdkDir.path, 'platforms', 'android-22'))
+ .deleteSync(recursive: true);
+
+ // First access to latestVersion triggers initialization and sees only remaining platforms.
+ expect(sdk.latestVersion, isNotNull);
+ expect(sdk.latestVersion!.sdkLevel, 23);
+ expect(sdk.sdkVersions.length, 1);
+ },
+ overrides: <Type, Generator>{
+ Config: () => config,
+ FileSystem: () => fileSystem,
+ ProcessManager: () => FakeProcessManager.any(),
+ },
+ );
+
+ testUsingContext(
+ 'evaluates sdkVersions and latestVersion lazily on first access',
+ () {
+ final Directory sdkDir = createSdkDirectory(fileSystem: fileSystem);
+
+ // Accessing latestVersion triggers initialization.
+ final sdk1 = AndroidSdk(sdkDir);
+ expect(sdk1.latestVersion, isNotNull);
+ expect(sdk1.latestVersion!.sdkLevel, 23);
+ expect(sdk1.sdkVersions.length, 2);
+
+ // Accessing sdkVersions triggers initialization independently.
+ final sdk2 = AndroidSdk(sdkDir);
+ expect(sdk2.sdkVersions.length, 2);
+ expect(sdk2.latestVersion, isNotNull);
+ expect(sdk2.latestVersion!.sdkLevel, 23);
+ },
+ overrides: <Type, Generator>{
+ Config: () => config,
+ FileSystem: () => fileSystem,
+ ProcessManager: () => FakeProcessManager.any(),
+ },
+ );
+
+ testUsingContext(
+ 'reinitialize updates sdkVersions and latestVersion when new platforms are installed',
+ () {
+ final Directory sdkDir = createSdkDirectory(fileSystem: fileSystem);
+ final sdk = AndroidSdk(sdkDir);
+
+ expect(sdk.latestVersion!.sdkLevel, 23);
+
+ // Add android-34 platform.
+ fileSystem
+ .directory(fileSystem.path.join(sdkDir.path, 'platforms', 'android-34'))
+ .createSync(recursive: true);
+
+ sdk.reinitialize();
+
+ expect(sdk.latestVersion!.sdkLevel, 34);
+ expect(sdk.sdkVersions.length, 3);
+ },
+ overrides: <Type, Generator>{
+ Config: () => config,
+ FileSystem: () => fileSystem,
+ ProcessManager: () => FakeProcessManager.any(),
+ },
+ );
});
const llvmHostDirectoryName = <String, String>{
@@ -533,7 +607,7 @@
final Directory sdkDir = createSdkDirectory(fileSystem: fileSystem, platform: platform);
config.setValue('android-sdk', sdkDir.path);
- final sdk = AndroidSdk(sdkDir, fileSystem: fileSystem);
+ final sdk = AndroidSdk(sdkDir);
late File clang;
late File ar;
late File ld;
@@ -584,7 +658,7 @@
final File ar = binDir.childFile('llvm-ar$extension')..createSync();
final File ld = binDir.childFile('ld.lld$extension')..createSync();
- final sdk = AndroidSdk(sdkDir, fileSystem: fileSystem);
+ final sdk = AndroidSdk(sdkDir);
expect(sdk.getNdkClangPath(platform: platform, config: config), clang.path);
expect(sdk.getNdkArPath(platform: platform, config: config), ar.path);
expect(sdk.getNdkLdPath(platform: platform, config: config), ld.path);
@@ -612,7 +686,7 @@
final File ar = binDir.childFile('llvm-ar$extension')..createSync();
final File ld = binDir.childFile('ld.lld$extension')..createSync();
- final sdk = AndroidSdk(sdkDir, fileSystem: fileSystem);
+ final sdk = AndroidSdk(sdkDir);
expect(sdk.getNdkClangPath(platform: platform, config: config), clang.path);
expect(sdk.getNdkArPath(platform: platform, config: config), ar.path);
expect(sdk.getNdkLdPath(platform: platform, config: config), ld.path);
@@ -643,7 +717,7 @@
final File ar = binDir.childFile('llvm-ar$extension')..createSync();
final File ld = binDir.childFile('ld.lld$extension')..createSync();
- final sdk = AndroidSdk(sdkDir, fileSystem: fileSystem);
+ final sdk = AndroidSdk(sdkDir);
expect(sdk.getNdkClangPath(platform: platform, config: config), clang.path);
expect(sdk.getNdkArPath(platform: platform, config: config), ar.path);
expect(sdk.getNdkLdPath(platform: platform, config: config), ld.path);
diff --git a/packages/flutter_tools/test/general.shard/cache_test.dart b/packages/flutter_tools/test/general.shard/cache_test.dart
index d1f7196..10e79a4 100644
--- a/packages/flutter_tools/test/general.shard/cache_test.dart
+++ b/packages/flutter_tools/test/general.shard/cache_test.dart
@@ -2099,7 +2099,7 @@
bool reinitialized = false;
@override
- void reinitialize({FileSystem? fileSystem}) {
+ void reinitialize() {
reinitialized = true;
}
}