Overview
This package provides two widgets, Pottery and LocalPottery, which manage the lifetime of Pots (single-type DI containers) according to the lifecycle of widgets in Flutter.
Why is this better than the scoping feature of Pot?
Pot itself from package:pot has the feature of scoping, but it is a package for Dart, not specific to Flutter.
Pottery is a utility that makes up for it. It makes use of the widget lifecycle
to limit the scope of pots. It is more natural in Flutter and less error-prone.
How will this make things better?
While it is convenient that you can access a pot stored in a global variable from anywhere, it gives you too much freedom, making you wonder how pots should be managed in a Flutter app. For example, you may easily lose track of from where in your app code a particular pot is used.
Pottery makes it possible to manage pots in a similar manner to using package:provider.
Examples
- Counters - simple
- pub.dev explorer - advanced
Usage
Pottery
Create a Pot as "pending" first if it is not necessary yet at the start of your app. The pot should usually be assigned to a global variable.
final counterNotifierPot = Pot.pending<CounterNotifier>();
Use Pottery and specify a factory when you are about to start using the pot.
Widget build(BuildContext context) {
// counterNotifierPot does not have a factory yet.
// Calling `counterNotifierPot()` here throws a PotNotReadyException.
...
return Scaffold(
body: Pottery(
pots: {
counterNotifierPot: CounterNotifier.new,
},
// The new factory specified in the pots argument above is ready
// before this builder is called for the first time.
builder: (context) {
// Methods and getters of counterNotifierPot are now available.
final count = counterNotifierPot();
...
},
),
),
);
pots is a Map with key-value pairs of a Pot and a factory. Each of the factories
becomes available for a corresponding Pot thereafter.
Note
It is easier to understand how to use Pottery by imagining it as something
similar to MultiProvider of the provider package, although they internally
work quite differently:
- MultiProvider
- Creates objects and provides them so that they are available in the subtree.
- Pottery
- Replaces factories to make pots ready so that they are available after that point. The widget tree is only used to manage the lifetime of factories and objects in pots, so pots are still available outside the tree.
Removing Pottery from the tree (e.g. navigating back from the page where
Pottery is used) resets all pots in the pots map and replaces their
factories to throw an PotNotReadyException.
Note
If a target pot is not pending and an object already exists in it when Pottery
is created, Pottery immediately replaces the object as well as the factory.
LocalPottery
This widget defines new factories for existing pots to create objects that are available only in the subtree.
An important fact is that the existing pots remain unchanged. The factories and
objects are associated with those pots and stored in LocalPottery for local
use. Therefore, calling yourPot() still returns the globally accessible object
stored in the pot itself. Use of() instead to obtain the local object.
final fooPot = Pot(() => Foo(111));
class ParentWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return LocalPottery(
pots: {
fooPot: () => Foo(222),
},
builder: (context) {
print(fooPot()); // 111
print(fooPot.of(context)); // 222
return ChildWidget();
},
);
}
}
class ChildWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
print(fooPot()); // 111
print(fooPot.of(context)); // 222
...
}
}
For usage in more practical use cases, see the examples in main2.dart and in
the document of LocalPottery.
Important differences in LocalPottery compared to Pottery:
- Objects are created immediately when
LocalPotteryis created, not when objects in pots are accessed for the first time. - Objects created with
LocalPotteryare only accessible with of(). - Objects created within
LocalPotteryare not automatically disposed when theLocalPotteryis removed from the tree. Use thedisposerargument ofLocalPottery(instead of the disposer in each pot) to define a custom clean-up function.
Below is an example of a disposer function that disposes of all ChangeNotifiers and subtypes:
LocalPottery(
pots: {
myChangeNotifier: () => MyChangeNotifier(),
intValueNotifier: () => ValueNotifier(111),
},
disposer: (pots) {
pots.values.whereType<ChangeNotifier>().forEach((v) => v.dispose());
},
builder: (context) { ... },
)
Caveats
Make sure to specify a factory that returns a correct type.
Key-value pairs passed to pots are not type-safe.
In the following example, a function returning an int value is specified as
a new factory of a Pot for String. Although it is obviously wrong, the static
analysis does not tell you about the mistake. The error only occurs at runtime.
final stringPot = Pot.pending<String>();
pots: {
stringPot: () => 123,
}
DevTools extension
This package includes the DevTools extension.
To use it, run your app in debug mode with Flutter 3.16 or newer and open the DevTools.
The extension starts when either Pottery or LocalPottery is first used.
It is also possible to start it earlier by calling Pottery.startExtension().
Note
Updates of the object in a pot caused by external factors (e.g. the object is
a ValueNotifier and its value is reassigned) are not automatically reflected
in the table view until an event of either Pot, Pottery or LocalPottery
happens. Press the refresh icon button if you want to see the changes quickly,
or use notifyObjectUpdate() on a pot to emit an event
to cause a refresh.