DEV Community

Cover image for StateNotifier, ChangeNotifier — Why, When, and How to use them.
danielAsaboro
danielAsaboro

Posted on • Updated on

StateNotifier, ChangeNotifier — Why, When, and How to use them.

StateProvider becomes unnecessarily challenging when you need to perform operations more complex than the basic counter increment and decrement example that we are commonly used to in Flutter.

In fact, it will fail when it comes to executing asynchronous operations or data transformations which involves applying conditional logic to a List Of Products in a Cart like in a Checkout Page.

And that's because it's just not suited for that specific purpose.

In cases where you are somehow able to manoeuvre your way through, your app's business logic will be littered all around the UI. This lack of clear separation of concerns can quickly lead to a tangled and hard-to-maintain codebase – a sure recipe for disaster in any sizable project.

Thankfully, there's a better Alternative...

Class Variant Providers.

All the Providers we discussed in the previous episode (Provider, FutureProvider, StreamProvider, and StateProvider) are called Function variant Providers because they are basically all functions.

Like functional programming, that comes with an implication:

  • Disorganization,
  • Little to no encapsulation,
  • Near-zero reusability,
  • No form of modularity etc...

Making maintainability and Testing very difficult.

OOP, on the other hand, lets you bundle data and methods that operate on it within a class. This makes documentation, debugging, and collaboration easy peasy.

The same Principle applies to Class Variants Providers

Class variants let you encapsulate the state and the logic for handling and providing data in a class. There are two ways of doing that in RiverPod:

  1. StateNotifier and
  2. ChangeNotifier

But I see ChangeNotifier as an Impostor

Change Notifier is an impostor

Its use is heavily discouraged because it allows mutable states. Mutable States have been historically known to be susceptible to subtle bugs, race conditions, and synchronization overhead.

While ChangeNotifier is heavily relied upon in Provider, it's only added in Riverpod to make the transition from Provider to Riverpod easy. So we will not talk about it beyond that.

As a result, StateNotifier will be this piece's focus.

StateNotifier: What is it?

StateNotifier is a simple solution to control state in an immutable manner". It's native to Flutter. And RiverPod only re-exports (not that you should care).

Alright, enough of the pedantic details.
Nothing beats seeing it in action.

StateNotifier In Action

Say you are tasked with building the Cart Page Features of an E-commerce app. Below are some of the required useful functionalities...

Ability to:

  1. Flat-out delete an Item(s).
  2. Mark an Item as a Wish and Move to the WishList
  3. Increase or Decrease the quantity of an Item on the list
  4. Recalculate the total price after each change.

Can you implement all of this with a StateProvider?

If yes, please link to your code In the comment section :)

While you are it, let's see how with a StateNotifier in 7 simple steps.

Step 1: Define what makes a Product




class Product {
  final int id;
  final String name;
  final double price;
  final int quantity;
  final bool isWishlisted;

  Product({
    required this.id,
    required this.name,
    required this.price,
    this.quantity = 1,
    this.isWishlisted = false,
  });

  @override
  String toString() {
    return 'Product{id: $id, name: $name, price: $price, quantity: $quantity, isWishlisted: $isWishlisted}';
  }
}



Enter fullscreen mode Exit fullscreen mode

You notice how all fields are marked final...?

This makes the class immutable which is a good thing as it becomes easier to compare the previous and the new state to check whether they are equal, implement an undo-redo mechanism, and debug the application state.

Step 2: Define what a Cart Entails




//... preceding code removed for brevity

class Cart {

  final List<Product> _productList;

  Cart({required this.productList}) : _productList = productList;

  double get totalPrice {
    //TODO
  }

  List<Product> get productList => _productList;

  Cart copyWith({List<Product>? productList}) {
    return Cart(
      productList: productList ?? _productList,
    );
  }
}



Enter fullscreen mode Exit fullscreen mode

Step 3: Now, create a StateNotifer class that notifies the Provider for every state Change in the enclosed class.

You do this by extending the StateNotifier class and passing the Class you want to track its changes as a parameter to the StateNotifier class type.




class CartNotifier extends StateNotifier<Cart> {
  CartNotifier() : super((Cart(productList: [])));
}



Enter fullscreen mode Exit fullscreen mode

If that looks confusing, let's break it down:

If you take a glimpse at the source code, you will find out that a StateNotifier is an abstract class which means it can't be instantiated but it can be extended.


 dart

abstract class StateNotifier<T> {
// class definition goes here
}



Enter fullscreen mode Exit fullscreen mode

You will also notice that it's a Generic class of type T (StateNotifier), which means you can pass whatever class you like to its type parameter.

When you do that, the resulting StateNotifier class will Notify others of every state changed in the pass Class type.

Said differently, your type T is the class you want to convert into a StateNotifier Class, and your classNotifier Class is the resulting combination after the operation.

Moving on...

Image description

What you see in the Super function call is just me calling the constructor of the SuperClass that was extended. This is what we've been doing all our lives when we create a Stateless or Stateful Widget.



class HomePageScreen extends StatelessWidget {

  const HomePageScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const Placeholder();
  }
}



Enter fullscreen mode Exit fullscreen mode

Step 4: Add your state modification methods to the StateNotifier class




