Flutter InheritedWidget: Share State Without a Package

Coding Liquids blog cover featuring Sagnik Bhattacharya for the Flutter InheritedWidget guide, with a widget tree propagating shared data down to descendants via of(context).
Coding Liquids blog cover featuring Sagnik Bhattacharya for the Flutter InheritedWidget guide.

Passing a value down five layers of widgets through constructors is the kind of chore that makes people reach for a state-management package before they need one. InheritedWidget is the built-in answer: place it once near the top of the tree, and any descendant reads it with a single of(context) call — no constructor threading, no package. It is also the engine under Theme, MediaQuery, and Provider, so learning it makes the whole ecosystem click. This tutorial covers what it solves, how of(context) finds the data, how updateShouldNotify controls rebuilds, a complete working example, the framework's own use of it, and when a package is the better call.

The Complete Flutter Guide course thumbnail

The Complete Flutter Guide: Build Android, iOS and Web apps

Go from scratch to building industry-standard apps with Riverpod, Firebase, animations, REST APIs, and more.

Enrol now

Every snippet below is paste-ready against current stable Flutter. If you want a step-by-step on how lookups crawl the tree first, the BuildContext guide sets up the mental model this tutorial builds on.

Follow me on Instagram@sagnikteaches

I post short Flutter diagrams and tips there most weeks, including how the framework moves data around.

Connect on LinkedInSagnik Bhattacharya

If you'd rather see the widget tree and rebuilds drawn out, the channel walks through these internals on real apps.

Subscribe on YouTube@codingliquids

What InheritedWidget solves

The problem is "prop drilling": a value lives at the top of a screen but is needed deep down, so you pass it through every widget in between, even the ones that ignore it. Each new field means editing constructors all the way down, and refactoring becomes painful.

Concretely, prop drilling looks like this — a theme colour threaded through widgets that do not care about it just to reach the leaf that does:

// Painful: every layer must accept and forward the value
HomePage(accent: accent)
  -> Toolbar(accent: accent)
    -> ActionRow(accent: accent)
      -> SaveButton(accent: accent) // finally used here

An InheritedWidget breaks that chain. You wrap a subtree once, and any descendant reads the value directly — regardless of how many layers sit between them. Crucially, the read also creates a dependency: when the inherited data changes, Flutter rebuilds exactly the widgets that read it and nothing else. That targeted rebuilding is what separates it from a global variable, which could never tell anyone it had changed, and from setState, which rebuilds an entire subtree rather than just the listeners.

An InheritedWidget is also immutable, like every widget. It does not store changing state itself; it carries a snapshot of data down the tree. To make the data change over time, a parent StatefulWidget rebuilds the InheritedWidget with new values — a pattern you will see in the worked example below.

How of(context) finds the data

The convention is a static of method that hides the lookup. Internally it calls context.dependOnInheritedWidgetOfExactType<T>(), which walks up from the calling element to the nearest ancestor of type T.

static MyData of(BuildContext context) {
  final widget =
      context.dependOnInheritedWidgetOfExactType<MyData>();
  assert(widget != null, 'No MyData found in context');
  return widget!;
}

This is not a slow tree crawl. Each element caches a map of its inherited ancestors by type, so the lookup is effectively O(1). The important side effect is that dependOnInheritedWidgetOfExactType registers the calling widget as a dependent. That is what lets Flutter rebuild it later. If you only want to read the value once without subscribing — say inside initState — use getInheritedWidgetOfExactType instead, which looks up without creating a dependency.

The word ExactType matters: the lookup matches the runtime type precisely, not subclasses. If you subclass your inherited widget, descendants asking for the base type will not find the subclass, so always look up the same concrete type you provided. This is also why a clear assert in your of method is worth the line — when no matching ancestor exists, the lookup returns null, and a descriptive message turns a confusing null-dereference into an obvious "you forgot to wrap this subtree" error.

There are two flavours of lookup worth knowing. dependOnInheritedWidgetOfExactType subscribes to the whole widget, so any notified change rebuilds you. For finer control over which changes you care about, Flutter offers InheritedModel, where dependents subscribe to specific named aspects and rebuild only when that aspect changes — the same idea MediaQuery uses so a widget reading only the screen size is not rebuilt when the padding changes.

updateShouldNotify controls rebuilds

When the InheritedWidget is rebuilt with a new instance, Flutter needs to know whether the dependents should rebuild too. It asks your override of updateShouldNotify, passing the old widget. Return true only when the data actually changed.

@override
bool updateShouldNotify(MyData oldWidget) => oldWidget.value != value;

This single method is the performance lever. If you return true unconditionally, every dependent rebuilds on every parent rebuild — back to square one. A cheap comparison keeps rebuilds proportional to real changes. Keep the comparison fast: compare scalars or use value equality, not deep walks over large lists.

A subtle point catches people out here. updateShouldNotify only runs when the InheritedWidget instance is replaced by a new one of the same type in the same position. If you mutate a mutable object the widget holds without rebuilding the widget itself, the method never fires and nothing updates. That is the deeper reason to keep the data immutable and to rebuild the inherited widget with fresh values rather than poking at the old ones in place. When the comparison is non-trivial — say a small config object with several fields — give that object a proper operator == and hashCode (or use a record) so the comparison stays correct and cheap.

