Introducing Collection Providers

A set of Change Notifiers that can be dropped in to enhance interacting with Providers.

The current Flutter Favorites for State Management is the Provider package. In point of fact, it's part of the official documentation for state management The Provider wraps the InheritedWidget and uses ChangeNotifiers to notify components of state updates.

The problem is that if you just want to share a simple collection of things, there's a lot of boilerplate that you have to write for it, because you have to write - at a minimum - a new model class to share between components. Most of the time, however, you end up writing a bunch of accessor methods to the underlying data as well.

The collection_providers package is a set of Change Notifiers that can be dropped in to make interacting with Providers feel more like interacting with standard collections. Because, as a developer, I'm lazy and sometimes typing that extra .value on the end of the model's variable is just too many extra characters1.

A basic ChangeNotifier example

Let's use the example model from the official documentation for state management:

NormalChangeNotifier.dart
class CartModel extends ChangeNotifier {
  /// Internal, private state of the cart.  final List<Item> _items = [];  
  /// An unmodifiable view of the items in the cart.
  UnmodifiableListView<Item> get items => UnmodifiableListView(_items);
  
  /// The current total price of all items (assuming all items cost $42).
  int get totalPrice => _items.length * 42;
  
  /// Adds [item] to cart. This and [removeAll] are the only ways to modify the  /// cart from the outside.  void add(Item item) {    _items.add(item);    // This call tells the widgets that are listening to this model to rebuild.    notifyListeners();  }    /// Removes all items from the cart.  void removeAll() {    _items.clear();    // This call tells the widgets that are listening to this model to rebuild.    notifyListeners();  }}

This is a pretty straight forward example. We have a list of items that we can add to and clear. We also have a couple of methods that allow us to get the totalPrice and an immutable view of the items in the cart.

Of the 25 lines of code, this 15 highlighted lines are boilerplate. That's 60% of the code that is boilerplate for creating and modifying the underlying collection of items. And the boilerplate just gets thicker if we want to add more methods for mutating the items in our cart. Don't forget to add notifyListeners to every method as well, if you don't then nothing that uses it works because your state never updates.

Converting to a ListChangeNotifier

We can remove all the boilerplate code by converting the above example to use a ListChangeNotifier.

NowUsingListChangeNotifier.dart
class CartModel extends ListChangeNotifier<Item> {
  /// An unmodifiable view of the items in the cart
  UnmodifiableListView<Item> get items => UnmodifiableListView(this);
  
