context.dart 6.47 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6
import 'dart:collection';
7

8
import 'package:meta/meta.dart';
9

10
// TODO(ianh): We should remove AppContext's mechanism and replace it with
11
// passing dependencies directly in constructors, methods, etc. See #47161.
12

13 14 15 16
/// 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.
17
typedef Generator = dynamic Function();
18

19 20 21 22 23
/// 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);
24

25 26
  /// The dependency cycle (last item depends on first item).
  final List<Type> cycle;
27

28 29 30
  @override
  String toString() => 'Dependency cycle detected: ${cycle.join(' -> ')}';
}
31

32 33 34 35
/// The Zone key used to look up the [AppContext].
@visibleForTesting
const Object contextKey = _Key.key;

36 37 38 39 40 41 42
/// 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`.
43
AppContext get context => Zone.current[contextKey] as AppContext? ?? AppContext._root;
44 45 46 47 48 49 50 51 52

/// 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
53
/// zones work, see https://api.dart.dev/stable/dart-async/Zone-class.html.
54 55 56 57 58 59 60 61
class AppContext {
  AppContext._(
    this._parent,
    this.name, [
    this._overrides = const <Type, Generator>{},
    this._fallbacks = const <Type, Generator>{},
  ]);

62 63
  final String? name;
  final AppContext? _parent;
64 65 66 67
  final Map<Type, Generator> _overrides;
  final Map<Type, Generator> _fallbacks;
  final Map<Type, dynamic> _values = <Type, dynamic>{};

68
  List<Type>? _reentrantChecks;
69 70

  /// Bootstrap context.
71
  static final AppContext _root = AppContext._(null, 'ROOT');
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90

  dynamic _boxNull(dynamic value) => value ?? _BoxedNull.instance;

  dynamic _unboxNull(dynamic value) => value == _BoxedNull.instance ? null : value;

  /// 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) {
91
    if (!generators.containsKey(type)) {
92
      return null;
93
    }
94

95 96 97
    return _values.putIfAbsent(type, () {
      _reentrantChecks ??= <Type>[];

98
      final int index = _reentrantChecks!.indexOf(type);
99 100
      if (index >= 0) {
        // We're already in the process of trying to generate this type.
101
        throw ContextDependencyCycleException._(
102
            UnmodifiableListView<Type>(_reentrantChecks!.sublist(index)));
103 104
      }

105
      _reentrantChecks!.add(type);
106
      try {
107
        return _boxNull(generators[type]!());
108
      } finally {
109 110
        _reentrantChecks!.removeLast();
        if (_reentrantChecks!.isEmpty) {
111
          _reentrantChecks = null;
112
        }
113 114
      }
    });
115
  }
116

117 118
  /// Gets the value associated with the specified [type], or `null` if no
  /// such value has been associated.
119
  T? get<T>() {
120 121
    dynamic value = _generateIfNecessary(T, _overrides);
    if (value == null && _parent != null) {
122
      value = _parent.get<T>();
123
    }
124
    return _unboxNull(value ?? _generateIfNecessary(T, _fallbacks)) as T?;
125 126
  }

127 128 129 130 131 132 133 134 135 136 137 138
  /// 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.
139
  Future<V> run<V>({
140 141 142 143 144
    required FutureOr<V> Function() body,
    String? name,
    Map<Type, Generator>? overrides,
    Map<Type, Generator>? fallbacks,
    ZoneSpecification? zoneSpecification,
145
  }) async {
146
    final AppContext child = AppContext._(
147 148
      this,
      name,
149 150
      Map<Type, Generator>.unmodifiable(overrides ?? const <Type, Generator>{}),
      Map<Type, Generator>.unmodifiable(fallbacks ?? const <Type, Generator>{}),
151
    );
152
    return runZoned<Future<V>>(
153
      () async => await body(),
154
      zoneValues: <_Key, AppContext>{_Key.key: child},
155
      zoneSpecification: zoneSpecification,
156 157
    );
  }
158

159 160
  @override
  String toString() {
161
    final StringBuffer buf = StringBuffer();
162
    String indent = '';
163
    AppContext? ctx = this;
164 165
    while (ctx != null) {
      buf.write('AppContext');
166
      if (ctx.name != null) {
167
        buf.write('[${ctx.name}]');
168 169
      }
      if (ctx._overrides.isNotEmpty) {
170
        buf.write('\n$indent  overrides: [${ctx._overrides.keys.join(', ')}]');
171 172
      }
      if (ctx._fallbacks.isNotEmpty) {
173
        buf.write('\n$indent  fallbacks: [${ctx._fallbacks.keys.join(', ')}]');
174 175
      }
      if (ctx._parent != null) {
176
        buf.write('\n$indent  parent: ');
177
      }
178 179
      ctx = ctx._parent;
      indent += '  ';
180
    }
181
    return buf.toString();
182
  }
183
}
184

185 186 187
/// Private key used to store the [AppContext] in the [Zone].
class _Key {
  const _Key();
188

189
  static const _Key key = _Key();
190

191 192 193
  @override
  String toString() => 'context';
}
194

195 196 197 198
/// Private object that denotes a generated `null` value.
class _BoxedNull {
  const _BoxedNull();

199
  static const _BoxedNull instance = _BoxedNull();
200
}