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
- A model that extends
ChangeNotifierand callsnotifyListeners(). - A scope widget that extends
InheritedNotifier<YourModel>with a staticof(context)helper. - 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
dependOnInheritedWidgetOfExactTypefinds the nearestSettingsabove the widget and registers the widget as a dependant.InheritedNotifierlistens 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
| InheritedNotifier | Provider | |
|---|---|---|
| Package needed | No | Yes |
| Code per model | A small scope class | One line: ChangeNotifierProvider |
| Disposes the model for you | No | Yes |
| Several models | Nest scopes yourself | MultiProvider |
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: ...).