Dart Tutorial

Flutter Lesson 38 of 83 3 min read

InheritedWidget in Flutter

Learn how InheritedWidget shares data down the widget tree in Flutter, how of(context) works, and why Provider is built on it.

On this page

You have used inherited widgets since your first app. Theme.of(context) and MediaQuery.sizeOf(context) both find data placed above in the tree, without it being passed through constructors. InheritedWidget is the mechanism behind them, and behind Provider.

What it does #

An InheritedWidget holds data. Any widget below it can ask for that data through context. When the data changes, exactly the widgets that asked are rebuilt.

Writing one #

import 'package:flutter/material.dart';

class AppConfig extends InheritedWidget {
  const AppConfig({
    super.key,
    required this.apiUrl,
    required this.currency,
    required super.child,
  });

  final String apiUrl;
  final String currency;

  // The conventional accessor.
  static AppConfig of(BuildContext context) {
    final config = context.dependOnInheritedWidgetOfExactType<AppConfig>();
    assert(config != null, 'No AppConfig found above this widget');
    return config!;
  }

  @override
  bool updateShouldNotify(AppConfig oldWidget) =>
      apiUrl != oldWidget.apiUrl || currency != oldWidget.currency;
}

class PriceLabel extends StatelessWidget {
  const PriceLabel(this.amount, {super.key});
  final int amount;

  @override
  Widget build(BuildContext context) {
    final currency = AppConfig.of(context).currency; // found from far above
    return Text('$currency $amount', style: const TextStyle(fontSize: 28));
  }
}

void main() {
  runApp(
    const AppConfig(
      apiUrl: 'https://api.example.com',
      currency: 'Rs.',
      child: MaterialApp(
        home: Scaffold(body: Center(child: PriceLabel(1499))),
      ),
    ),
  );
}
PiecePurpose
FieldsThe data being shared
of(context)Looks up the tree for the nearest AppConfig and registers the caller to be rebuilt when it changes
updateShouldNotifyDecides whether dependants need rebuilding when the widget is replaced

PriceLabel did not receive currency through its constructor, nor did any widget between it and the top.

Making the data changeable #

An InheritedWidget is immutable. To change what it holds, wrap it in a StatefulWidget that rebuilds it with new values.

import 'package:flutter/material.dart';

class CounterScope extends InheritedWidget {
  const CounterScope({
    super.key,
    required this.count,
    required this.increment,
    required super.child,
  });

  final int count;
  final VoidCallback increment;

  static CounterScope of(BuildContext context) =>
      context.dependOnInheritedWidgetOfExactType<CounterScope>()!;

  @override
  bool updateShouldNotify(CounterScope oldWidget) => count != oldWidget.count;
}

class CounterHost extends StatefulWidget {
  const CounterHost({super.key, required this.child});
  final Widget child;

  @override
  State<CounterHost> createState() => _CounterHostState();
}

class _CounterHostState extends State<CounterHost> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return CounterScope(
      count: _count,
      increment: () => setState(() => _count++),
      child: widget.child,
    );
  }
}

void main() {
  runApp(
    const CounterHost(
      child: MaterialApp(home: CounterPage()),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    final scope = CounterScope.of(context);
    return Scaffold(
      body: Center(child: Text('${scope.count}', style: const TextStyle(fontSize: 48))),
      floatingActionButton: FloatingActionButton(
        onPressed: scope.increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}

Because widget.child is the same object on every rebuild of CounterHost, Flutter does not rebuild the whole app. Only widgets that called CounterScope.of(context) are rebuilt.

Reading without listening #

dependOnInheritedWidgetOfExactType subscribes the caller to changes. To read once without subscribing, for example in a button’s onPressed, use:

final scope = context.getInheritedWidgetOfExactType<CounterScope>()!;

This is the difference between context.watch and context.read in Provider.

InheritedNotifier #

If your data is a ChangeNotifier or ValueNotifier, InheritedNotifier wires up the rebuilding for you, with no wrapping StatefulWidget.

class CartScope extends InheritedNotifier<CartModel> {
  const CartScope({super.key, required CartModel cart, required super.child})
      : super(notifier: cart);

  static CartModel of(BuildContext context) =>
      context.dependOnInheritedWidgetOfExactType<CartScope>()!.notifier!;
}

Should you write these yourself? #

Rarely. It is a fair amount of code for each piece of state, which is exactly why the provider package exists: it is a friendlier wrapper around InheritedWidget.

It is still worth understanding, because:

  • Theme.of, MediaQuery.of, Navigator.of and Localizations.of all work this way.
  • It explains the error “No X found in context”: you asked from a context that is not below the provider.
  • It explains why context matters, and why you cannot call of(context) in initState.

Try it yourself #

Write a UserScope inherited widget that holds a user’s name and a function to change it. Read the name on two different screens, and change it from a third.

Practise in the playground Updated by Santosh Adhikari