Make AppContext immutable and race-free (#15984)

This updates AppContext per the recommendations in #15352

Fixes #15352
diff --git a/packages/flutter_tools/lib/src/android/android_studio.dart b/packages/flutter_tools/lib/src/android/android_studio.dart
index 3877aac..45897b1 100644
--- a/packages/flutter_tools/lib/src/android/android_studio.dart
+++ b/packages/flutter_tools/lib/src/android/android_studio.dart
@@ -12,8 +12,7 @@
 import '../globals.dart';
 import '../ios/plist_utils.dart';
 
-AndroidStudio get androidStudio =>
-    context.putIfAbsent(AndroidStudio, AndroidStudio.latestValid);
+AndroidStudio get androidStudio => context[AndroidStudio];
 
 // Android Studio layout:
 
diff --git a/packages/flutter_tools/lib/src/android/android_workflow.dart b/packages/flutter_tools/lib/src/android/android_workflow.dart
index 7ce5ba5..4b3649d 100644
--- a/packages/flutter_tools/lib/src/android/android_workflow.dart
+++ b/packages/flutter_tools/lib/src/android/android_workflow.dart
@@ -17,7 +17,7 @@
 import '../globals.dart';
 import 'android_sdk.dart';
 
-AndroidWorkflow get androidWorkflow => context.putIfAbsent(AndroidWorkflow, () => new AndroidWorkflow());
+AndroidWorkflow get androidWorkflow => context[AndroidWorkflow];
 
 enum LicensesAccepted {
   none,
diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart
index e1c8b01..f539a5a 100644
--- a/packages/flutter_tools/lib/src/artifacts.dart
+++ b/packages/flutter_tools/lib/src/artifacts.dart
@@ -84,8 +84,8 @@
 abstract class Artifacts {
   static Artifacts get instance => context[Artifacts];
 
-  static void useLocalEngine(String engineSrcPath, EngineBuildPaths engineBuildPaths) {
-    context.setVariable(Artifacts, new LocalEngineArtifacts(engineSrcPath, engineBuildPaths.targetEngine, engineBuildPaths.hostEngine));
+  static LocalEngineArtifacts getLocalEngine(String engineSrcPath, EngineBuildPaths engineBuildPaths) {
+    return new LocalEngineArtifacts(engineSrcPath, engineBuildPaths.targetEngine, engineBuildPaths.hostEngine);
   }
 
   // Returns the requested [artifact] for the [platform] and [mode] combination.
diff --git a/packages/flutter_tools/lib/src/asset.dart b/packages/flutter_tools/lib/src/asset.dart
index 0915d17..44194d6 100644
--- a/packages/flutter_tools/lib/src/asset.dart
+++ b/packages/flutter_tools/lib/src/asset.dart
@@ -21,9 +21,9 @@
 /// Injected factory class for spawning [AssetBundle] instances.
 abstract class AssetBundleFactory {
   /// The singleton instance, pulled from the [AppContext].
-  static AssetBundleFactory get instance => context == null
-      ? _kManifestFactory
-      : context.putIfAbsent(AssetBundleFactory, () => _kManifestFactory);
+  static AssetBundleFactory get instance => context[AssetBundleFactory];
+
+  static AssetBundleFactory get defaultInstance => _kManifestFactory;
 
   /// Creates a new [AssetBundle].
   AssetBundle createBundle();
diff --git a/packages/flutter_tools/lib/src/base/build.dart b/packages/flutter_tools/lib/src/base/build.dart
index 2f70774..57ab3b9 100644
--- a/packages/flutter_tools/lib/src/base/build.dart
+++ b/packages/flutter_tools/lib/src/base/build.dart
@@ -17,7 +17,7 @@
 import 'file_system.dart';
 import 'process.dart';
 
-GenSnapshot get genSnapshot => context.putIfAbsent(GenSnapshot, () => const GenSnapshot());
+GenSnapshot get genSnapshot => context[GenSnapshot];
 
 /// A snapshot build configuration.
 class SnapshotType {
diff --git a/packages/flutter_tools/lib/src/base/context.dart b/packages/flutter_tools/lib/src/base/context.dart
index 1b488e8..459adc4 100644
--- a/packages/flutter_tools/lib/src/base/context.dart
+++ b/packages/flutter_tools/lib/src/base/context.dart
@@ -3,79 +3,182 @@
 // found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:collection';
 
-typedef void ErrorHandler(dynamic error, StackTrace stackTrace);
+import 'package:meta/meta.dart';
 
-/// A singleton for application functionality. This singleton can be different
-/// on a per-Zone basis.
-AppContext get context => Zone.current['context'];
+/// Generates an [AppContext] value.
+///
+/// Generators are allowed to return `null`, in which case the context will
+/// store the `null` value as the value for that type.
+typedef dynamic Generator();
 
+/// An exception thrown by [AppContext] when you try to get a [Type] value from
+/// the context, and the instantiation of the value results in a dependency
+/// cycle.
+class ContextDependencyCycleException implements Exception {
+  ContextDependencyCycleException._(this.cycle);
+
+  /// The dependency cycle (last item depends on first item).
+  final List<Type> cycle;
+
+  @override
+  String toString() => 'Dependency cycle detected: ${cycle.join(' -> ')}';
+}
+
+/// The current [AppContext], as determined by the [Zone] hierarchy.
+///
+/// This will be the first context found as we scan up the zone hierarchy, or
+/// the "root" context if a context cannot be found in the hierarchy. The root
+/// context will not have any values associated with it.
+///
+/// This is guaranteed to never return `null`.
+AppContext get context => Zone.current[_Key.key] ?? AppContext._root;
+
+/// A lookup table (mapping types to values) and an implied scope, in which
+/// code is run.
+///
+/// [AppContext] is used to define a singleton injection context for code that
+/// is run within it. Each time you call [run], a child context (and a new
+/// scope) is created.
+///
+/// Child contexts are created and run using zones. To read more about how
+/// zones work, see https://www.dartlang.org/articles/libraries/zones.
 class AppContext {
-  final Map<Type, dynamic> _instances = <Type, dynamic>{};
-  Zone _zone;
+  AppContext._(
+    this._parent,
+    this.name, [
+    this._overrides = const <Type, Generator>{},
+    this._fallbacks = const <Type, Generator>{},
+  ]);
 
-  AppContext() : _zone = Zone.current;
+  final String name;
+  final AppContext _parent;
+  final Map<Type, Generator> _overrides;
+  final Map<Type, Generator> _fallbacks;
+  final Map<Type, dynamic> _values = <Type, dynamic>{};
 
-  bool isSet(Type type) {
-    if (_instances.containsKey(type))
-      return true;
+  List<Type> _reentrantChecks;
 
-    final AppContext parent = _calcParent(_zone);
-    return parent != null ? parent.isSet(type) : false;
-  }
+  /// Bootstrap context.
+  static final AppContext _root = new AppContext._(null, 'ROOT');
 
-  dynamic getVariable(Type type) {
-    if (_instances.containsKey(type))
-      return _instances[type];
+  dynamic _boxNull(dynamic value) => value ?? _BoxedNull.instance;
 
-    final AppContext parent = _calcParent(_zone);
-    return parent?.getVariable(type);
-  }
+  dynamic _unboxNull(dynamic value) => value == _BoxedNull.instance ? null : value;
 
-  void setVariable(Type type, dynamic instance) {
-    _instances[type] = instance;
-  }
-
-  dynamic operator[](Type type) => getVariable(type);
-
-  dynamic putIfAbsent(Type type, dynamic ifAbsent()) {
-    dynamic value = getVariable(type);
-    if (value != null) {
-      return value;
-    }
-    value = ifAbsent();
-    setVariable(type, value);
-    return value;
-  }
-
-  AppContext _calcParent(Zone zone) {
-    final Zone parentZone = zone.parent;
-    if (parentZone == null)
+  /// Returns the generated value for [type] if such a generator exists.
+  ///
+  /// If [generators] does not contain a mapping for the specified [type], this
+  /// returns `null`.
+  ///
+  /// If a generator existed and generated a `null` value, this will return a
+  /// boxed value indicating null.
+  ///
+  /// If a value for [type] has already been generated by this context, the
+  /// existing value will be returned, and the generator will not be invoked.
+  ///
+  /// If the generator ends up triggering a reentrant call, it signals a
+  /// dependency cycle, and a [ContextDependencyCycleException] will be thrown.
+  dynamic _generateIfNecessary(Type type, Map<Type, Generator> generators) {
+    if (!generators.containsKey(type))
       return null;
 
-    final AppContext parentContext = parentZone['context'];
-    return parentContext == this
-        ? _calcParent(parentZone)
-        : parentContext;
+    return _values.putIfAbsent(type, () {
+      _reentrantChecks ??= <Type>[];
+
+      final int index = _reentrantChecks.indexOf(type);
+      if (index >= 0) {
+        // We're already in the process of trying to generate this type.
+        throw new ContextDependencyCycleException._(
+            new UnmodifiableListView<Type>(_reentrantChecks.sublist(index)));
+      }
+
+      _reentrantChecks.add(type);
+      try {
+        return _boxNull(generators[type]());
+      } finally {
+        _reentrantChecks.removeLast();
+        if (_reentrantChecks.isEmpty)
+          _reentrantChecks = null;
+      }
+    });
   }
 
-  Future<dynamic> runInZone(dynamic method(), {
-    ZoneBinaryCallback<dynamic, dynamic, StackTrace> onError
+  /// Gets the value associated with the specified [type], or `null` if no
+  /// such value has been associated.
+  dynamic operator [](Type type) {
+    dynamic value = _generateIfNecessary(type, _overrides);
+    if (value == null && _parent != null)
+      value = _parent[type];
+    return _unboxNull(value ?? _generateIfNecessary(type, _fallbacks));
+  }
+
+  /// Runs [body] in a child context and returns the value returned by [body].
+  ///
+  /// If [overrides] is specified, the child context will return corresponding
+  /// values when consulted via [operator[]].
+  ///
+  /// If [fallbacks] is specified, the child context will return corresponding
+  /// values when consulted via [operator[]] only if its parent context didn't
+  /// return such a value.
+  ///
+  /// If [name] is specified, the child context will be assigned the given
+  /// name. This is useful for debugging purposes and is analogous to naming a
+  /// thread in Java.
+  V run<V>({
+    @required V body(),
+    String name,
+    Map<Type, Generator> overrides,
+    Map<Type, Generator> fallbacks,
   }) {
-    return runZoned(
-      () => _run(method),
-      zoneValues: <String, dynamic>{ 'context': this },
-      onError: onError
+    final AppContext child = new AppContext._(
+      this,
+      name,
+      new Map<Type, Generator>.unmodifiable(overrides ?? const <Type, Generator>{}),
+      new Map<Type, Generator>.unmodifiable(fallbacks ?? const <Type, Generator>{}),
+    );
+    return runZoned<V>(
+      body,
+      zoneValues: <_Key, AppContext>{_Key.key: child},
     );
   }
 
-  Future<dynamic> _run(dynamic method()) async {
-    final Zone previousZone = _zone;
-    try {
-      _zone = Zone.current;
-      return await method();
-    } finally {
-      _zone = previousZone;
+  @override
+  String toString() {
+    final StringBuffer buf = new StringBuffer();
+    String indent = '';
+    AppContext ctx = this;
+    while (ctx != null) {
+      buf.write('AppContext');
+      if (ctx.name != null)
+        buf.write('[${ctx.name}]');
+      if (ctx._overrides.isNotEmpty)
+        buf.write('\n$indent  overrides: [${ctx._overrides.keys.join(', ')}]');
+      if (ctx._fallbacks.isNotEmpty)
+        buf.write('\n$indent  fallbacks: [${ctx._fallbacks.keys.join(', ')}]');
+      if (ctx._parent != null)
+        buf.write('\n$indent  parent: ');
+      ctx = ctx._parent;
+      indent += '  ';
     }
+    return buf.toString();
   }
 }
+
+/// Private key used to store the [AppContext] in the [Zone].
+class _Key {
+  const _Key();
+
+  static const _Key key = const _Key();
+
+  @override
+  String toString() => 'context';
+}
+
+/// Private object that denotes a generated `null` value.
+class _BoxedNull {
+  const _BoxedNull();
+
+  static const _BoxedNull instance = const _BoxedNull();
+}
diff --git a/packages/flutter_tools/lib/src/base/file_system.dart b/packages/flutter_tools/lib/src/base/file_system.dart
index 736fff4..74fa9dc 100644
--- a/packages/flutter_tools/lib/src/base/file_system.dart
+++ b/packages/flutter_tools/lib/src/base/file_system.dart
@@ -22,19 +22,15 @@
 ///
 /// By default it uses local disk-based implementation. Override this in tests
 /// with [MemoryFileSystem].
-FileSystem get fs => context == null ? _kLocalFs : context[FileSystem];
+FileSystem get fs => context[FileSystem] ?? _kLocalFs;
 
-/// Enables recording of file system activity to the specified base recording
-/// [location].
-///
-/// This sets the [active file system](fs) to one that records all invocation
-/// activity before delegating to a [LocalFileSystem].
+/// Gets a [FileSystem] that will record file system activity to the specified
+/// base recording [location].
 ///
 /// Activity will be recorded in a subdirectory of [location] named `"file"`.
 /// It is permissible for [location] to represent an existing non-empty
 /// directory as long as there is no collision with the `"file"` subdirectory.
-void enableRecordingFileSystem(String location) {
-  final FileSystem originalFileSystem = fs;
+RecordingFileSystem getRecordingFileSystem(String location) {
   final Directory dir = getRecordingSink(location, _kRecordingType);
   final RecordingFileSystem fileSystem = new RecordingFileSystem(
       delegate: _kLocalFs, destination: dir);
@@ -42,22 +38,19 @@
     await fileSystem.recording.flush(
       pendingResultTimeout: const Duration(seconds: 5),
     );
-    context.setVariable(FileSystem, originalFileSystem);
   }, ShutdownStage.SERIALIZE_RECORDING);
-  context.setVariable(FileSystem, fileSystem);
+  return fileSystem;
 }
 
-/// Enables file system replay mode.
-///
-/// This sets the [active file system](fs) to one that replays invocation
-/// activity from a previously recorded set of invocations.
+/// Gets a [FileSystem] that replays invocation activity from a previously
+/// recorded set of invocations.
 ///
 /// [location] must represent a directory to which file system activity has
 /// been recorded (i.e. the result of having been previously passed to
-/// [enableRecordingFileSystem]), or a [ToolExit] will be thrown.
-void enableReplayFileSystem(String location) {
+/// [getRecordingFileSystem]), or a [ToolExit] will be thrown.
+ReplayFileSystem getReplayFileSystem(String location) {
   final Directory dir = getReplaySource(location, _kRecordingType);
-  context.setVariable(FileSystem, new ReplayFileSystem(recording: dir));
+  return new ReplayFileSystem(recording: dir);
 }
 
 /// Create the ancestor directories of a file path if they do not already exist.
diff --git a/packages/flutter_tools/lib/src/base/flags.dart b/packages/flutter_tools/lib/src/base/flags.dart
index 429b03c..81442a9 100644
--- a/packages/flutter_tools/lib/src/base/flags.dart
+++ b/packages/flutter_tools/lib/src/base/flags.dart
@@ -8,7 +8,7 @@
 
 /// command-line flags and options that were specified during the invocation of
 /// the Flutter tool.
-Flags get flags => context?.getVariable(Flags) ?? const _EmptyFlags();
+Flags get flags => context[Flags];
 
 /// Encapsulation of the command-line flags and options that were specified
 /// during the invocation of the Flutter tool.
@@ -52,8 +52,8 @@
   }
 }
 
-class _EmptyFlags implements Flags {
-  const _EmptyFlags();
+class EmptyFlags implements Flags {
+  const EmptyFlags();
 
   @override
   ArgResults get _globalResults => null;
diff --git a/packages/flutter_tools/lib/src/base/io.dart b/packages/flutter_tools/lib/src/base/io.dart
index 326c487..5500d2a 100644
--- a/packages/flutter_tools/lib/src/base/io.dart
+++ b/packages/flutter_tools/lib/src/base/io.dart
@@ -155,23 +155,8 @@
   io.IOSink get stderr => io.stderr;
 }
 
-io.IOSink get stderr {
-  if (context == null)
-    return io.stderr;
-  final Stdio contextStreams = context[Stdio];
-  return contextStreams.stderr;
-}
+io.IOSink get stderr => context[Stdio].stderr;
 
-Stream<List<int>> get stdin {
-  if (context == null)
-    return io.stdin;
-  final Stdio contextStreams = context[Stdio];
-  return contextStreams.stdin;
-}
+Stream<List<int>> get stdin => context[Stdio].stdin;
 
-io.IOSink get stdout {
-  if (context == null)
-    return io.stdout;
-  final Stdio contextStreams = context[Stdio];
-  return contextStreams.stdout;
-}
+io.IOSink get stdout => context[Stdio].stdout;
diff --git a/packages/flutter_tools/lib/src/base/os.dart b/packages/flutter_tools/lib/src/base/os.dart
index 0d7bb12..ea844cb 100644
--- a/packages/flutter_tools/lib/src/base/os.dart
+++ b/packages/flutter_tools/lib/src/base/os.dart
@@ -11,7 +11,7 @@
 import 'process_manager.dart';
 
 /// Returns [OperatingSystemUtils] active in the current app context (i.e. zone).
-OperatingSystemUtils get os => context.putIfAbsent(OperatingSystemUtils, () => new OperatingSystemUtils());
+OperatingSystemUtils get os => context[OperatingSystemUtils];
 
 abstract class OperatingSystemUtils {
   factory OperatingSystemUtils() {
diff --git a/packages/flutter_tools/lib/src/base/platform.dart b/packages/flutter_tools/lib/src/base/platform.dart
index dd96240..c781499 100644
--- a/packages/flutter_tools/lib/src/base/platform.dart
+++ b/packages/flutter_tools/lib/src/base/platform.dart
@@ -14,26 +14,29 @@
 const Platform _kLocalPlatform = const LocalPlatform();
 const String _kRecordingType = 'platform';
 
-Platform get platform => context == null ? _kLocalPlatform : context[Platform];
+Platform get platform => context[Platform] ?? _kLocalPlatform;
 
-/// Enables serialization of the current [platform] to the specified base
-/// recording [location].
+/// Serializes the current [platform] to the specified base recording
+/// [location].
 ///
 /// Platform metadata will be recorded in a subdirectory of [location] named
 /// `"platform"`. It is permissible for [location] to represent an existing
 /// non-empty directory as long as there is no collision with the `"platform"`
 /// subdirectory.
-Future<Null> enableRecordingPlatform(String location) async {
+///
+/// Returns the existing platform.
+Future<Platform> getRecordingPlatform(String location) async {
   final Directory dir = getRecordingSink(location, _kRecordingType);
   final File file = _getPlatformManifest(dir);
   await file.writeAsString(platform.toJson(), flush: true);
+  return platform;
 }
 
-Future<Null> enableReplayPlatform(String location) async {
+Future<FakePlatform> getReplayPlatform(String location) async {
   final Directory dir = getReplaySource(location, _kRecordingType);
   final File file = _getPlatformManifest(dir);
   final String json = await file.readAsString();
-  context.setVariable(Platform, new FakePlatform.fromJson(json));
+  return new FakePlatform.fromJson(json);
 }
 
 File _getPlatformManifest(Directory dir) {
diff --git a/packages/flutter_tools/lib/src/base/port_scanner.dart b/packages/flutter_tools/lib/src/base/port_scanner.dart
index e1c0a45..a3e7167 100644
--- a/packages/flutter_tools/lib/src/base/port_scanner.dart
+++ b/packages/flutter_tools/lib/src/base/port_scanner.dart
@@ -7,14 +7,9 @@
 import 'context.dart';
 import 'io.dart';
 
-const PortScanner _kLocalPortScanner = const HostPortScanner();
 const int _kMaxSearchIterations = 20;
 
-PortScanner get portScanner {
-  return context == null
-      ? _kLocalPortScanner
-      : context.putIfAbsent(PortScanner, () => _kLocalPortScanner);
-}
+PortScanner get portScanner => context[PortScanner];
 
 abstract class PortScanner {
   const PortScanner();
diff --git a/packages/flutter_tools/lib/src/base/process_manager.dart b/packages/flutter_tools/lib/src/base/process_manager.dart
index bb39578..18b5703 100644
--- a/packages/flutter_tools/lib/src/base/process_manager.dart
+++ b/packages/flutter_tools/lib/src/base/process_manager.dart
@@ -16,43 +16,32 @@
 const ProcessManager _kLocalProcessManager = const LocalProcessManager();
 
 /// The active process manager.
-ProcessManager get processManager {
-  return context == null
-      ? _kLocalProcessManager
-      : context[ProcessManager];
-}
+ProcessManager get processManager => context[ProcessManager] ?? _kLocalProcessManager;
 
-/// Enables recording of process invocation activity to the specified base
-/// recording [location].
-///
-/// This sets the [active process manager](processManager) to one that records
-/// all process activity before delegating to a [LocalProcessManager].
+/// Gets a [ProcessManager] that will record process invocation activity to the
+/// specified base recording [location].
 ///
 /// Activity will be recorded in a subdirectory of [location] named `"process"`.
 /// It is permissible for [location] to represent an existing non-empty
 /// directory as long as there is no collision with the `"process"`
 /// subdirectory.
-void enableRecordingProcessManager(String location) {
-  final ProcessManager originalProcessManager = processManager;
+RecordingProcessManager getRecordingProcessManager(String location) {
   final Directory dir = getRecordingSink(location, _kRecordingType);
   const ProcessManager delegate = const LocalProcessManager();
   final RecordingProcessManager manager = new RecordingProcessManager(delegate, dir);
   addShutdownHook(() async {
     await manager.flush(finishRunningProcesses: true);
-    context.setVariable(ProcessManager, originalProcessManager);
   }, ShutdownStage.SERIALIZE_RECORDING);
-  context.setVariable(ProcessManager, manager);
+  return manager;
 }
 
-/// Enables process invocation replay mode.
-///
-/// This sets the [active process manager](processManager) to one that replays
-/// process activity from a previously recorded set of invocations.
+/// Gets a [ProcessManager] that replays process activity from a previously
+/// recorded set of invocations.
 ///
 /// [location] must represent a directory to which process activity has been
 /// recorded (i.e. the result of having been previously passed to
-/// [enableRecordingProcessManager]), or a [ToolExit] will be thrown.
-Future<Null> enableReplayProcessManager(String location) async {
+/// [getRecordingProcessManager]), or a [ToolExit] will be thrown.
+Future<ReplayProcessManager> getReplayProcessManager(String location) async {
   final Directory dir = getReplaySource(location, _kRecordingType);
 
   ProcessManager manager;
@@ -69,5 +58,5 @@
     throwToolExit('Invalid replay-from: $error');
   }
 
-  context.setVariable(ProcessManager, manager);
+  return manager;
 }
diff --git a/packages/flutter_tools/lib/src/base/utils.dart b/packages/flutter_tools/lib/src/base/utils.dart
index f980e19..735ab94 100644
--- a/packages/flutter_tools/lib/src/base/utils.dart
+++ b/packages/flutter_tools/lib/src/base/utils.dart
@@ -45,7 +45,7 @@
 }
 
 bool get isRunningOnBot {
-  final BotDetector botDetector = context?.getVariable(BotDetector) ?? _kBotDetector;
+  final BotDetector botDetector = context[BotDetector] ?? _kBotDetector;
   return botDetector.isRunningOnBot;
 }
 
@@ -231,7 +231,7 @@
       value.toRadixString(16).padLeft(count, '0');
 }
 
-Clock get clock => context.putIfAbsent(Clock, () => const Clock());
+Clock get clock => context[Clock];
 
 typedef Future<Null> AsyncCallback();
 
diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart
index cad13b3..4cd9bb2 100644
--- a/packages/flutter_tools/lib/src/commands/daemon.dart
+++ b/packages/flutter_tools/lib/src/commands/daemon.dart
@@ -50,21 +50,24 @@
   Future<Null> runCommand() {
     printStatus('Starting device daemon...');
 
-    final AppContext appContext = new AppContext();
     final NotifyingLogger notifyingLogger = new NotifyingLogger();
-    appContext.setVariable(Logger, notifyingLogger);
 
     Cache.releaseLockEarly();
 
-    return appContext.runInZone(() async {
-      final Daemon daemon = new Daemon(
-          stdinCommandStream, stdoutCommandResponse,
-          daemonCommand: this, notifyingLogger: notifyingLogger);
+    return context.run<Future<Null>>(
+      body: () async {
+        final Daemon daemon = new Daemon(
+            stdinCommandStream, stdoutCommandResponse,
+            daemonCommand: this, notifyingLogger: notifyingLogger);
 
-      final int code = await daemon.onExit;
-      if (code != 0)
-        throwToolExit('Daemon exited with non-zero exit code: $code', exitCode: code);
-    });
+        final int code = await daemon.onExit;
+        if (code != 0)
+          throwToolExit('Daemon exited with non-zero exit code: $code', exitCode: code);
+      },
+      overrides: <Type, Generator>{
+        Logger: () => notifyingLogger,
+      },
+    );
   }
 }
 
@@ -810,10 +813,12 @@
   dynamic _runInZone(AppDomain domain, dynamic method()) {
     _logger ??= new _AppRunLogger(domain, this, parent: logToStdout ? logger : null);
 
-    final AppContext appContext = new AppContext();
-    appContext.setVariable(Logger, _logger);
-    appContext.setVariable(Stdio, const Stdio());
-    return appContext.runInZone(method);
+    return context.run<dynamic>(
+      body: method,
+      overrides: <Type, Generator>{
+        Logger: () => _logger,
+      },
+    );
   }
 }
 
diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart
index 5d8bc67..9abaec3 100644
--- a/packages/flutter_tools/lib/src/context_runner.dart
+++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -4,38 +4,73 @@
 
 import 'dart:async';
 
-import 'package:process/process.dart';
+import 'package:quiver/time.dart';
 
+import 'android/android_sdk.dart';
+import 'android/android_studio.dart';
+import 'android/android_workflow.dart';
+import 'artifacts.dart';
+import 'asset.dart';
+import 'base/build.dart';
 import 'base/config.dart';
 import 'base/context.dart';
-import 'base/file_system.dart';
+import 'base/flags.dart';
 import 'base/io.dart';
 import 'base/logger.dart';
 import 'base/os.dart';
 import 'base/platform.dart';
+import 'base/port_scanner.dart';
 import 'base/utils.dart';
 import 'cache.dart';
-import 'disabled_usage.dart';
+import 'devfs.dart';
+import 'device.dart';
+import 'doctor.dart';
+import 'ios/cocoapods.dart';
+import 'ios/ios_workflow.dart';
+import 'ios/mac.dart';
+import 'ios/simulators.dart';
+import 'ios/xcodeproj.dart';
+import 'run_hot.dart';
 import 'usage.dart';
+import 'version.dart';
 
-typedef Future<Null> Runner(List<String> args);
-
-Future<Null> runInContext(List<String> args, Runner runner) {
-  final AppContext executableContext = new AppContext();
-  executableContext.setVariable(Logger, new StdoutLogger());
-  return executableContext.runInZone(() {
-    // Initialize the context with some defaults.
-    // This list must be kept in sync with lib/executable.dart.
-    context.putIfAbsent(BotDetector, () => const BotDetector());
-    context.putIfAbsent(Stdio, () => const Stdio());
-    context.putIfAbsent(Platform, () => const LocalPlatform());
-    context.putIfAbsent(FileSystem, () => const LocalFileSystem());
-    context.putIfAbsent(ProcessManager, () => const LocalProcessManager());
-    context.putIfAbsent(Logger, () => new StdoutLogger());
-    context.putIfAbsent(Cache, () => new Cache());
-    context.putIfAbsent(Config, () => new Config());
-    context.putIfAbsent(OperatingSystemUtils, () => new OperatingSystemUtils());
-    context.putIfAbsent(Usage, () => new DisabledUsage());
-    return runner(args);
-  });
+Future<T> runInContext<T>(
+  FutureOr<T> runner(), {
+  Map<Type, dynamic> overrides,
+}) async {
+  return await context.run<Future<T>>(
+    name: 'global fallbacks',
+    body: () async => await runner(),
+    overrides: overrides,
+    fallbacks: <Type, Generator>{
+      AndroidSdk: AndroidSdk.locateAndroidSdk,
+      AndroidStudio: AndroidStudio.latestValid,
+      AndroidWorkflow: () => new AndroidWorkflow(),
+      Artifacts: () => new CachedArtifacts(),
+      AssetBundleFactory: () => AssetBundleFactory.defaultInstance,
+      BotDetector: () => const BotDetector(),
+      Cache: () => new Cache(),
+      Clock: () => const Clock(),
+      CocoaPods: () => const CocoaPods(),
+      Config: () => new Config(),
+      DevFSConfig: () => new DevFSConfig(),
+      DeviceManager: () => new DeviceManager(),
+      Doctor: () => new Doctor(),
+      Flags: () => const EmptyFlags(),
+      FlutterVersion: () => new FlutterVersion(const Clock()),
+      GenSnapshot: () => const GenSnapshot(),
+      HotRunnerConfig: () => new HotRunnerConfig(),
+      IMobileDevice: () => const IMobileDevice(),
+      IOSSimulatorUtils: () => new IOSSimulatorUtils(),
+      IOSWorkflow: () => const IOSWorkflow(),
+      Logger: () => platform.isWindows ? new WindowsStdoutLogger() : new StdoutLogger(),
+      OperatingSystemUtils: () => new OperatingSystemUtils(),
+      PortScanner: () => const HostPortScanner(),
+      SimControl: () => new SimControl(),
+      Stdio: () => const Stdio(),
+      Usage: () => new Usage(),
+      Xcode: () => new Xcode(),
+      XcodeProjectInterpreter: () => new XcodeProjectInterpreter(),
+    },
+  );
 }
diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart
index 2e1ca55..1a70543 100644
--- a/packages/flutter_tools/lib/src/doctor.dart
+++ b/packages/flutter_tools/lib/src/doctor.dart
@@ -197,7 +197,7 @@
 }
 
 abstract class DoctorValidator {
-  DoctorValidator(this.title);
+  const DoctorValidator(this.title);
 
   final String title;
 
diff --git a/packages/flutter_tools/lib/src/ios/cocoapods.dart b/packages/flutter_tools/lib/src/ios/cocoapods.dart
index e020c17..1307f00 100644
--- a/packages/flutter_tools/lib/src/ios/cocoapods.dart
+++ b/packages/flutter_tools/lib/src/ios/cocoapods.dart
@@ -31,7 +31,7 @@
   brew upgrade cocoapods
   pod setup''';
 
-CocoaPods get cocoaPods => context.putIfAbsent(CocoaPods, () => const CocoaPods());
+CocoaPods get cocoaPods => context[CocoaPods];
 
 class CocoaPods {
   const CocoaPods();
diff --git a/packages/flutter_tools/lib/src/ios/ios_workflow.dart b/packages/flutter_tools/lib/src/ios/ios_workflow.dart
index d561ff0..c44ce74 100644
--- a/packages/flutter_tools/lib/src/ios/ios_workflow.dart
+++ b/packages/flutter_tools/lib/src/ios/ios_workflow.dart
@@ -13,10 +13,10 @@
 import 'cocoapods.dart';
 import 'mac.dart';
 
-IOSWorkflow get iosWorkflow => context.putIfAbsent(IOSWorkflow, () => new IOSWorkflow());
+IOSWorkflow get iosWorkflow => context[IOSWorkflow];
 
 class IOSWorkflow extends DoctorValidator implements Workflow {
-  IOSWorkflow() : super('iOS toolchain - develop for iOS devices');
+  const IOSWorkflow() : super('iOS toolchain - develop for iOS devices');
 
   @override
   bool get appliesToHostPlatform => platform.isMacOS;
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index 89497eb..7356716 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -35,9 +35,9 @@
 // Homebrew.
 const PythonModule kPythonSix = const PythonModule('six');
 
-IMobileDevice get iMobileDevice => context.putIfAbsent(IMobileDevice, () => const IMobileDevice());
+IMobileDevice get iMobileDevice => context[IMobileDevice];
 
-Xcode get xcode => context.putIfAbsent(Xcode, () => new Xcode());
+Xcode get xcode => context[Xcode];
 
 class PythonModule {
   const PythonModule(this.name);
diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
index e35d6aa..0144a2e 100644
--- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart
+++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
@@ -85,10 +85,7 @@
   localsFile.writeAsStringSync(localsBuffer.toString());
 }
 
-XcodeProjectInterpreter get xcodeProjectInterpreter => context.putIfAbsent(
-  XcodeProjectInterpreter,
-  () => new XcodeProjectInterpreter(),
-);
+XcodeProjectInterpreter get xcodeProjectInterpreter => context[XcodeProjectInterpreter];
 
 /// Interpreter of Xcode projects.
 class XcodeProjectInterpreter {
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index c65e9a7..bd9ca25 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -222,46 +222,49 @@
   /// and [runCommand] to execute the command
   /// so that this method can record and report the overall time to analytics.
   @override
-  Future<Null> run() async {
+  Future<Null> run() {
     final DateTime startTime = clock.now();
 
-    context.setVariable(FlutterCommand, this);
+    return context.run<Future<Null>>(
+      name: 'command',
+      overrides: <Type, Generator>{FlutterCommand: () => this},
+      body: () async {
+        if (flutterUsage.isFirstRun)
+          flutterUsage.printWelcome();
 
-    if (flutterUsage.isFirstRun)
-      flutterUsage.printWelcome();
+        FlutterCommandResult commandResult;
+        try {
+          commandResult = await verifyThenRunCommand();
+        } on ToolExit {
+          commandResult = const FlutterCommandResult(ExitStatus.fail);
+          rethrow;
+        } finally {
+          final DateTime endTime = clock.now();
+          printTrace('"flutter $name" took ${getElapsedAsMilliseconds(endTime.difference(startTime))}.');
+          if (usagePath != null) {
+            final List<String> labels = <String>[];
+            if (commandResult?.exitStatus != null)
+              labels.add(getEnumName(commandResult.exitStatus));
+            if (commandResult?.timingLabelParts?.isNotEmpty ?? false)
+              labels.addAll(commandResult.timingLabelParts);
 
-    FlutterCommandResult commandResult;
-    try {
-      commandResult = await verifyThenRunCommand();
-    } on ToolExit {
-      commandResult = const FlutterCommandResult(ExitStatus.fail);
-      rethrow;
-    } finally {
-      final DateTime endTime = clock.now();
-      printTrace('"flutter $name" took ${getElapsedAsMilliseconds(endTime.difference(startTime))}.');
-      if (usagePath != null) {
-        final List<String> labels = <String>[];
-        if (commandResult?.exitStatus != null)
-          labels.add(getEnumName(commandResult.exitStatus));
-        if (commandResult?.timingLabelParts?.isNotEmpty ?? false)
-          labels.addAll(commandResult.timingLabelParts);
-
-        final String label = labels
-            .where((String label) => !isBlank(label))
-            .join('-');
-        flutterUsage.sendTiming(
-          'flutter',
-          name,
-          // If the command provides its own end time, use it. Otherwise report
-          // the duration of the entire execution.
-          (commandResult?.endTimeOverride ?? endTime).difference(startTime),
-          // Report in the form of `success-[parameter1-parameter2]`, all of which
-          // can be null if the command doesn't provide a FlutterCommandResult.
-          label: label == '' ? null : label,
-        );
-      }
-    }
-
+            final String label = labels
+                .where((String label) => !isBlank(label))
+                .join('-');
+            flutterUsage.sendTiming(
+              'flutter',
+              name,
+              // If the command provides its own end time, use it. Otherwise report
+              // the duration of the entire execution.
+              (commandResult?.endTimeOverride ?? endTime).difference(startTime),
+              // Report in the form of `success-[parameter1-parameter2]`, all of which
+              // can be null if the command doesn't provide a FlutterCommandResult.
+              label: label == '' ? null : label,
+            );
+          }
+        }
+      },
+    );
   }
 
   /// Perform validation then call [runCommand] to execute the command.
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
index 14b3ac4..940309a 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart
@@ -7,8 +7,10 @@
 
 import 'package:args/args.dart';
 import 'package:args/command_runner.dart';
+import 'package:file/file.dart';
+import 'package:platform/platform.dart';
+import 'package:process/process.dart';
 
-import '../android/android_sdk.dart';
 import '../artifacts.dart';
 import '../base/common.dart';
 import '../base/context.dart';
@@ -168,12 +170,14 @@
 
   @override
   Future<Null> runCommand(ArgResults topLevelResults) async {
-    context.setVariable(Flags, new Flags(topLevelResults));
+    final Map<Type, dynamic> contextOverrides = <Type, dynamic>{
+      Flags: new Flags(topLevelResults),
+    };
 
     // Check for verbose.
     if (topLevelResults['verbose']) {
       // Override the logger.
-      context.setVariable(Logger, new VerboseLogger(context[Logger]));
+      contextOverrides[Logger] = new VerboseLogger(logger);
     }
 
     String recordTo = topLevelResults['record-to'];
@@ -214,9 +218,11 @@
       recordTo = recordTo.trim();
       if (recordTo.isEmpty)
         throwToolExit('record-to location not specified');
-      enableRecordingProcessManager(recordTo);
-      enableRecordingFileSystem(recordTo);
-      await enableRecordingPlatform(recordTo);
+      contextOverrides.addAll(<Type, dynamic>{
+        ProcessManager: getRecordingProcessManager(recordTo),
+        FileSystem: getRecordingFileSystem(recordTo),
+        Platform: await getRecordingPlatform(recordTo),
+      });
       VMService.enableRecordingConnection(recordTo);
     }
 
@@ -224,66 +230,74 @@
       replayFrom = replayFrom.trim();
       if (replayFrom.isEmpty)
         throwToolExit('replay-from location not specified');
-      await enableReplayProcessManager(replayFrom);
-      enableReplayFileSystem(replayFrom);
-      await enableReplayPlatform(replayFrom);
+      contextOverrides.addAll(<Type, dynamic>{
+        ProcessManager: await getReplayProcessManager(replayFrom),
+        FileSystem: getReplayFileSystem(replayFrom),
+        Platform: await getReplayPlatform(replayFrom),
+      });
       VMService.enableReplayConnection(replayFrom);
     }
 
-    logger.quiet = topLevelResults['quiet'];
-
-    if (topLevelResults.wasParsed('color'))
-      logger.supportsColor = topLevelResults['color'];
-
-    // We must set Cache.flutterRoot early because other features use it (e.g.
-    // enginePath's initializer uses it).
-    final String flutterRoot = topLevelResults['flutter-root'] ?? _defaultFlutterRoot;
-    Cache.flutterRoot = fs.path.normalize(fs.path.absolute(flutterRoot));
-
-    if (platform.environment['FLUTTER_ALREADY_LOCKED'] != 'true')
-      await Cache.lock();
-
-    if (topLevelResults['suppress-analytics'])
-      flutterUsage.suppressAnalytics = true;
-
-    _checkFlutterCopy();
-    await FlutterVersion.instance.ensureVersionFile();
-    if (topLevelResults.command?.name != 'upgrade') {
-      await FlutterVersion.instance.checkFlutterVersionFreshness();
-    }
-
-    if (topLevelResults.wasParsed('packages'))
-      PackageMap.globalPackagesPath = fs.path.normalize(fs.path.absolute(topLevelResults['packages']));
-
-    // See if the user specified a specific device.
-    deviceManager.specifiedDeviceId = topLevelResults['device-id'];
-
     // Set up the tooling configuration.
     final String enginePath = _findEnginePath(topLevelResults);
     if (enginePath != null) {
-      Artifacts.useLocalEngine(enginePath, _findEngineBuildPath(topLevelResults, enginePath));
+      contextOverrides.addAll(<Type, dynamic>{
+        Artifacts: Artifacts.getLocalEngine(enginePath, _findEngineBuildPath(topLevelResults, enginePath)),
+      });
     }
 
-    // The Android SDK could already have been set by tests.
-    context.putIfAbsent(AndroidSdk, AndroidSdk.locateAndroidSdk);
+    await context.run<Future<Null>>(
+      overrides: contextOverrides.map<Type, Generator>((Type type, dynamic value) {
+        return new MapEntry<Type, Generator>(type, () => value);
+      }),
+      body: () async {
+        logger.quiet = topLevelResults['quiet'];
 
-    if (topLevelResults['version']) {
-      flutterUsage.sendCommand('version');
-      String status;
-      if (topLevelResults['machine']) {
-        status = const JsonEncoder.withIndent('  ').convert(FlutterVersion.instance.toJson());
-      } else {
-        status = FlutterVersion.instance.toString();
-      }
-      printStatus(status);
-      return;
-    }
+        if (topLevelResults.wasParsed('color'))
+          logger.supportsColor = topLevelResults['color'];
 
-    if (topLevelResults['machine']) {
-      throwToolExit('The --machine flag is only valid with the --version flag.', exitCode: 2);
-    }
+        // We must set Cache.flutterRoot early because other features use it (e.g.
+        // enginePath's initializer uses it).
+        final String flutterRoot = topLevelResults['flutter-root'] ?? _defaultFlutterRoot;
+        Cache.flutterRoot = fs.path.normalize(fs.path.absolute(flutterRoot));
 
-    await super.runCommand(topLevelResults);
+        if (platform.environment['FLUTTER_ALREADY_LOCKED'] != 'true')
+          await Cache.lock();
+
+        if (topLevelResults['suppress-analytics'])
+          flutterUsage.suppressAnalytics = true;
+
+        _checkFlutterCopy();
+        await FlutterVersion.instance.ensureVersionFile();
+        if (topLevelResults.command?.name != 'upgrade') {
+          await FlutterVersion.instance.checkFlutterVersionFreshness();
+        }
+
+        if (topLevelResults.wasParsed('packages'))
+          PackageMap.globalPackagesPath = fs.path.normalize(fs.path.absolute(topLevelResults['packages']));
+
+        // See if the user specified a specific device.
+        deviceManager.specifiedDeviceId = topLevelResults['device-id'];
+
+        if (topLevelResults['version']) {
+          flutterUsage.sendCommand('version');
+          String status;
+          if (topLevelResults['machine']) {
+            status = const JsonEncoder.withIndent('  ').convert(FlutterVersion.instance.toJson());
+          } else {
+            status = FlutterVersion.instance.toString();
+          }
+          printStatus(status);
+          return;
+        }
+
+        if (topLevelResults['machine']) {
+          throwToolExit('The --machine flag is only valid with the --version flag.', exitCode: 2);
+        }
+
+        await super.runCommand(topLevelResults);
+      },
+    );
   }
 
   String _tryEnginePath(String enginePath) {
diff --git a/packages/flutter_tools/lib/src/usage.dart b/packages/flutter_tools/lib/src/usage.dart
index e1efafb..1202a19 100644
--- a/packages/flutter_tools/lib/src/usage.dart
+++ b/packages/flutter_tools/lib/src/usage.dart
@@ -49,7 +49,7 @@
   }
 
   /// Returns [Usage] active in the current app context.
-  static Usage get instance => context.putIfAbsent(Usage, () => new Usage());
+  static Usage get instance => context[Usage];
 
   Analytics _analytics;
 
diff --git a/packages/flutter_tools/lib/src/version.dart b/packages/flutter_tools/lib/src/version.dart
index 30b74c0..7e88904 100644
--- a/packages/flutter_tools/lib/src/version.dart
+++ b/packages/flutter_tools/lib/src/version.dart
@@ -167,7 +167,7 @@
       await _run(<String>['git', 'remote', 'remove', _kVersionCheckRemote]);
   }
 
-  static FlutterVersion get instance => context.putIfAbsent(FlutterVersion, () => new FlutterVersion(const Clock()));
+  static FlutterVersion get instance => context[FlutterVersion];
 
   /// Return a short string for the version (e.g. `master/0.0.59-pre.92`, `scroll_refactor/a76bc8e22b`).
   String getVersionString({bool redactUnknownBranches: false}) {