context.dart 6.32 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 11 12 13
/// 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.
14
typedef Generator = dynamic Function();
15

16 17 18 19 20
/// 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);
21

22 23
  /// The dependency cycle (last item depends on first item).
  final List<Type> cycle;
24

25 26 27
  @override
  String toString() => 'Dependency cycle detected: ${cycle.join(' -> ')}';
}
28

29 30 31 32
/// The Zone key used to look up the [AppContext].
@visibleForTesting
const Object contextKey = _Key.key;

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

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

59 60
  final String? name;
  final AppContext? _parent;
61 62 63 64
  final Map<Type, Generator> _overrides;
  final Map<Type, Generator> _fallbacks;
  final Map<Type, dynamic> _values = <Type, dynamic>{};

65
  List<Type>? _reentrantChecks;
66 67

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

  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) {
88
    if (!generators.containsKey(type)) {
89
      return null;
90
    }
91

92 93 94
    return _values.putIfAbsent(type, () {
      _reentrantChecks ??= <Type>[];

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

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

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

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

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

182 183 184
/// Private key used to store the [AppContext] in the [Zone].
class _Key {
  const _Key();
185

186
  static const _Key key = _Key();
187

188 189 190
  @override
  String toString() => 'context';
}
191

192 193 194 195
/// Private object that denotes a generated `null` value.
class _BoxedNull {
  const _BoxedNull();

196
  static const _BoxedNull instance = _BoxedNull();
197
}