pottery 0.1.1 copy "pottery: ^0.1.1" to clipboard
pottery: ^0.1.1 copied to clipboard

Widgets to limit the scope where particular Pots are available in the widget tree.

Pub Version pottery CI codecov

Overview #

A package that provides two widgets, Pottery and ScopedPottery.

They limit the scope where particular Pots are available in the widget tree. Using them make it clearer from which point onwards pots are used.

Why is this better than the scoping feature of Pot? #

Pot is not a package specific to Flutter but it is for Dart in general, therefore its scoping feature is not designed for Flutter either.

Pottery makes use of the widget lifecycle to limit the scope of pots. It is more natural in Flutter and less error-prone.

How is this beneficial? #

It is convenient that you can access a pot stored in a global variable from anywhere, but it gives you too much freedom, making it difficult to keep the architecture of your app well-organised.

By using Pottery, it becomes possible to manage pots in a similar manner to using package:provider. See the example described later in this document.

Getting started #

This package contains the pot package and exposes it. It is enough to only add pottery without pot to pubspec.yaml.

dependencies:
  pottery: ^x.x.x

Examples #

Usage #

This package comes with two widgets:

Pottery #

Create a pot as pending if it is not necessary yet at the start of an app.

final counterNotifierPot = Pot.pending<CounterNotifier>();

Use Pottery and specify a factory right before you need to use 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.

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 down the tree.
  • Pottery
    • Replaces factories to make pots ready so that they are available after that point. The widget tree is only used to manage the lifespan 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.

ScopedPottery #

This widget defines new factories for existing pots and binds the objects created by them to the pots so that those objects are made available to descendants.

An important fact is that the factories of the existing pots are not actually replaced, therefore calling the call() method of a pot still returns the object held in the global pot. Use of() instead to obtain the scoped object. The example below illustrates the behaviour.

final fooPot = Pot(() => Foo(111));
class ParentWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ScopedPottery(
      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
    ...
  }
}

See the examples in main2.dart and in the document of ScopedPottery for usage in more practical use cases.

Note that there are several important differences between ScopedPottery and Pottery:

  • Objects are created immediately when ScopedPottery is created, not when objects in Pots are accessed for the first time.
  • As already mentioned, objects created with ScopedPottery are only accessible with of().
  • Objects created with ScopedPottery are not automatically discarded when the ScopedPottery is removed from the tree. Use the disposer argument to specify a callback function to clean them up.

Caveats #

Make sure to specify a factory that returns a correct type. #

The pots argument is not type-safe as it uses a generic Map.

final counterNotifierPot = Pot.pending<CounterNotifier>();
pots: {
  counterNotifierPot: TodoNotifier.new,
}

In this example, the factory of counterNotifierPot must be a function that returns CounterNotifier. However, the static analysis does not tell you it is wrong to specify a factory that creates TodoNotifier. The error only occurs at runtime.

Usage with Grab #

The author created Pot and Pottery mainly for using them in combination with Grab. You can use Pottery + Grab as an alternative to package:provider.

There is however an important thing to remember. The extension methods of Grab require the BuildContext of the widget that has the Grab mixin, not the one passed to the builder function of Pottery.

class MyWidget extends StatelessWidget with Grab {
  const MyWidget();

  @override
  Widget build(BuildContext context) {
    return Pottery(
      pots: { ... },
      builder: (context) {
        // The BuildContext passed to this callback
        // cannot be used for methods of Grab.
        final count = counterNotifierPot().grab(context); // Bad
      },
    )
  }
}

It is actually possible to get around it by using the outer BuildContext instead.

Widget build(BuildContext context) {
  return Pottery(
    pots: { ... },
    builder: (innerContext) {
      // Grab works if you use the `context` passed to
      // the build method instead of `innerContext`.
      final count = counterNotifierPot().grab(context);
    },
  );
)

However, using grab methods this way is discouraged as it is confusing and can easily lead to a bug. If you are using grab_lints, it will warn you about it.

Make sure to use Pottery a little earlier to get pots ready before they are used in a build method. Here are two options for it.

Option 1 #

Using Pottery in the builder function of PageRoute before navigation.

ElevatedButton(
  onPressed: () => Navigator.of(context).push(
    MaterialPageRoute<void>(
      builder: (_) => Pottery(
        pots: { ... },
        builder: (_) => const CounterPage(),
      ),
    ),
  ),
  child: const Text('To CounterPage'),
)

Option 2 #

Using Pottery in the builder function of PageRoute in a route method.

This is essentially the same as Option 1, but more recommended because Pottery is used in the class of the actual page where pots are used. It makes more sense and helps you easily grasp the scope of pots when you get back to the code after a long while.

class CounterPage extends StatelessWidget {
  const CounterPage._();

  static Route<void> route() {
    return MaterialPageRoute(
      builder: (_) => Pottery(
        pots: { ... },
        builder: (_) => const CounterPage._(),
      ),
    );
  }

  Widget build(BuildContext context) {
    final count = counterNotifierPot().grab(context);
  }
}
ElevatedButton(
  onPressed: () => Navigator.of(context).push(CounterPage.route()),
  child: const Text('To CounterPage'),
)
2
likes
0
pub points
49%
popularity

Publisher

verified publisherkaboc.cc

Widgets to limit the scope where particular Pots are available in the widget tree.

Repository (GitHub)
View/report issues

Topics

#dependency-injection

License

unknown (LICENSE)

Dependencies

flutter, pot

More

Packages that depend on pottery