StateProvider
StateProvider
is a provider that exposes a way to modify its state.
It is a simplification of NotifierProvider, designed to avoid
having to write a Notifier class for very simple use-cases.
StateProvider
exists primarily to allow the modification of
simple variables by the User Interface.
The state of a StateProvider
is typically one of:
- an enum, such as a filter type
- a String, typically the raw content of a text field
- a boolean, for checkboxes
- a number, for pagination or age form fields
You should not use StateProvider
if:
- your state needs validation logic
- your state is a complex object (such as a custom class, a list/map, ...)
- the logic for modifying your state is more advanced than a simple
count++
.
For more advanced cases, consider using NotifierProvider instead and
create a Notifier class.
While the initial boilerplate will be a bit larger, having a custom
Notifier class is critical for the long-term maintainability of your
project – as it centralizes the business logic of your state in a single place.
Usage example: Changing the filter type using a dropdown
A real-world use-case of StateProvider
would be to manage the state of
simple form components like dropdowns/text fields/checkboxes.
In particular, we will see how to use StateProvider
to implement a dropdown
that allows changing how a list of products is sorted.
For the sake of simplicity, the list of products that we will obtain will be built directly in the application and will be as follows:
class Product {
Product({required this.name, required this.price});
final String name;
final double price;
}
final _products = [
Product(name: 'iPhone', price: 999),
Product(name: 'cookie', price: 2),
Product(name: 'ps5', price: 500),
];
final productsProvider = Provider<List<Product>>((ref) {
return _products;
});
In a real-world application, this list would typically be obtained using FutureProvider by making a network request.
The User Interface could then show the list of products by doing:
Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider);
return Scaffold(
body: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
title: Text(product.name),
subtitle: Text('${product.price} \$'),
);
},
),
);
}
Now that we're done with the base, we can add a dropdown, which will
allow filtering our products either by price or by name.
For that, we will use DropDownButton.
// An enum representing the filter type
enum ProductSortType {
name,
price,
}
Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Products'),
actions: [
DropdownButton<ProductSortType>(
value: ProductSortType.price,
onChanged: (value) {},
items: const [
DropdownMenuItem(
value: ProductSortType.name,
child: Icon(Icons.sort_by_alpha),
),
DropdownMenuItem(
value: ProductSortType.price,
child: Icon(Icons.sort),
),
],
),
],
),
body: ListView.builder(
// ... /* SKIP */
itemBuilder: (c, i) => Container(), /* SKIP END */
),
);
}
Now that we have a dropdown, let's create a StateProvider
and
synchronize the state of the dropdown with our provider.
First, let's create the StateProvider
:
final productSortTypeProvider = StateProvider<ProductSortType>(
// We return the default sort type, here name.
(ref) => ProductSortType.name,
);
Then, we can connect this provider with our dropdown by doing:
DropdownButton<ProductSortType>(
// When the sort type changes, this will rebuild the dropdown
// to update the icon shown.
value: ref.watch(productSortTypeProvider),
// When the user interacts with the dropdown, we update the provider state.
onChanged: (value) =>
ref.read(productSortTypeProvider.notifier).state = value!,
items: [
// ...
],
),
With this, we should now be able to change the sort type.
It has no impact on the list of products yet though! It's now time for the
final part: Updating our productsProvider
to sort the list of products.
A key component of implementing this is to use ref.watch, to have
our productsProvider
obtain the sort type and recompute the list of
products whenever the sort type changes.
The implementation would be:
final productsProvider = Provider<List<Product>>((ref) {
final sortType = ref.watch(productSortTypeProvider);
switch (sortType) {
case ProductSortType.name:
return _products.sorted((a, b) => a.name.compareTo(b.name));
case ProductSortType.price:
return _products.sorted((a, b) => a.price.compareTo(b.price));
}
});
That's all! This change is enough for the User Interface to automatically re-render the list of products when the sort type changes.
Here is the complete example on Dartpad:
How to update the state based on the previous value without reading the provider twice
Sometimes, you want to update the state of a StateProvider
based on the previous value.
Naturally, you may end-up writing:
final counterProvider = StateProvider<int>((ref) => 0);
class HomeView extends ConsumerWidget {
const HomeView({super.key});
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
// We're updating the state from the previous value, we ended-up reading
// the provider twice!
ref.read(counterProvider.notifier).state = ref.read(counterProvider.notifier).state + 1;
},
),
);
}
}
While there's nothing particularly wrong with this snippet, the syntax is a bit inconvenient.
To make the syntax a bit better, we can use the update
function.
This function will take a callback that will receive the current state and is expected
to return the new state.
We can use it to refactor our previous code to:
final counterProvider = StateProvider<int>((ref) => 0);
class HomeView extends ConsumerWidget {
const HomeView({super.key});
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(counterProvider.notifier).update((state) => state + 1);
},
),
);
}
}
This change achieves the same effect while making the syntax a bit better.