The Complete Flutter Guide course thumbnail

Understand Flutter's internals, then ship

The Complete Flutter Guide teaches the widget tree, rebuilds, and state management inside real, production-grade apps.

Enrol now

A complete working example

Here is a self-contained counter shared through an InheritedWidget. A StatefulWidget owns the mutable state and rebuilds the InheritedWidget with a fresh value; descendants read the count and call back to increment it.

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) {
    final scope =
        context.dependOnInheritedWidgetOfExactType<CounterScope>();
    assert(scope != null, 'No CounterScope in context');
    return scope!;
  }

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

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

  @override
  State<CounterProvider> createState() => _CounterProviderState();
}

class _CounterProviderState extends State<CounterProvider> {
  int _count = 0;

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

// Any descendant — no constructor threading needed:
class CountText extends StatelessWidget {
  const CountText({super.key});

  @override
  Widget build(BuildContext context) {
    final scope = CounterScope.of(context);
    return Text('Count: ${scope.count}');
  }
}

Wrap your screen in CounterProvider, drop a CountText anywhere inside it, and call CounterScope.of(context).increment() from a button. Only the widgets that called of(context) rebuild when the count changes — because updateShouldNotify compares the old and new counts.

How Theme and MediaQuery use it

You already use this pattern dozens of times a day. Theme.of(context) and MediaQuery.of(context) are exactly the of convention shown above, backed by an InheritedWidget (an _InheritedTheme and an inherited model for media data) installed high in the tree by MaterialApp.

final colours = Theme.of(context).colorScheme;
final width = MediaQuery.of(context).size.width;

Because these are dependency-registering lookups, a widget that reads MediaQuery.of(context) rebuilds when the screen rotates or the keyboard changes the viewport — the framework propagates the new value through the same machinery your counter just used. That is the whole point: once you understand InheritedWidget, the framework's data flow stops being magic.

When to use a package instead

A raw InheritedWidget is perfect for immutable or rarely changing data: a theme, a logged-in user, a configuration object, a localisation table. The boilerplate is small and there are no dependencies.

For mutable app state it starts to creak. You end up hand-writing the StatefulWidget wrapper, threading callbacks, and disposing controllers yourself. That is precisely what Provider and Riverpod automate — they wrap InheritedWidget and add mutable models, lifecycle disposal, and scoped rebuilds with far less code. For a single observable value, a ValueNotifier is lighter still. Use the raw widget when you want zero dependencies; reach for a package when the state is mutable and shared widely.

A practical heuristic: if the data is read-only from the widgets' point of view and changes through a single parent — a theme, a feature flag, a route argument shared across a screen — the raw InheritedWidget is a clean, dependency-free fit. The moment you find yourself writing increment, add, remove, and load methods on the owning state and forwarding each as a callback, you are reimplementing a package by hand, and a model-based solution will be shorter and less error-prone. Either way the lookup, the dependency registration, and the targeted rebuild you have just learned are exactly what runs underneath — which is why this single widget is worth understanding before you pick a package.

Common mistakes

  • Returning true from updateShouldNotify always. Compare old and new data, or every dependent rebuilds needlessly.
  • Reading of(context) in initState. Dependency lookups belong in build or didChangeDependencies; use getInheritedWidgetOfExactType for a one-off read.
  • Putting mutable state on the InheritedWidget. It must be immutable; keep mutable state in a parent StatefulWidget and rebuild the inherited widget with new values.
  • Expecting it to persist across the whole app. It only covers its subtree — place it above every widget that needs it.
  • Forgetting the null assert in of(context). If no ancestor exists the lookup returns null; assert with a clear message so failures are obvious.

Frequently asked questions

What is an InheritedWidget in Flutter?

It is a built-in widget that exposes data to every descendant without passing it through constructors. Descendants read it with an of(context) lookup, and only the widgets that depend on it rebuild when the data changes. Theme, MediaQuery, and packages like Provider are all built on it.

How does of(context) work?

It calls context.dependOnInheritedWidgetOfExactType, which finds the nearest ancestor of the requested type via a cached per-element map, so the lookup is effectively O(1). It also registers the caller as a dependent, so that widget rebuilds when the inherited data changes.

What does updateShouldNotify do?

It decides whether dependents should rebuild when the InheritedWidget is replaced. Flutter calls it with the old widget, and you return true only if the data actually changed. A cheap comparison such as oldWidget.value != value keeps rebuilds proportional to real changes.

Should I use InheritedWidget or Provider?

Use a raw InheritedWidget for zero dependencies and immutable or rarely changing data. Reach for Provider or Riverpod when you need mutable models, lifecycle disposal, and less boilerplate, since they wrap InheritedWidget and add those conveniences for you.

Further reads

Keep going with the tutorials that pair with this guide:

Sources: Flutter documentation — InheritedWidget, BuildContext.dependOnInheritedWidgetOfExactType, Theme, and MediaQuery (api.flutter.dev). Verified against current stable Flutter.