Skip to main content

StateProvider

caution

The content of this page may be outdated.
It will be updated in the future, but for now you may want to refer to the content in the top of the sidebar instead (introduction/essentials/case-studies/...)

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.