StateNotifierProvider: How to link your stateNotifier to your UI Widgets

StateNotifierProvider: How to link your stateNotifier to your UI Widgets

In the previous post, we created our custom StateNotifier class by extending the StateNotifier class. However, the state changes weren't in any way linked to our Widgets/UI code.

I also hinted that it's the job of a special type of Provider (the StateNotifierProvider) to do that linking for us. It's similar to how a FutureProvider or StreamProvider links the result of a Future or Stream to the UI.

In summary:

• Future => FutureProvider • Stream => StreamProvider • StateNotifier => StateNotifierProvider

That cleared, let's see how to do it.

1. Create a StateNotifierProvider


final cartProvider = StateNotifierProvider<CartNotifier, Cart>((ref) {
  return CartNotifier();
});

That looks like a mouthful.

So let's break it down.

StateNotifierProvider is a class or type provided by Riverpod that allows you to create a provider for managing the application state. It requires two generic type parameters:

  1. The user-defined StateNotifier type created by extending StateNotifier
  2. The State type that the Provider will hold.

In our case, it's <CartNotifier, Cart>.

In addition to what was said earlier, the CartNotifier is also responsible for managing the provider's state. While the Cart represents the data structure that the CartNotifier will manage.

StateNotifierProvider is an anonymous function


StateNotifierProvider((ref) { return CartNotifier(); });

It takes a reference to ref as a parameter, which is used to access other providers or perform side effects within the provider initialization.

Then it creates an instance of the CartNotifier class, which we already said is responsible for managing the provider's state. You can also call a StateNotifierProvider a Constructor Function (a special function that creates and initializes an object instance of a class) for this reason.

In summary


final cartProvider = StateNotifierProvider<CartNotifier, Cart>((ref) {
  return CartNotifier();
});

The block of code above creates a Riverpod provider named cartProvider that uses a CartNotifier to manage the state of type Cart.

When you access cartProvider in your code, it will provide the current state of Cart managed by the CartNotifier.

2. Use ref.watch() to read and continuously listen to the state Changes in your UI


// Preceding code body removed for brevity

class EachProductWidget extends ConsumerWidget {
  final Product product;
  final int index;

  const EachProductWidget({
    super.key,
    required this.product,
    required this.index,
  });
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Card(
       // The UI code to display each product as a Card
       // UI code removed for brevity and clarity.
    );
  }
}

class ProductListView extends ConsumerWidget {
  const ProductListView({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final cart = ref.watch(cartProvider);
    return Scaffold(
      body: Container(
        height: 600,
         // UI/Widget Code goes here
        // It contains a listview builder
       // iterating through the list...
      // of provided products;
      ),
    );
  }
}

It's that simple.

Use ref.read() to call the modification methods


// surrounding code removed for brevity
    IconButton(
                  icon: Icon(Icons.remove),
                  onPressed: () {
                    ref
                        .read(cartProvider.notifier)
                        .decreaseQuantityAtIndex(index);
                  },
                ),

While we could use ref.watch and it would work, ref.read is the better candidate for calling the state modification methods because it reads the provider once but without listening to state changes.

This ensures we never run into race conditions.

You will observe that we are not reading the provider, but the "notifier" method on it. This is because that's the only to access the modification method on a provider.

Image description

Reading the provider alone won't expose those methods

Image description

If you forget anything, don't forget this:

• Use ref.watch in Build Methods, and • use ref.read in Callbacks;

No need to call SetState or notifyListeners — Riverpod does that automatically for you.

If you are confused with the whole thing...

Remember that converting Futures, Streams, and Simple Objects into their respective Providers is a one-step, simple process. That's because they don't have methods acting on them.

The case is different with Complex Objects which also have methods acting on them. This is why we first wrap it in a StateNotifier before converting it into its respective Provider;

Still confused? Reach out to me on Twitter;

Todo Challenge

We've achieved a lot at this point in our state Management journey.

We learnt how to Create, Read, Update, and Delete data that influences our app's state. We learnt how to make our widgets respond to user input and cause changes on another widget without relying on callbacks.

It's been a job well done.

However, our simple cart service isn't entirely complete yet. There are a few loose ends that need to be tied up. So, I'm leaving you with five final challenges:

  1. Complete the implementation of the addToCart method.
  2. Make the Wishlist icon respond to the isWishlisted field.
  3. Create a method for decreasing the item count in the cart.
  4. Create a method to delete a product from the Cart completely.
  5. Provide a warning to the user when they attempt to decrease the item count when it's already at count 1.

Tweet at me with your solutions @AsaboroD