Flutter Tutorial

Flutter Intermediate Lesson 43 of 135 3 min read

InheritedNotifier in Flutter: Share a ChangeNotifier Down the Tree

Learn InheritedNotifier with an easy example: share a ChangeNotifier with every screen and rebuild only the widgets that use it, with no package.

On this page

InheritedNotifier is an InheritedWidget that is connected to a notifier (ChangeNotifier, ValueNotifier or any Listenable). When the notifier changes, every widget that read it rebuilds automatically.

It gives you the main benefit of the Provider package with about ten lines of your own code.

The three parts

  1. A model that extends ChangeNotifier and calls notifyListeners().
  2. A scope widget that extends InheritedNotifier<YourModel> with a static of(context) helper.
  3. Widgets that call YourScope.of(context) to read the model.

A theme switcher shared by two screens

import 'package:flutter/material.dart';

// 1. The model
class SettingsModel extends ChangeNotifier {
  bool darkMode = false;
  double textScale = 1.0;

  void toggleDark() {
    darkMode = !darkMode;
    notifyListeners();
  }

  void setTextScale(double value) {
    textScale = value;
    notifyListeners();
  }
}

// 2. The scope
class Settings extends InheritedNotifier<SettingsModel> {
  const Settings({super.key, required SettingsModel model, required super.child})
      : super(notifier: model);

  static SettingsModel of(BuildContext context) {
    final scope = context.dependOnInheritedWidgetOfExactType<Settings>();
    assert(scope != null, 'No Settings found above this widget');
    return scope!.notifier!;
  }
}

void main() {
  runApp(Settings(model: SettingsModel(), child: const SettingsApp()));
}

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

  @override
  Widget build(BuildContext context) {
    final settings = Settings.of(context); // rebuilds when settings change
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(colorSchemeSeed: Colors.indigo),
      darkTheme: ThemeData(colorSchemeSeed: Colors.indigo, brightness: Brightness.dark),
      themeMode: settings.darkMode ? ThemeMode.dark : ThemeMode.light,
      builder: (context, child) => MediaQuery(
        data: MediaQuery.of(context).copyWith(
          textScaler: TextScaler.linear(settings.textScale),
        ),
        child: child!,
      ),
      home: const HomePage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: Center(
        child: FilledButton(
          onPressed: () => Navigator.push(
            context,
            MaterialPageRoute(builder: (_) => const SettingsPage()),
          ),
          child: const Text('Open settings'),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final settings = Settings.of(context);
    return Scaffold(
      appBar: AppBar(title: const Text('Settings')),
      body: ListView(
        children: [
          SwitchListTile(
            title: const Text('Dark mode'),
            value: settings.darkMode,
            onChanged: (_) => settings.toggleDark(),
          ),
          ListTile(
            title: const Text('Text size'),
            subtitle: Slider(
              min: 0.8,
              max: 1.6,
              divisions: 4,
              value: settings.textScale,
              onChanged: settings.setTextScale,
            ),
          ),
        ],
      ),
    );
  }
}

Change the switch on the settings screen and the whole app changes theme, because SettingsApp called Settings.of(context). No setState, no package.

How it works

  • dependOnInheritedWidgetOfExactType finds the nearest Settings above the widget and registers the widget as a dependant.
  • InheritedNotifier listens to the notifier. When it fires, Flutter rebuilds every dependant.
  • Widgets that never called of(context) are not rebuilt.

Reading without rebuilding

In a button’s onPressed you only need to call a method, not rebuild. Use getInheritedWidgetOfExactType, which does not register a dependency:

static SettingsModel read(BuildContext context) =>
    context.getInheritedWidgetOfExactType<Settings>()!.notifier!;

This is the same idea as context.read in Provider.

With a ValueNotifier

It works with a single value too:

class CountScope extends InheritedNotifier<ValueNotifier<int>> {
  const CountScope({super.key, required ValueNotifier<int> count, required super.child})
      : super(notifier: count);

  static ValueNotifier<int> of(BuildContext context) =>
      context.dependOnInheritedWidgetOfExactType<CountScope>()!.notifier!;
}

InheritedNotifier vs Provider

InheritedNotifierProvider
Package neededNoYes
Code per modelA small scope classOne line: ChangeNotifierProvider
Disposes the model for youNoYes
Several modelsNest scopes yourselfMultiProvider

For one or two shared models, InheritedNotifier is enough. For many, Provider saves typing.

Try it yourself

Add a fontFamily choice ('Roboto' or 'Georgia') to SettingsModel and apply it in ThemeData(fontFamily: ...).

Practise in the playground Updated