pot 0.9.0 copy "pot: ^0.9.0" to clipboard
pot: ^0.9.0 copied to clipboard

A simple and type-safe dependency injection (DI) package, designed for easy scoping and testing.

Pub Version pot CI codecov

A simple and type-safe dependency injection (DI) package, designed for easy scoping and testing.

What is a pot? #

Pot is a single-type DI container holding an object of a particular type.

Each pot has a Singleton factory function triggered to create an object as needed. It is possible to replace the factory or remove the object in a pot at your preferred timing, which is useful for testing as well as for implementing app features.

Advantages #

  • Easy
    • Straightforward because it is specialised for DI, without other features.
    • Simple API that you can be sure how to use.
    • A pot as a global variable is easy to handle; auto-completion works in IDEs.
  • Safe
    • Not dependent on types.
      • No runtime error basically. The object always exists when it is accessed as long as the pot has a valid factory.
    • Not dependent on strings.
      • Pot does not provide a dangerous way to access the content like referencing it by type name.

Examples #

  • Pottery
    • A Flutter package with utility widgets for Pot, automatically managing the lifecycle of pots and the objects they hold based on the widget lifecycle.
    • Whether to use this is up to you. It is just an additional utility.

Usage #

Assign a pot created with a singleton factory to a global variable.

final counterPot = Pot(() => Counter(0));

Now you can use the pot in whatever file importing the above declaration.

Warning

Avoid assigning a pot to a local variable because its state will persist even after the local variable's reference is lost. Otherwise, you must always ensure that you call dispose() manually when it is no longer needed.

Getting the object #

Calling call() triggers the factory to create an object in the pot on the fly if no object has been created yet or one has already been removed. Otherwise, the existing object is returned.

The call() method is a special function in Dart that allows a class instance to be called like a function, so instead of counterPot.call(), you can write it as follows:

void main() {
  final counter = counterPot();
  ...
}

Creating an object #

Use create() if you want to instantiate an object without obtaining it.

This is practically the same as call(), except that create() does not return the created object while call() does. It has no effect if the pot already has an object.

void main() {
  counterPot.create();
  ...
}

Removing the object from a pot #

You can remove the object with several methods like reset(). The resources are saved if objects are properly removed when they become unnecessary.

Even if an object is removed, the pot itself remains. A new object is created when it is needed again.

If a callback function is passed to the disposer parameter of the constructor, it is triggered when the object is removed from the pot. Use it for doing a clean-up related to the object.

final counterPot = Pot(
  () => Counter(0),
  disposer: (counter) => counter.dispose(),
);
void main() {
  final counterA = counterPot(); // 0
  counter.increment(); // 1

  // Removes the Counter object from the pot
  // and triggers the disposer.
  counterPot.reset();

  final counterB = counterPot(); // 0

  // Disposes the pot to release resources.
  // This also triggers the disposer.
  counterPot.dispose();

  final counterC = counterPot(); // Error
}

The replace(), Pot.popScope(), Pot.resetAllInScope() and Pot.uninitialize() methods also remove existing objects. These will be explained in later sections of this document.

Advanced usage #

Replacing factory and object #

Pots created by Pot.replaceable() or Pot.pending() have the replace() method. It replaces the object factory function, which was set in the constructor of Pot, and the object held in the pot if existing.

final userPot = Pot.replaceable(() => User.none());
Future<User> signIn() async {
  final userId = await Auth.signIn(...);
  userPot.replace(() => User(id: userId));
  return userPot();
}

Note

The replace() method removes the existing object, triggering the disposer, but only if the pot has an object. It behaves differently depending on whether the object exists. See the document of replace() for details on the behaviour.

Creating a pot with no factory #

Pot.pending() is an alternative to Pot.replaceable(), useful if the object is unnecessary or the factory is unavailable until some point.

Note

PotNotReadyException occurs if the pot is used before a valid factory is set with replace().

final userPot = Pot.pending<User>();
// final user = userPot(); // PotNotReadyException

...

userPot.replace(() => User(id: userId));
final user = userPot();

