// Copyright 2014 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> 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 await 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(); }