HomebaseWorks
Lesson 4 of 4

Sharing state across screens with ChangeNotifier and Provider

Move state out of a single widget, notify listeners when it changes, and read it from any screen with watch and read.

EWritten by the HomebaseWorks editorial team · reviewed 26 Oct 2026

setState works for one widget. The moment a cart count must appear on three screens, passing values through every constructor becomes painful and error-prone. Flutter screenings expect you to know at least one answer to this, and ChangeNotifier with the Provider package is the one most teams start with.

Advertisement

The model

A ChangeNotifier is a plain class that can tell listeners something changed. Keep the data private and expose read-only views so no widget can mutate it without a notification.

import 'package:flutter/foundation.dart';

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();
  }
}

Providing it above the app

Add provider under dependencies in pubspec.yaml and run flutter pub get. Then wrap the app so every route can reach the model. Placing the provider above MaterialApp matters: screens opened with Navigator are built under MaterialApp, so a provider inside one screen would not be visible to the next.

import 'package:provider/provider.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => CartModel(),
      child: const MyApp(),
    ),
  );
}

watch versus read

context.watch<T>() subscribes the widget, so it rebuilds whenever notifyListeners runs. Use it in build. context.read<T>() gets the model once without subscribing. Use it inside callbacks such as a button press.

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

  @override
  Widget build(BuildContext context) {
    final count = context.watch<CartModel>().count;
    return Text('$count items');
  }
}

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

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () => context.read<CartModel>().add('Notebook'),
      child: const Text('Add notebook'),
    );
  }
}

Tap the button and only CartBadge rebuilds, because it is the only widget watching. AddButton used read, so it stays as it is. For a large screen where only one small part depends on the model, Consumer<CartModel> wraps just that part. Provider is a thin layer over InheritedWidget, which is the built-in mechanism; interviewers sometimes ask you to say so.

Common mistakes and a practice habit

  • Calling watch inside a callback. Flutter throws an assertion because listening outside build makes no sense.
  • Calling read in build and wondering why the number never updates.
  • Changing the list and forgetting notifyListeners().
  • Exposing the raw list so another widget adds to it silently. List.unmodifiable prevents this.

Practice habit: build a two-screen app where screen one adds items and screen two shows the count and a clear button. Confirm the count is correct after navigating back and forth, then replace one watch with Consumer and check the rebuild behaviour with the widget inspector.

Advertisement
Continue with Flutter4 lessons · 1 interview set · 1 test · 1 certificate
Not tested

More in Web and app