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:
- StateNotifier and
- ChangeNotifier
But I see ChangeNotifier as 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:
- Flat-out delete an Item(s).
- Mark an Item as a Wish and Move to the WishList
- Increase or Decrease the quantity of an Item on the list
- 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}';
}
}
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});
double get totalPrice {
//TODO
}
List<Product> get productList => _productList;
Cart copyWith({List<Product>? productList}) {
return Cart(
productList: productList ?? _productList,
);
}
}
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: [])));
}
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.
abstract class StateNotifier<T> {
// class definition goes here
}
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...
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();
}
}
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
}
}
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);
}
}
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)
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
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.
- It compares the previous state value with the current one,
- 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.