Skip to content

[tool] Wire dependency injection into runner and executable with required non-nullable contexts - #190922

Open
bkonyi wants to merge 3 commits into
flutter:masterfrom
bkonyi:di/02-runner-bootstrap
Open

[tool] Wire dependency injection into runner and executable with required non-nullable contexts#190922
bkonyi wants to merge 3 commits into
flutter:masterfrom
bkonyi:di/02-runner-bootstrap

Conversation

@bkonyi

@bkonyi bkonyi commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Part 2 of the modular dependency injection migration (stacked on #190724).

Wires explicit dependency injection into the Flutter tool entrypoints:

  • Wires ToolDependencies.bootstrap into runner.dart to initialize dependencies at startup.
  • Updates FlutterCommandRunner to accept non-nullable toolContext, androidContext, appleContext, and toolDependencies.
  • Updates executable.dart to forward ToolDependencies into generateCommands.
  • Updates test_flutter_command_runner.dart and hermetic test doubles.

Part of #47161

@bkonyi
bkonyi requested review from a team as code owners August 11, 2026 14:53
@bkonyi
bkonyi requested review from mboetger and removed request for a team August 11, 2026 14:53
@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label Aug 11, 2026
@github-actions github-actions Bot added tool Affects the "flutter" command-line tool. See also t: labels. team-android Owned by Android platform team team-ios Owned by iOS platform team team-macos Owned by the macOS platform team labels Aug 11, 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 refactors dependency injection in flutter_tools by introducing AndroidContext, AppleContext, ToolContext, and ToolDependencies to manage and bootstrap tool dependencies instead of relying on global variables. Feedback on these changes highlights opportunities to improve testability and efficiency, such as using the shared finalShutdownHooks for the file system, utilizing finalSystemClock.now() for the terminal, preferring injected analytics over global analytics, avoiding a potential null pointer crash with DevtoolsLauncher.instance, and optimizing path traversal in _gatherProjectPaths by using fs.path.basename.

Comment on lines +175 to +179
delegate: LocalFileSystem(
LocalSignals.instance,
Signals.defaultExitSignals,
shutdownHooks ?? ShutdownHooks(),
),

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.

critical

In ToolDependencies.bootstrap, finalShutdownHooks is defined on line 136 as shutdownHooks ?? ShutdownHooks(). However, on line 178, a new instance is created via shutdownHooks ?? ShutdownHooks() and passed to LocalFileSystem. Because shutdownHooks is typically null during normal execution, this creates a completely separate, orphaned ShutdownHooks instance for the file system. Any shutdown hooks registered by the file system (such as deleting temporary directories/files) will be registered on this orphaned instance and will never be executed when the main finalShutdownHooks are triggered. We should pass finalShutdownHooks instead.

Suggested change
delegate: LocalFileSystem(
LocalSignals.instance,
Signals.defaultExitSignals,
shutdownHooks ?? ShutdownHooks(),
),
delegate: LocalFileSystem(
LocalSignals.instance,
Signals.defaultExitSignals,
finalShutdownHooks,
),
References
  1. Avoid duplicating state: Keep only one source of truth. (link)

Comment on lines +142 to +147
AnsiTerminal(
stdio: finalStdio,
platform: finalPlatform,
now: DateTime.now(),
shutdownHooks: finalShutdownHooks,
);

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.

medium

AnsiTerminal is initialized with DateTime.now(), which bypasses the mockable finalSystemClock defined on line 134. This makes it impossible to mock or control the time for the terminal in unit tests. We should use finalSystemClock.now() instead.

Suggested change
AnsiTerminal(
stdio: finalStdio,
platform: finalPlatform,
now: DateTime.now(),
shutdownHooks: finalShutdownHooks,
);
AnsiTerminal(
stdio: finalStdio,
platform: finalPlatform,
now: finalSystemClock.now(),
shutdownHooks: finalShutdownHooks,
);

Comment on lines 476 to 478
if ((topLevelResults[FlutterGlobalOptions.kSuppressAnalyticsFlag] as bool?) ?? false) {
globals.analytics.suppressTelemetry();
}

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.

medium

To align with the dependency injection refactoring, prefer using the injected toolDependencies?.analytics over the global globals.analytics.

Suggested change
if ((topLevelResults[FlutterGlobalOptions.kSuppressAnalyticsFlag] as bool?) ?? false) {
globals.analytics.suppressTelemetry();
}
if ((topLevelResults[FlutterGlobalOptions.kSuppressAnalyticsFlag] as bool?) ?? false) {
(toolDependencies?.analytics ?? globals.analytics).suppressTelemetry();
}

Comment on lines 500 to +511
if ((topLevelResults[FlutterGlobalOptions.kVersionFlag] as bool?) ?? false) {
globals.analytics.send(
Event.flutterCommandResult(
commandPath: 'version',
result: 'success',
commandHasTerminal: globals.stdio.hasTerminal,
),
);
final FlutterVersion version = globals.flutterVersion.fetchTagsAndGetVersion(
clock: globals.systemClock,
try {
globals.analytics.send(
Event.flutterCommandResult(
commandPath: 'version',
result: 'success',
commandHasTerminal: _toolContext.stdio.hasTerminal,
),
);
} on UnsupportedError catch (_) {
// Context not available in unit tests without context.
}

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.

medium

Prefer using the injected toolDependencies?.analytics over the global globals.analytics to support proper dependency injection.

Suggested change
if ((topLevelResults[FlutterGlobalOptions.kVersionFlag] as bool?) ?? false) {
globals.analytics.send(
Event.flutterCommandResult(
commandPath: 'version',
result: 'success',
commandHasTerminal: globals.stdio.hasTerminal,
),
);
final FlutterVersion version = globals.flutterVersion.fetchTagsAndGetVersion(
clock: globals.systemClock,
try {
globals.analytics.send(
Event.flutterCommandResult(
commandPath: 'version',
result: 'success',
commandHasTerminal: _toolContext.stdio.hasTerminal,
),
);
} on UnsupportedError catch (_) {
// Context not available in unit tests without context.
}
if ((topLevelResults[FlutterGlobalOptions.kVersionFlag] as bool?) ?? false) {
try {
(toolDependencies?.analytics ?? globals.analytics).send(
Event.flutterCommandResult(
commandPath: "version",
result: "success",
commandHasTerminal: _toolContext.stdio.hasTerminal,
),
);
} on UnsupportedError catch (_) {
// Context not available in unit tests without context.
}

Comment on lines +536 to +538
if (shouldPrintDtdUri) {
DevtoolsLauncher.instance!.printDtdUri = shouldPrintDtdUri;
}

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.

medium

Using the null-assertion operator ! on DevtoolsLauncher.instance can cause a runtime crash if it is null, even when shouldPrintDtdUri is true. Since DevtoolsLauncher.instance is often null in unit tests, we should use the null-safe operator ?. instead.

Suggested change
if (shouldPrintDtdUri) {
DevtoolsLauncher.instance!.printDtdUri = shouldPrintDtdUri;
}
if (shouldPrintDtdUri) {
DevtoolsLauncher.instance?.printDtdUri = shouldPrintDtdUri;
}

Comment on lines +576 to +578
if (entity is Directory && !fs.path.split(entity.path).contains('.dart_tool')) {
return _gatherProjectPaths(fs, entity.path);
}

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.

medium

In _gatherProjectPaths, checking !fs.path.split(entity.path).contains('.dart_tool') splits the entire path string into a list of segments and searches through it on every single directory traversal. Since we are doing a recursive pre-order traversal and skip .dart_tool at the first level we encounter it, we only need to check if the current directory's name (the basename) is .dart_tool. Using fs.path.basename(entity.path) != '.dart_tool' is much more efficient and avoids unnecessary string and list allocations.

Suggested change
if (entity is Directory && !fs.path.split(entity.path).contains('.dart_tool')) {
return _gatherProjectPaths(fs, entity.path);
}
if (entity is Directory && fs.path.basename(entity.path) != ".dart_tool") {
return _gatherProjectPaths(fs, entity.path);
}
References
  1. Suggest simplification and refactoring: Assess whether the code can be made simpler or refactored to enhance readability and maintainability. (link)

@bkonyi
bkonyi force-pushed the di/02-runner-bootstrap branch from cf41bc2 to cb1f566 Compare August 11, 2026 15:31
@bkonyi
bkonyi force-pushed the di/02-runner-bootstrap branch from cb1f566 to 5533bb8 Compare August 11, 2026 17:58
victorsanni pushed a commit to victorsanni/flutter that referenced this pull request Aug 12, 2026
flutter#190724)

## Summary

Part 1 of the modular dependency injection migration.

Defines the foundational modular dependency injection containers and
bootstrapper for `flutter_tools`:
1. **`ToolContext`**: Layer 1 OS wrappers (`HostEnvironment`) and Layer
2 SDK state (`ToolConfiguration`).
2. **Nullable Sub-contexts**: `AndroidContext` and `AppleContext` for
clean platform isolation.
3. **`ToolDependencies`**: Topologically instantiates and manages the
dependency graph at startup with explicit overrides and lazy closures.
4. **Hermetic Test Doubles**: Includes `dependency_injection_test.dart`
and fake context doubles.

Followed by Part 2 (flutter#190922) for runner wiring.

Part of flutter#47161
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 team-ios Owned by iOS platform team team-macos Owned by the macOS 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.

1 participant