It is also possible to remove the existing factory of a replaceable pot by resetAsPending() to switch the state of the pot to pending.

Replacements for testing #

If you need to replace the factory function only in tests, you may want to use a non-replaceable pot and to use replaceForTesting() instead of replace(). This helps prevent accidentally calling replace outside of tests in test-only scenarios.

The replaceForTesting method is available even on a non-replaceable pot. Using it outside of a test will be flagged by static analysis.

final counterPot = Pot(() => Counter(0)); // Non-replaceable pot
void main() {
  test('Some test', () {
    counterPot.replaceForTesting(() => Counter(100));
    ...
  });
}

Listening for events #

The static method listen() allows you to listen for events related to pots. See the document of PotEventKind for event types, such as instantiated and reset.

final removeListener = Pot.listen((event) {
  ...
});

// Don't forget to stop listening when it is no longer necessary.
removeListener();

Note

  • Events of changes in the objects held in pots are not emitted automatically. Call notifyObjectUpdate() to manually emit those events if necessary.
  • There is no guarantee that the event data format remains unchanged in the future. Use the method and the data passed to the callback function only for debugging purposes.

Even more advanced usage #

Scoping #

Pot or Pottery

The scoping feature of this package is for Dart in general, not designed for Flutter. In Flutter, consider using Pottery instead. It is a utility wrapping this pot package for Flutter. It limits the lifespan of pots according to the lifecycle of widgets, which is more natural in Flutter and less error-prone.

What is scoping

A "scope" in this package is a notion related to the lifespan of an object held in a pot. It is given a sequential number starting from 0. Adding a scope increments the index number of the current scope, and removing one decrements it.

For example, if a scope is added when the current index number is 0, the number turns 1. If an object is then created, it gets bound to the current scope 1. It means the object exists while the current scope is 1 or newer, so when the scope 1 is removed, the object is removed and the disposer is triggered. The current index number goes back to 0.

final counterPot = Pot(() => Counter());
void main() {
  print(Pot.currentScope);     // 0

  Pot.pushScope();
  print(Pot.currentScope);     // 1

  // The Counter object is created here, and it gets bound to scope 1.
  final counter = counterPot();
  print(counterPot.hasObject); // true

  // The scope 1 is removed, causing the object to be removed.
  Pot.popScope();
  print(Pot.currentScope);     // 0
  print(counterPot.hasObject); // false
}

Just calling Pot.popScope() resets all pots bound to the current scope.

Combining replace() with scoping #

If an object is used only from some point onwards, you can make use of Pot.popScope() and replace().

Declare a pot with Pot.pending() initially, and replace the factory with the actual one after adding a scope. It allows a factory to be set only in a specific scope, and enables the object to be removed from the pot by removal of the scope.

// A dummy factory for the moment, which only throws an exception if called.
final userPot = Pot.pending<User>();

...

// A new scope is added, and the dummy factory is replaced with the actual one.
Pot.pushScope();
todoPot.replace(() => User(id, name));

// The User object is created and gets bound to the current scope.
final user = userPot();

...

// The scope is removed and the object is removed from the pot.
// It is a good practice to remove the factory so that it throws
// if called unexpectedly after this.
Pot.popScope();
userPot.resetAsPending();

Resetting pots in the current scope #

Pot.resetAllInScope() resets all pots bound to the current scope, removing all objects from the pots. The scope itself is not removed.

The behaviour of each reset caused by this method is the same as when you call reset() manually for each pot; the disposer is triggered in the same way.

Resetting pots in all scopes #

Pot.uninitialize() removes all scopes and resets all pots. This is useful to reset all pots and scopes to the initial state for testing. It may also be used to make the app behave as if it has restarted.

5
likes
160
points
361
downloads

Publisher

verified publisherkaboc.cc

Weekly Downloads

A simple and type-safe dependency injection (DI) package, designed for easy scoping and testing.

Repository (GitHub)
View/report issues

Topics

#dependency-injection

Documentation

API reference

License

MIT (license)

Dependencies

meta

More

Packages that depend on pot