Skip to content

[flutter_tools] Lazily initialize AndroidStudio validation and optimize candidate selection - #191968

Merged
auto-submit[bot] merged 19 commits into
flutter:masterfrom
bkonyi:optimize_androidstudio_constructor_initialization
Oct 13, 2026
Merged

auto-submit[bot] merged 19 commits into
flutter:masterfrom
bkonyi:optimize_androidstudio_constructor_initialization

Conversation

@bkonyi

@bkonyi bkonyi commented Aug 28, 2026 •

Copy link
Copy Markdown
Contributor

Description

This PR optimizes Android toolchain initialization by deferring expensive synchronous operations, candidate discovery, and SDK location scans out of tool startup.

Why this is needed now

As part of the modular dependency injection migration (#190724), context objects like AndroidContext are created upfront during ToolDependencies.bootstrap / runner initialization rather than via the old global ambient context (AppContext).

Previously, AndroidStudio and AndroidSdk were initialized lazily only when a command or service queried globals.androidStudio or context.get<AndroidStudio>(). With upfront AndroidContext creation, AndroidStudio.latestValid() and AndroidSdk.locateAndroidSdk() ran on tool startup for every command. Because the constructors and locators performed synchronous filesystem scans and executed java -version (a JVM cold start), this introduced unnecessary latency to tool startup, including commands completely unrelated to Android (such as flutter config or iOS-specific workflows).

Changes

  • Defers synchronous Java validation (_initAndValidate()) out of the AndroidStudio constructor so that object instantiation is fast and side-effect free.
  • isValid, javaPath, and validationMessages are evaluated lazily on first access.
  • In AndroidStudio.latestValid(), candidate installations are sorted in descending order of version prior to validation, allowing candidate discovery to short-circuit upon encountering the first valid installation without executing Java checks against older, unneeded candidates.
  • AndroidContext accepts lazy builder closures (required AndroidSdk? Function() androidSdkBuilder, required AndroidStudio? Function() androidStudioBuilder, required GradleUtils Function() gradleUtilsBuilder, and required Java? Function() javaBuilder) so commands that do not use Android do not pay any startup initialization cost.
  • Java.find accepts required AndroidStudio? Function() androidStudioBuilder builder closure so that creating the Java object does not eagerly trigger AndroidStudio.latestValid().

Related Issues

Part of #47161 (flutter_tools modular dependency injection migration).

Tests

  • Updated packages/flutter_tools/test/general.shard/android/android_studio_test.dart to remove obsolete FakeCommand expectations for unneeded java -version executions.
  • Added unit tests verifying:
    • Constructor instantiation does not execute validation or touch the process manager.
    • isValid, javaPath, and validationMessages evaluate lazily on first access across independent instances.
    • latestValid() short-circuits candidate validation once a valid installation is found.
    • latestValid() falls back to older candidate installations when a newer installation is invalid.
  • Updated packages/flutter_tools/test/general.shard/context/dependency_injection_test.dart to verify:
    • AndroidContext lazily evaluates and memoizes androidSdk, androidStudio, gradleUtils, and java.
    • AndroidContext memoizes null results without re-evaluating builder closures.
    • ToolDependencies.bootstrap shares the same AndroidStudio instance across AndroidContext and Java.
    • ToolDependencies.bootstrap lazily evaluates androidSdk on first access rather than on startup.
  • Updated packages/flutter_tools/test/general.shard/android/java_test.dart to verify:
    • Java.find does not invoke the androidStudioBuilder factory if jdk-dir is configured.

…ze candidate selection

Defers synchronous Java validation (`_initAndValidate()`) out of the `AndroidStudio`
constructor so that object instantiation is fast and side-effect free. Access to
`isValid`, `javaPath`, and `validationMessages` now evaluates validation lazily on first read.

Additionally, `AndroidStudio.latestValid()` now sorts discovered candidates in descending
order of version before testing validity, allowing candidate discovery to short-circuit
on the newest valid installation without executing Java version checks against older,
unneeded candidate installations.
@bkonyi
bkonyi requested a review from a team as a code owner August 28, 2026 16:57
@bkonyi
bkonyi requested review from mboetger and removed request for a team August 28, 2026 16:57
@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label Aug 28, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces lazy validation for AndroidStudio instances, deferring initialization until properties like javaPath, isValid, or validationMessages are accessed. This allows the latestValid method to short-circuit validation once a valid installation is found. The tests are updated to verify this lazy behavior and short-circuiting logic. The review feedback suggests copying the studios list before sorting it in latestValid to prevent potential runtime errors or side effects from in-place sorting.

I am having trouble creating individual review comments. Click here to see my feedback.

packages/flutter_tools/lib/src/android/android_studio.dart (269-275)

medium

Sorting the studios list in-place can lead to unexpected side effects if the list returned by allInstalled() is cached, shared, or unmodifiable (which would cause a runtime UnsupportedError).

To prevent this, copy the list before sorting it.

    final List<AndroidStudio> sortedStudios = studios.toList()..sort(_compareCandidates);

    for (final studio in sortedStudios) {
      if (studio.isValid) {
        return studio;
      }
    }

@bkonyi
bkonyi requested a review from jason-simmons August 28, 2026 17:04
@github-actions github-actions Bot added tool Affects the "flutter" command-line tool. See also t: labels. team-android Owned by Android platform team labels Aug 28, 2026
@jason-simmons

Copy link
Copy Markdown
Contributor

With this PR, commands that do not need AndroidStudio are still paying the cost of its initialization because globals.androidStudio and ToolDependencies are calling AndroidStudio.latestValid.

latestValid will still need to do the _initAndValidate work for at least one AndroidStudio instance even if it is never used afterward.

bkonyi added 9 commits August 28, 2026 18:39
…ntext

Accepts lazy factory closures in AndroidContext so that AndroidStudio and Java
discovery are not executed on CLI startup. Uses late final variables in
ToolDependencies and AndroidContext to memoize discovery on first access and
share instances without duplicate disk scans.
Updates Java.find to take an AndroidStudio factory closure instead of an
instance so that AndroidStudio discovery is not eagerly triggered when
resolving Java.
Renames androidStudio and java initializers to androidStudioBuilder and
javaBuilder to clearly distinguish factory closures from resolved properties.
…es in ToolDependencies

Defines androidStudioBuilder and javaBuilder directly as closure functions
in ToolDependencies.init rather than local late final variables.
Accepts androidSdkBuilder in AndroidContext so that AndroidSdk.locateAndroidSdk
is not eagerly executed on CLI startup. Evaluates and memoizes androidSdk on
first access.
Accepts gradleUtilsBuilder in AndroidContext so that all AndroidContext
properties are lazily initialized on demand via builder closures.
@bkonyi

bkonyi commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Good catch! The eager execution was originating from ToolDependencies.bootstrap initializing EmulatorManager, which was eagerly reading finalAndroidContext.java. Reading that getter evaluated Java.find(...) and invoked androidStudioBuilder() / AndroidStudio.latestValid on startup.

EmulatorManager has now been updated to accept a Java? Function()? javaBuilder closure and evaluate _java lazily. ToolDependencies wires javaBuilder: () => finalAndroidContext.java into EmulatorManager. This ensures neither Java nor AndroidStudio.latestValid is evaluated on startup or basic command execution unless explicitly accessed.

@mboetger

Copy link
Copy Markdown
Contributor

Good catch! The eager execution was originating from ToolDependencies.bootstrap initializing EmulatorManager, which was eagerly reading finalAndroidContext.java. Reading that getter evaluated Java.find(...) and invoked androidStudioBuilder() / AndroidStudio.latestValid on startup.

EmulatorManager has now been updated to accept a Java? Function()? javaBuilder closure and evaluate _java lazily. ToolDependencies wires javaBuilder: () => finalAndroidContext.java into EmulatorManager. This ensures neither Java nor AndroidStudio.latestValid is evaluated on startup or basic command execution unless explicitly accessed.

Is it just the EmulatorManager - or does it happen when we just pass the params to bootstrap?

final ToolDependencies toolDeps = await ToolDependencies.bootstrap(                  
      androidSdk: globals.androidSdk,       // -> AndroidSdk.locateAndroidSdk()                                                                     
      androidStudio: globals.androidStudio, // -> AndroidStudio.latestValid()                                                                  
...                                      
    );                             

@bkonyi

bkonyi commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Is it just the EmulatorManager - or does it happen when we just pass the params to bootstrap?

final ToolDependencies toolDeps = await ToolDependencies.bootstrap(                  
      androidSdk: globals.androidSdk,       // -> AndroidSdk.locateAndroidSdk()                                                                     
      androidStudio: globals.androidStudio, // -> AndroidStudio.latestValid()                                                                  
...                                      
    );                             

Sorry, not sure how I missed this reply.

Yes, this also happens when we pass the params to bootstrap as well. Fixed!

mboetger
mboetger previously approved these changes Oct 12, 2026
…_comments

# Conflicts:
#	packages/flutter_tools/test/general.shard/context/dependency_injection_test.dart
@bkonyi bkonyi added the autosubmit Merge PR when tree becomes green via auto submit App label Oct 13, 2026
@auto-submit
auto-submit Bot added this pull request to the merge queue Oct 13, 2026
Merged via the queue into flutter:master with commit debf408 Oct 13, 2026
23 checks passed
@flutter-dashboard flutter-dashboard Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Oct 13, 2026
auto-submit Bot pushed a commit to flutter/packages that referenced this pull request Oct 14, 2026
Roll Flutter from 4249ccbc18a3 to 426be968b5eb (44 revisions)

flutter/flutter@4249ccb...426be96

2026-10-14 [email protected] Allow color delta tolerance in in-memory fuzzy image comparator (flutter/flutter#193937)
2026-10-14 [email protected] [web] Drop stale composing range after text is replaced mid-composition (flutter/flutter#194096)
2026-10-14 [email protected] Roll Skia from 2a2bbea66844 to acde3755d2e2 (1 revision) (flutter/flutter#194376)
2026-10-14 [email protected] Roll Skia from 128c983b12de to 2a2bbea66844 (1 revision) (flutter/flutter#194371)
2026-10-14 [email protected] Roll Skia from 48a294ed4955 to 128c983b12de (3 revisions) (flutter/flutter#194353)
2026-10-14 [email protected] feat(flutter_tools): extract WebDebugConfig from DebuggingOptions (flutter/flutter#194312)
2026-10-14 [email protected] Roll Skia from ea6781c5b6d1 to 48a294ed4955 (2 revisions) (flutter/flutter#194349)
2026-10-14 [email protected] Include <vector> in Android proc_table.h. (flutter/flutter#194325)
2026-10-14 [email protected] Roll Dart SDK from 5933cb486b4a to 6b50116630d0 (28 revisions) (flutter/flutter#194341)
2026-10-14 [email protected] Move GLES3 instancing API entry points to FOR_EACH_IMPELLER_GLES3_PROC (flutter/flutter#194319)
2026-10-14 [email protected] feat(flutter_tools): extract AndroidGradleConfig and move toGradleConfig to AndroidBuildInfo (flutter/flutter#194016)
2026-10-14 [email protected] Roll Skia from ae297bec6012 to ea6781c5b6d1 (8 revisions) (flutter/flutter#194331)
2026-10-14 [email protected] [flutter_tools] Migrate gradle_errors and deferred_components_prebuild_validator away from globals (flutter/flutter#193649)
2026-10-14 [email protected] [Impeller] Cache GLES sampler configuration in RenderPassStateCache (flutter/flutter#193922)
2026-10-14 [email protected] Roll pub packages (flutter/flutter#194332)
2026-10-13 [email protected] [tool] Migrate RunCommand to modular dependency injection (flutter/flutter#190791)
2026-10-13 [email protected] Include `engine/src/flutter/.ci.yaml` in quarterly `goldctl` checklist (flutter/flutter#194316)
2026-10-13 [email protected] [flutter_tools] Remove the last AMD module system support (flutter/flutter#193607)
2026-10-13 [email protected] Roll Skia from 8b3394a60654 to ae297bec6012 (5 revisions) (flutter/flutter#194310)
2026-10-13 [email protected] [web] Remove the a11y placeholder only after the gesture ends (flutter/flutter#194109)
2026-10-13 [email protected] Upgrade goldctl to the latest version (flutter/flutter#193932)
2026-10-13 [email protected] [flutter_tools] Lazily initialize AndroidStudio validation and optimize candidate selection (flutter/flutter#191968)
2026-10-13 [email protected] [flutter_tools] Clean up outdated widget preview TODOs and refactor static state (flutter/flutter#189437)
2026-10-13 [email protected] Roll Skia from e5b5db6d0d6f to 8b3394a60654 (2 revisions) (flutter/flutter#194306)
2026-10-13 [email protected] Redistribute TESTOWNERS for Android team (flutter/flutter#193861)
2026-10-13 [email protected] feat(flutter_tools): introduce Test and Drive specification objects (flutter/flutter#194017)
2026-10-13 [email protected] [Impeller] Keep the aspect ratio when shrinking images to the max texture size (flutter/flutter#194010)
2026-10-13 [email protected] [flutter tool] preserve inherited `DASH__SUPPRESS_ANALYTICS=true` for sub-tools (flutter/flutter#194261)
2026-10-13 [email protected] Roll Packages from d8d9e97 to a1ed23c (9 revisions) (flutter/flutter#194302)
2026-10-13 [email protected] Use PE/COFF AOT snapshots for Windows x64 (flutter/flutter#187594)
2026-10-13 [email protected] Fix Expansible crash when PageStorage holds a non-bool value (flutter/flutter#192195)
2026-10-13 [email protected] Roll Skia from ffeccb3ca58a to e5b5db6d0d6f (1 revision) (flutter/flutter#194297)
2026-10-13 [email protected] Explain why initState() is too early to depend on an inherited widget (flutter/flutter#192727)
2026-10-13 [email protected] Refactor TestPlatformDispatcher (flutter/flutter#192747)
2026-10-13 [email protected] Roll Skia from 3de0984208ec to ffeccb3ca58a (2 revisions) (flutter/flutter#194291)
2026-10-13 [email protected] Roll Skia from 0580c2b780f6 to 3de0984208ec (2 revisions) (flutter/flutter#194281)
2026-10-13 [email protected] Roll Skia from f641d55e07a8 to 0580c2b780f6 (3 revisions) (flutter/flutter#194275)
2026-10-13 [email protected] Roll Skia from 0e5d8d1be81b to f641d55e07a8 (1 revision) (flutter/flutter#194266)
2026-10-13 [email protected] Scaffold view metrics overrides (flutter/flutter#192764)
2026-10-13 [email protected] [web] Add SemanticsRole.listBox and SemanticsRole.option (flutter/flutter#192971)
2026-10-13 [email protected] Pin mac_unopt to macOS 15 (flutter/flutter#194254)
2026-10-13 [email protected] Roll pub packages (flutter/flutter#194257)
2026-10-12 [email protected] Roll Skia from 17f6986c226c to 0e5d8d1be81b (4 revisions) (flutter/flutter#194249)
2026-10-12 [email protected] Make DisplayLists' use of DlPaint, SkPaint, and impeller::Paint agree on the default isAntiAlias (flutter/flutter#193939)

If this roll has caused a breakage, revert this CL and set the roller
...
suryaKommana2662 pushed a commit to suryaKommana2662/flutter that referenced this pull request Oct 29, 2026
…ze candidate selection (flutter#191968)

## Description

This PR optimizes Android toolchain initialization by deferring
expensive synchronous operations, candidate discovery, and SDK location
scans out of tool startup.

### Why this is needed now
As part of the modular dependency injection migration
([flutter#190724](flutter#190724)), context
objects like `AndroidContext` are created upfront during
`ToolDependencies.bootstrap` / runner initialization rather than via the
old global ambient context (`AppContext`).

Previously, `AndroidStudio` and `AndroidSdk` were initialized lazily
only when a command or service queried `globals.androidStudio` or
`context.get<AndroidStudio>()`. With upfront `AndroidContext` creation,
`AndroidStudio.latestValid()` and `AndroidSdk.locateAndroidSdk()` ran on
tool startup for every command. Because the constructors and locators
performed synchronous filesystem scans and executed `java -version` (a
JVM cold start), this introduced unnecessary latency to tool startup,
including commands completely unrelated to Android (such as `flutter
config` or iOS-specific workflows).

### Changes
- Defers synchronous Java validation (`_initAndValidate()`) out of the
`AndroidStudio` constructor so that object instantiation is fast and
side-effect free.
- `isValid`, `javaPath`, and `validationMessages` are evaluated lazily
on first access.
- In `AndroidStudio.latestValid()`, candidate installations are sorted
in descending order of version prior to validation, allowing candidate
discovery to short-circuit upon encountering the first valid
installation without executing Java checks against older, unneeded
candidates.
- `AndroidContext` accepts lazy builder closures (`required AndroidSdk?
Function() androidSdkBuilder`, `required AndroidStudio? Function()
androidStudioBuilder`, `required GradleUtils Function()
gradleUtilsBuilder`, and `required Java? Function() javaBuilder`) so
commands that do not use Android do not pay any startup initialization
cost.
- `Java.find` accepts `required AndroidStudio? Function()
androidStudioBuilder` builder closure so that creating the `Java` object
does not eagerly trigger `AndroidStudio.latestValid()`.

## Related Issues
Part of [flutter#47161](flutter#47161)
(flutter_tools modular dependency injection migration).

## Tests
- Updated
`packages/flutter_tools/test/general.shard/android/android_studio_test.dart`
to remove obsolete `FakeCommand` expectations for unneeded `java
-version` executions.
- Added unit tests verifying:
- Constructor instantiation does not execute validation or touch the
process manager.
- `isValid`, `javaPath`, and `validationMessages` evaluate lazily on
first access across independent instances.
- `latestValid()` short-circuits candidate validation once a valid
installation is found.
- `latestValid()` falls back to older candidate installations when a
newer installation is invalid.
- Updated
`packages/flutter_tools/test/general.shard/context/dependency_injection_test.dart`
to verify:
- `AndroidContext` lazily evaluates and memoizes `androidSdk`,
`androidStudio`, `gradleUtils`, and `java`.
- `AndroidContext` memoizes `null` results without re-evaluating builder
closures.
- `ToolDependencies.bootstrap` shares the same `AndroidStudio` instance
across `AndroidContext` and `Java`.
- `ToolDependencies.bootstrap` lazily evaluates `androidSdk` on first
access rather than on startup.
- Updated
`packages/flutter_tools/test/general.shard/android/java_test.dart` to
verify:
- `Java.find` does not invoke the `androidStudioBuilder` factory if
`jdk-dir` is configured.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD team-android Owned by Android platform team tool Affects the "flutter" command-line tool. See also t: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants