Overriding

The Scope package supports the concept of overriding a scoped key.

The ability to override a scope key solves a number of common design pattern problems with DI particularly when it comes to unit testing.

At any point in your code you can introduce a new Scope that will override existing Scopes and your GlobalScope.

When calling use or Scope.use, the scope package performs a search for the passed key.

The search order is critical to understanding the overriding mechanism.

  • search for the nearest Scope on the call stack

  • search up the hierarchy of Scopes on the call stack

  • search the GlobalScope

  • check if the withDefault argument was passed to use

  • check if the key was created using ScopeKey.withDefault .

  • throw a MissingDependencyException if the key wasn't found.

The best way to understand this is via an example:

import 'package:scope/scope.dart';

final userKey = ScopeKey<User>();
final counterKey = ScopeKey<int>();

final globalUser = User('real');
final testUser = User('test');
final innerUser = User('inner');

final globalDb = Db('global_db');
final liveDb = Db('live_db');
final testDb = Db('test_db');
final dbKey = ScopeKey<Db>.withDefault(liveDb);

/// Demonstrates how the `use` method resolves overridden
/// scope keys.
void main() {
  var counter = 0;
  GlobalScope()
    ..value<User>(userKey, globalUser)
    ..sequence<int>(counterKey, () => counter++);

  /// just GlobalScope in scope
  assert(use(userKey) == globalUser, 'take from global scope');

  /// override the GlobalScope with outerscope
  Scope('outerscope')
    ..value<User>(userKey, testUser)
    ..run(() {
      assert(use(userKey) == testUser, 'take from outerscope');
      assert(use(counterKey) == 0, 'always from the GlobalScope');
    });
  assert(use(userKey) == globalUser, 'take from GlobalScope');
  assert(use(counterKey) == 1, 'always from the GlobalScope');

  /// override the GlobalScope and outerscope with
  /// innerscope
  Scope('outerscope')
    ..value<User>(userKey, testUser)
    ..run(() {
      assert(use(counterKey) == 2, 'always from the GlobalScope');
      
      Scope('innerscope')
        ..value<User>(userKey, innerUser)
        ..run(() {
          assert(use(userKey) == innerUser, 'take from innerscope');
          assert(use(counterKey) == 3, 'always from the GlobalScope');
        });

      assert(use(userKey) == testUser, 'take from outerscope');
    });

  /// we are out of all Scope's so GlobalScope back in play
  assert(use(userKey) == globalUser, 'take from GlobalScope');
  assert(use(counterKey) == 4, 'always from the GlobalScope');

  /// No key in scope; get the keys default
  assert(use(dbKey) == liveDb, 'take from the withDefault value of the dbKey');

  /// No key in scope; use the default value provided to use
  assert(use(dbKey, withDefault: () => testDb) == testDb,
      'take from the withDefault provided to use');

  /// inject a dbKey into the global scope
  GlobalScope().value<Db>(dbKey, globalDb);

  /// Global key in scope; so use it.
  assert(
      use(dbKey, withDefault: () => testDb) == globalDb, 'use the global key');

  /// override the GlobalScope with outerscope
  Scope('outerscope')
    ..value<Db>(dbKey, globalDb)
    ..run(() {
      assert(use(dbKey) == globalDb, 'take from outerscope');
      assert(use(counterKey) == 5, 'always from the GlobalScope');
    });
}

class User {
  User(this.name);
  String name;
}

class Db {
  Db(this.databaseName);

  String databaseName;
}

Last updated