  /// The current total price of all items (assuming all items cost $42).
  int get totalPrice => length * 42;
}

We took our model from 25 lines of code to 7. I like that much better. On top of that, we also have all the functionality of the underlying List that we can use to interact with the model for free, instead of just the two methods from the previous model.

The items getter was left in, because it creates an unmodifiable view. There are still places where this is useful. The main gain of the new class is that we get all the mutating methods for free.

We can now interact with our model very simply using the existing Provider classes:

DecoratingOverTheCart.dart
// Remove a specific item from the cart
Provider.of<CartModel>(context, listen: false).removeWhere((item) => item.id === 3);
// Sort by price
context.read<CartModel>().sort((a, b) => a.price.compareTo(b.price));

NOTE: Provider.of<T>(context, listen: false) is functionally equivalent to context.read<T>(). Similarly, Provider.of<T>(context) is equivalent to context.watch<T>().

Temporarily pausing notifications

One big disadvantage of this method is that every time we modify the underlying collection, the listeners are notified. This can be really obnoxious if we want to do multiple updates to the list (like a database transaction) before we notify the listeners.

pauseNotifications

pauseNotifications will pause notification while a callback is executed, then will optionally notify listeners after the callback has been completed.

Take for example, we have an item that has a promotion running. The user added the item, but didn't get the promotion. We give them the option to add the promotion item instead. Here's the code that lets us remove the non-promo item and add the promo item and only notify at the end after both operations have succeeded.

PauseNotifications.dart
const cart = context.read<CartModel>();
cart.pauseNotifications(() {
  // Remove the non-promo item from the cart
  cart.removeWhere((item) => item.id == 42);
  // Add the promo item to the cart instead
  cart.add(promoItem);
}, true); // Notify at the end

The true as the second argument will tell the CollectionNotifier to notify the listeners after the pause callback has finished executing. The default is false and you must manually notify listeners at some point after the pause is done (or hit another operation that will notify when it completes).

If the callback returns a value, that value will be returned from pauseNotifications. Which can be handy at times.

pauseNotificationsAsync

Say we want to do the same thing, but we also want to save the shopping cart to the back-end server instead of just on the phone. You can also pause notifications while a async operation is performed. Let's take the same code as above, but also sync it to our server.

We'll use pauseNotificationsAsync.

PauseNotificationsAsync.dart
const cart = context.read<CartModel>();
// The `await` below is optional, depending on if you want your code to pause for this
await cart.pauseNotificationsAsync(() async {
  // Remove the non-promo item from the cart
  cart.removeWhere((item) => item.id == 42);
  // Add the promo item to the cart instead
  cart.add(promoItem);
  // Sync to the backend
  await api.syncCart(this);
}, true); // Notify at the end

This help is the same as the previous, except it will wait for the asynchronous operation to finish before it tries to notify the listeners.

Other Collections: Map, Set

Included in the library are two more Change Notifiers for common collections: MapChangeNotifier and SetChangeNotifier. As subclasses of CollectionNotifier, they have the same pause ability but the implement the Map and Set interfaces respectively.

Initializing with an existing collection

All of the Collection Providers can be initialized with initial values from another collection. The collection is copied and the copy is what is manipulated by the Change Notifier.

CollectionProvider and CollectionConsumer

A CollectionProvider is a drop-in replacement for a simple Provider, that just enforces the type safety that the change notifier is a CollectionChangeNotifier.

Likewise, a CollectionConsumer is a drop-in replacement for a simple Consumer for enforcing type safety.

However, you can just use Provider and Consumer like you normally would with no ill effects.

"Anonymous" Collection Change Notifier

To avoid writing any boilerplate, you can use what I have dubbed an "anonymous" Collection Change Notifier. There are to instances of these in the Example Application.

To do this, simply initialize a provider and directly return a CollectionChangeNotifier for the desired collection type.

AnonymousCollectionChangeNotifier.dart
// Initialize the provider
Provider( // or CollectionProvider
  create: (_) => ListChangeNotifier<Item>(),
  child: ...
);
// or
MultiProvider(
  providers: [
    Provider( // or CollectionProvider
      create: (_) => MapChangeNotifier<String, Item>(),
    ),
  ],
  child: ...
);

// Reading a value from the provider
var directly = context.read<ListChangeNotifier<Item>>();
// Via consumer
Consumer<MapChangeNotifier<String, Item>>( // or CollectionConsumer<...>
  builder: (context, mapProvider, child) {
      return ...;
  }  
)

In Closing

I've found these collection wrappers to be very useful in my apps. It has sped up several areas of my development to be able to just interact with the model as if it were just a normal collection. Only writing the boilerplate once and be able to access it by extending the classes is great.

I hope you find it as useful as I have.


  1. This sounds like a joke but it is 100% true. I don't like typing accessors over and over. I'll assign them to a temp variable so I only have to type it once. Sometimes, in a lambda, you don't have that option and then I end up typing the accessor over and over and I curse my name for designing something so frustrating. I'm not alone in this. People learn DVORAK because it saves time with typing things. You know what else saves time? Not having to type accessors over and over.

Published under ,  on .

Brian Armstrong

Brian Armstrong

I'm Brian Armstrong, a SaaS developer with 15+ years programming experience. I am a Flutter evangelist and React.js enthusiast.