//All previous package imports apply but are removed for brevity

class CartNotifier extends StateNotifier<Cart> {
  CartNotifier()
      : super((Cart(productList: <Product>[])));

  void addToCart() {
  //TODO
  }

  void removeThisElementFromCart() {
    //TODO
  }

  void increaseQuantityAtIndex() {
    //TODO
  }

  void decreaseQuantityAtIndex() {
    //TODO
  }

  void toggleWishlist() {
     //TODO
  }
}




Enter fullscreen mode Exit fullscreen mode

Do you see how each modification function returns nothing?
Their only job is to change state, nothing more.

I won't implement the addToCard method for two reasons: It will stretch the length of the tutorial and Two, which is the most important — it's a good take-home practice exercise.

Instead, we will implement the removeThisElementFromCart method. As a result, I will give you a list of Products to manipulate as a starting Point.



import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

List<Product> productListPassed = [
Product(
id: 1,
name: 'Product A',
price: 19.99,
quantity: 3,
isWishlisted: true,
),
Product(
id: 2,
name: 'Product B',
price: 29.99,
quantity: 5,
isWishlisted: false,
),
Product(
id: 3,
name: 'Product C',
price: 9.99,
quantity: 2,
isWishlisted: true,
),
// Add more products as needed
];

class CartNotifier extends StateNotifier<Cart> {
CartNotifier()
: super((Cart(productList: productListPassed)));

void addToCart() {
//TODO
}

void removeThisElementFromCart(int productIndex) {
final productList = state.productList;
productList.removeAt(productIndex);
state = state.copyWith(productList: productList);
}

}

Enter fullscreen mode Exit fullscreen mode




Let's break down what just happened in the removeThisElementFromCart Method

we know how our Cart is basically a List of Products and to access each product, all we need to do is identify its index (which is what is passed to the function as a parameter)

Image description

In line 1 of the function body...

we extracted the current list of products in the state object and saved it to the "productList" variable.

The variable "state" is Riverpod's way of representing the current state of the Class passed to the StateNotifier Class which in our case is the Cart object.

As you can see in the following screenshot, it also gives you access to all the fields and methods available on the Cart Object

Image description

Line 2 is pretty straightforward

We removed the item at the passed index. YOu can read about the removeAt method here on Flutter Documentation website.

All it does is remove the item at the specified index from the list, reduce the length by one, and move other items down by one position.

And Finally...

In line 3, we reassign the state variables.

Internally, this triggers two actions.

  1. It compares the previous state value with the current one,
  2. It calls NotifyListeners() only when the values are not equal.

NotifyListeners is what informs all subscribed listeners to a state object of a change in value. But before it can do that, we need to link our StateNotifier class to our Widgets(UI Elements).

And the only way to do that is by using a Provider.

Since our StateNotifier is not just your typical object, we can't use a futureprovider, streamprovider, or Stateprovider. So we will need a special Provider to help us do that.

It's called the StateNotifierProvider and it will be the focus of our next lesson.

Top comments (7)

Collapse
 
fadel1411 profile image
faddevlab

What does "??" means in
productList: productList ?? _productList,
?

Collapse
 
danielasaboro profile image
danielAsaboro

I'm sorry I missed this message, I wouldn't have seen it had someone not reply.
i hope their response was helpful.

to add to it:

it's a shorthand ternary operator

productList = productList != null ? productList : _productList;

I hope this helps.
once again, apologies for the late response

Collapse
 
amitbhandari7777 profile image
Amit Bhandari

In general case
If you pass productList it will be shown
or else _productList will be shown

Collapse
 
fadel1411 profile image
faddevlab

Do you have a Github or GItlab based repository on this code?

Collapse
 
danielasaboro profile image
danielAsaboro

i should provide it soon

Collapse
 
shaysframe profile image
Shay • Edited

Moving on...
description image
What you see in the Super function call is just me calling the constructor of the SuperClass that was extended. This is what we've been doing all our lives when we create a Stateless or Stateful Widget.

here I'm a little confused.

Also, the code snippet of the following seems not correct, or maybe my IDE is wrong?

class Cart {
  final List<Product> productList;

  Cart({required this.productList});

  double get totalPrice {
    //TODO
  }

  List<Product> get productList => _productList;

  Cart copyWith({List<Product>? productList}) {
    return Cart(
      productList: productList ?? _productList,
    );
  }
Enter fullscreen mode Exit fullscreen mode

should the final List<Product> productList; be _productList as it's not the same as the get productList? or something totally different? because if I rename the productList to _productList a lot of other things also get changed... for example:

class Cart {
  final List<Product> _productList;

  Cart({required List<Product> productList}) : _productList = productList;

  double get totalPrice {
    //TODO
  }

  List<Product> get productList => _productList;

  Cart copyWith({List<Product>? productList}) {
    return Cart(
      productList: productList ?? _productList,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

and this change was made by the IDE automatically not by me. So I'm not sure whether the IDE is right or not if you could explain what's happening and what's going on that'd be helpful. thanks.

Collapse
 
danielasaboro profile image
danielAsaboro

ouch, i'm so sorry i didn't catch this quick, the code your ide automatically corrected is the right thing, i missed it...thanks for pointing that out