Flutter Tutorial

Flutter Intermediate Lesson 47 of 135 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