Dart Tutorial

Flutter Lesson 40 of 83 4 min read

Provider in Flutter: State Management Tutorial

Learn the provider package in Flutter: ChangeNotifierProvider, context.watch, read and select, Consumer and MultiProvider, with a cart example.

On this page

provider is the most widely used state management package in Flutter. It does one job: put an object above part of the widget tree so that any widget below can get it, and be rebuilt when it changes. It is a convenient wrapper around InheritedWidget.

flutter pub add provider

Three steps #

  1. Write a model, a ChangeNotifier.
  2. Provide it above the widgets that need it.
  3. Consume it with context.watch or context.read.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// 1. The model
class CartModel extends ChangeNotifier {
  final List<String> _items = [];

  List<String> get items => List.unmodifiable(_items);
  int get count => _items.length;

  void add(String item) {
    _items.add(item);
    notifyListeners();
  }

  void clear() {
    _items.clear();
    notifyListeners();
  }
}

void main() {
  runApp(
    // 2. Provide it above the whole app
    ChangeNotifierProvider(
      create: (context) => CartModel(),
      child: const MaterialApp(home: ShopPage()),
    ),
  );
}

class ShopPage extends StatelessWidget {
  const ShopPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Shop'),
        actions: const [CartCount()],
      ),
      body: ListView(
        children: [
          for (final name in ['Pen', 'Notebook', 'Bag'])
            ListTile(
              title: Text(name),
              trailing: IconButton(
                icon: const Icon(Icons.add_shopping_cart),
                // 3a. read: call a method, do not listen
                onPressed: () => context.read<CartModel>().add(name),
              ),
            ),
        ],
      ),
    );
  }
}

class CartCount extends StatelessWidget {
  const CartCount({super.key});

  @override
  Widget build(BuildContext context) {
    // 3b. watch: rebuild this widget whenever the cart changes
    final count = context.watch<CartModel>().count;
    return Padding(
      padding: const EdgeInsets.only(right: 16),
      child: Badge(label: Text('$count'), child: const Icon(Icons.shopping_cart)),
    );
  }
}

No constructor passes the cart anywhere. CartCount could be on a different screen and would work the same. When an item is added, only CartCount rebuilds. The list does not, because it never watched.

watch, read and select #

CallRebuilds the widget?Use in
context.watch<T>()Yes, on every changebuild, to display data
context.read<T>()NoCallbacks such as onPressed, to call methods
context.select<T, R>((m) => m.field)Only when that one value changesbuild, to listen to part of a model
// Rebuilds only when the count changes, not when anything else in the cart does.
final count = context.select<CartModel, int>((cart) => cart.count);

The rule that prevents most bugs: watch in build, read in callbacks. Using watch inside onPressed throws. Using read in build shows stale data.

Consumer #

Consumer rebuilds only the part of a build method inside its builder. It is useful when you cannot easily split out a widget, or when the provider is created in the same build method.

Consumer<CartModel>(
  builder: (context, cart, child) => Text('${cart.count} items'),
)

Several providers #

MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (_) => AuthModel()),
    ChangeNotifierProvider(create: (_) => CartModel()),
    Provider(create: (_) => ApiClient()), // a plain object that never changes
  ],
  child: const MyApp(),
)

Kinds of provider #

ProviderSupplies
ProviderA value that does not change, such as a service or repository
ChangeNotifierProviderA ChangeNotifier, and rebuilds listeners when it notifies
FutureProviderThe result of a future
StreamProviderThe latest value from a stream
ProxyProviderA value built from other providers

ChangeNotifierProvider also calls dispose() on the model when it is removed from the tree.

One model depending on another #

MultiProvider(
  providers: [
    Provider(create: (_) => ApiClient()),
    ChangeNotifierProvider(
      create: (context) => ProductsModel(context.read<ApiClient>()),
    ),
  ],
  child: const MyApp(),
)

Scoping #

A provider is visible only to widgets below it. Put app-wide state above MaterialApp. Put state for one screen above that screen, and it is created when the screen opens and disposed when it closes.

Navigator.push(
  context,
  MaterialPageRoute<void>(
    builder: (_) => ChangeNotifierProvider(
      create: (_) => CheckoutModel(),
      child: const CheckoutPage(),
    ),
  ),
);

The error everyone meets #

Error: Could not find the correct Provider<CartModel> above this Widget

It means the context you used is not below the provider. The usual causes:

  • The provider is inside a page, and you pushed a new route. Routes are siblings under the navigator, not children of the page. Move the provider above MaterialApp.
  • You created the provider and read it in the same build method, with the same context. Use a Consumer, or split the reading part into its own widget.

Loading data in a model #

class ProductsModel extends ChangeNotifier {
  ProductsModel(this._api);
  final ApiClient _api;

  List<Product> products = [];
  bool loading = false;
  String? error;

  Future<void> load() async {
    loading = true;
    error = null;
    notifyListeners();
    try {
      products = await _api.fetchProducts();
    } catch (e) {
      error = 'Could not load products';
    } finally {
      loading = false;
      notifyListeners();
    }
  }
}

// Start loading as soon as the model is created:
ChangeNotifierProvider(create: (context) => ProductsModel(context.read<ApiClient>())..load())

Try it yourself #

Build a favourites feature. A FavouritesModel holds a set of product ids with a toggle(id) method. A product list shows a heart on each row, filled when the product is a favourite. The app bar shows the number of favourites, and a second screen lists them.

Practise in the playground Updated by Santosh Adhikari