blob: ea1dd4d83d7f8f41fa36c2de531a856a1752408e [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:collection';
import 'package:meta/meta.dart';
/// 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 Generator = dynamic Function();
/// 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 Zone key used to look up the [AppContext].
@visibleForTesting
const Object contextKey = _Key.key;
/// 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[contextKey] as AppContext? ?? 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://api.dart.dev/stable/dart-async/Zone-class.html.
class AppContext {
AppContext._(
this._parent,
this.name, [
this._overrides = const <Type, Generator>{},
this._fallbacks = const <Type, Generator>{},
]);
final String? name;
final AppContext? _parent;
final Map<Type, Generator> _overrides;
final Map<Type, Generator> _fallbacks;
final Map<Type, dynamic> _values = <Type, dynamic>{};
List<Type>? _reentrantChecks;
/// Bootstrap context.
static final AppContext _root = AppContext._(null, 'ROOT');
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) {
if (!generators.containsKey(type)) {
return null;
}
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 ContextDependencyCycleException._(
UnmodifiableListView<Type>(_reentrantChecks!.sublist(index)));
}
_reentrantChecks!.add(type);
try {
return _boxNull(generators[type]!());
} finally {
_reentrantChecks!.removeLast();
if (_reentrantChecks!.isEmpty) {
_reentrantChecks = null;
}
}
});
}
/// Gets the value associated with the specified [type], or `null` if no
/// such value has been associated.
T? get<T>() {
dynamic value = _generateIfNecessary(T, _overrides);
if (value == null && _parent != null) {
value = _parent!.get<T>();
}
return _unboxNull(value ?? _generateIfNecessary(T, _fallbacks)) as T?;
}
/// 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.
Future<V> run<V>({
required FutureOr<V> Function() body,
String? name,
Map<Type, Generator>? overrides,
Map<Type, Generator>? fallbacks,
ZoneSpecification? zoneSpecification,
}) async {
final AppContext child = AppContext._(
this,
name,
Map<Type, Generator>.unmodifiable(overrides ?? const <Type, Generator>{}),
Map<Type, Generator>.unmodifiable(fallbacks ?? const <Type, Generator>{}),
);
return runZoned<Future<V>>(
() async => await body(),
zoneValues: <_Key, AppContext>{_Key.key: child},
zoneSpecification: zoneSpecification,
);
}
@override
String toString() {
final StringBuffer buf = 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 = _Key();
@override
String toString() => 'context';
}
/// Private object that denotes a generated `null` value.
class _BoxedNull {
const _BoxedNull();
static const _BoxedNull instance = _BoxedNull();
}