State Management Fundamentals

InheritedNotifier

45 min Lesson 5 of 14

What is InheritedNotifier?

InheritedNotifier is a special subclass of InheritedWidget that automatically rebuilds dependents whenever a Listenable (typically a ChangeNotifier) fires a notification. With a standard InheritedWidget, you must manually decide when dependents should update by overriding updateShouldNotify. InheritedNotifier removes that burden entirely — it listens to the notifier you provide and triggers rebuilds for you.

Key Difference: InheritedWidget rebuilds dependents only when the parent widget rebuilds and updateShouldNotify returns true. InheritedNotifier rebuilds dependents whenever the attached Listenable calls notifyListeners(), regardless of parent rebuilds.

InheritedNotifier vs InheritedWidget

Let’s compare the two approaches side by side to understand why InheritedNotifier is often the better choice when your state is driven by a ChangeNotifier.

Standard InheritedWidget (Manual Approach)

class CounterInherited extends InheritedWidget {
  final int count;

  const CounterInherited({
    super.key,
    required this.count,
    required super.child,
  });

  static CounterInherited of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<CounterInherited>()!;
  }

  @override
  bool updateShouldNotify(CounterInherited oldWidget) {
    return count != oldWidget.count;
  }
}

With the standard approach you must rebuild the widget that holds CounterInherited every time the count changes. Typically that means calling setState in a StatefulWidget parent, which triggers a full rebuild of the subtree above InheritedWidget.

Using ChangeNotifier with InheritedNotifier

A ChangeNotifier is a simple class that maintains a list of listeners and notifies them when state changes. InheritedNotifier connects directly to a ChangeNotifier, so when you call notifyListeners(), dependents rebuild automatically.

Step 1 — Create a ChangeNotifier

class CounterNotifier extends ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }

  void decrement() {
    if (_count > 0) {
      _count--;
      notifyListeners();
    }
  }

  void reset() {
    _count = 0;
    notifyListeners();
  }
}

Step 2 — Create the InheritedNotifier Wrapper

class CounterProvider extends InheritedNotifier<CounterNotifier> {
  const CounterProvider({
    super.key,
    required CounterNotifier super.notifier,
    required super.child,
  });

  static CounterNotifier of(BuildContext context) {
    return context
        .dependOnInheritedWidgetOfExactType<CounterProvider>()!
        .notifier!;
  }
}
Tip: The static of method is a convention that makes accessing the notifier from descendant widgets clean and concise. Always include a null assertion (!) or provide a fallback to catch errors early.

Automatic Rebuild on Notification

The beauty of InheritedNotifier is that you never need to call setState in the parent. The notifier itself drives rebuilds. Here is a complete example wiring everything together:

Complete Counter Example

class CounterApp extends StatefulWidget {
  const CounterApp({super.key});

  @override
  State<CounterApp> createState() => _CounterAppState();
}

class _CounterAppState extends State<CounterApp> {
  final _counter = CounterNotifier();

  @override
  void dispose() {
    _counter.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return CounterProvider(
      notifier: _counter,
      child: const Scaffold(
        body: Center(child: CounterDisplay()),
        floatingActionButton: CounterButtons(),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final counter = CounterProvider.of(context);
    return Text(
      '\${counter.count}',
      style: const TextStyle(fontSize: 48),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final counter = CounterProvider.of(context);
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        FloatingActionButton(
          onPressed: counter.increment,
          child: const Icon(Icons.add),
        ),
        const SizedBox(height: 8),
        FloatingActionButton(
          onPressed: counter.decrement,
          child: const Icon(Icons.remove),
        ),
      ],
    );
  }
}
Warning: Always dispose of your ChangeNotifier in the dispose method of the StatefulWidget that owns it. Failing to do so causes memory leaks because listeners remain registered even after the widget is removed from the tree.

Creating Reusable InheritedNotifier Wrappers

You can create a generic, reusable wrapper that works with any ChangeNotifier subclass. This eliminates boilerplate when you have multiple notifiers in your app.

Generic InheritedNotifier Wrapper

class NotifierProvider<T extends ChangeNotifier>
    extends InheritedNotifier<T> {
  const NotifierProvider({
    super.key,
    required T super.notifier,
    required super.child,
  });

  static T of<T extends ChangeNotifier>(BuildContext context) {
    final provider = context
        .dependOnInheritedWidgetOfExactType<NotifierProvider<T>>();
    assert(provider != null,
        'No NotifierProvider<\$T> found in context');
    return provider!.notifier!;
  }
}

// Usage with any notifier:
// NotifierProvider<CounterNotifier>(notifier: counter, child: ...)
// final counter = NotifierProvider.of<CounterNotifier>(context);

Practical Example: Theme Switcher

A common use case is toggling between light and dark themes. The ThemeNotifier stores the current mode and notifies the widget tree when it changes.

Theme Switcher with InheritedNotifier

class ThemeNotifier extends ChangeNotifier {
  ThemeMode _mode = ThemeMode.light;

  ThemeMode get mode => _mode;
  bool get isDark => _mode == ThemeMode.dark;

  void toggle() {
    _mode = _mode == ThemeMode.light
        ? ThemeMode.dark
        : ThemeMode.light;
    notifyListeners();
  }

  void setMode(ThemeMode newMode) {
    if (_mode != newMode) {
      _mode = newMode;
      notifyListeners();
    }
  }
}

class ThemeProvider extends InheritedNotifier<ThemeNotifier> {
  const ThemeProvider({
    super.key,
    required ThemeNotifier super.notifier,
    required super.child,
  });

  static ThemeNotifier of(BuildContext context) {
    return context
        .dependOnInheritedWidgetOfExactType<ThemeProvider>()!
        .notifier!;
  }
}

// In your root widget:
class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final _theme = ThemeNotifier();

  @override
  void dispose() {
    _theme.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ThemeProvider(
      notifier: _theme,
      child: Builder(
        builder: (ctx) {
          final theme = ThemeProvider.of(ctx);
          return MaterialApp(
            themeMode: theme.mode,
            theme: ThemeData.light(useMaterial3: true),
            darkTheme: ThemeData.dark(useMaterial3: true),
            home: const HomeScreen(),
          );
        },
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final theme = ThemeProvider.of(context);
    return IconButton(
      icon: Icon(theme.isDark ? Icons.light_mode : Icons.dark_mode),
      onPressed: theme.toggle,
    );
  }
}

Practical Example: Shopping Cart

A shopping cart is an excellent real-world example. Multiple widgets need to read the cart contents — the cart badge, the cart page, and the checkout summary.

Shopping Cart with InheritedNotifier

class CartItem {
  final String id;
  final String name;
  final double price;
  int quantity;

  CartItem({
    required this.id,
    required this.name,
    required this.price,
    this.quantity = 1,
  });

  double get total => price * quantity;
}

class CartNotifier extends ChangeNotifier {
  final List<CartItem> _items = [];

  List<CartItem> get items => List.unmodifiable(_items);
  int get itemCount => _items.fold(0, (sum, i) => sum + i.quantity);
  double get totalPrice => _items.fold(0, (sum, i) => sum + i.total);

  void addItem(String id, String name, double price) {
    final index = _items.indexWhere((i) => i.id == id);
    if (index >= 0) {
      _items[index].quantity++;
    } else {
      _items.add(CartItem(id: id, name: name, price: price));
    }
    notifyListeners();
  }

  void removeItem(String id) {
    _items.removeWhere((i) => i.id == id);
    notifyListeners();
  }

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

class CartProvider extends InheritedNotifier<CartNotifier> {
  const CartProvider({
    super.key,
    required CartNotifier super.notifier,
    required super.child,
  });

  static CartNotifier of(BuildContext context) {
    return context
        .dependOnInheritedWidgetOfExactType<CartProvider>()!
        .notifier!;
  }
}

// Cart badge that updates automatically:
class CartBadge extends StatelessWidget {
  const CartBadge({super.key});

  @override
  Widget build(BuildContext context) {
    final cart = CartProvider.of(context);
    return Badge(
      label: Text('\${cart.itemCount}'),
      child: const Icon(Icons.shopping_cart),
    );
  }
}
Tip: Return List.unmodifiable from your notifier’s getter to prevent external code from mutating the list directly. All mutations should go through the notifier’s methods so that notifyListeners() is always called.

Summary

  • InheritedNotifier extends InheritedWidget and automatically listens to a Listenable.
  • It removes the need for updateShouldNotify — rebuilds are driven by notifyListeners().
  • No setState is required in the parent widget; the notifier drives all dependent widget rebuilds.
  • You can create generic, reusable wrappers for any ChangeNotifier subclass.
  • Always dispose of your ChangeNotifier in the owning StatefulWidget’s dispose method.
  • Common use cases include counters, theme switchers, shopping carts, and any shared mutable state.