Flutter InheritedWidget Basics
Real apps share data across many screens: the logged-in user, the theme, the shopping cart. Passing that data through every constructor along the widget tree becomes tiring and fragile. InheritedWidget is Flutter's built-in answer. It places data high in the tree and lets any widget below read it directly.
The Problem: Prop Drilling
Imagine a company where the CEO must pass a message through every manager to reach one engineer. Each manager carries a message that does not concern them. Flutter calls this pattern prop drilling.
App (has userName)
└── HomeScreen (receives userName, ignores it)
└── Body (receives userName, ignores it)
└── ProfileCard (finally uses userName)
Each middle widget needs a constructor parameter it never uses. Adding a new value means editing every widget on the path.
The Solution: A Shared Notice Board
An InheritedWidget works like a notice board in the lobby. Anyone in the building walks to the board and reads the notice without help from the managers.
App
└── UserScope (InheritedWidget, holds userName)
└── HomeScreen
└── Body
└── ProfileCard ──── reads userName directly
Building an InheritedWidget
A basic version has three parts: the stored data, a static of method for easy access, and the updateShouldNotify method.
class UserScope extends InheritedWidget {
final String userName;
const UserScope({
super.key,
required this.userName,
required super.child,
});
static UserScope of(BuildContext context) {
final scope = context.dependOnInheritedWidgetOfExactType<UserScope>();
assert(scope != null, 'UserScope not found above this widget');
return scope!;
}
@override
bool updateShouldNotify(UserScope oldWidget) {
return userName != oldWidget.userName;
}
}
Reading Values Below
Any descendant calls UserScope.of(context).
class ProfileCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final name = UserScope.of(context).userName;
return Text('Hello, $name');
}
}
The call registers ProfileCard as a dependent. Flutter rebuilds it whenever the scope changes.
Placing the Scope
Wrap the part of the tree that needs the data.
void main() {
runApp(
UserScope(
userName: 'Asha',
child: MaterialApp(home: HomeScreen()),
),
);
}
How updateShouldNotify Works
When a parent rebuilds and creates a new UserScope object, Flutter compares the old object with the new one through updateShouldNotify. A return value of true rebuilds every dependent widget. A return value of false skips the rebuild and saves work.
New UserScope created
│
▼
updateShouldNotify(old)?
│ │
true false
│ │
rebuild do nothing
dependents
Making Data Change
An InheritedWidget is immutable, so its fields never change after creation. Combine it with a StatefulWidget that holds the changing value and rebuilds the scope with a fresh copy.
class UserHolder extends StatefulWidget {
final Widget child;
const UserHolder({super.key, required this.child});
@override
State<UserHolder> createState() => UserHolderState();
}
class UserHolderState extends State<UserHolder> {
String userName = 'Guest';
void login(String name) {
setState(() {
userName = name;
});
}
@override
Widget build(BuildContext context) {
return UserScope(userName: userName, child: widget.child);
}
}
Built-In Examples
Flutter uses this mechanism everywhere. Theme.of(context), MediaQuery.of(context), and Navigator.of(context) all read from an inherited widget placed by MaterialApp. You have used this pattern since your first app.
Limits of the Raw Approach
| Limit | Impact |
|---|---|
| Lots of boilerplate code | Slows down development |
| Immutable data | Needs a StatefulWidget helper |
| Rebuilds every dependent | Wastes work on large trees |
Why It Matters
Packages such as Provider and Riverpod remove the boilerplate while keeping the same core idea: data lives above, and widgets below subscribe to it. Understanding InheritedWidget explains why those tools behave the way they do, and it helps you read their error messages with confidence.
When to Use It Directly
Most teams reach for a package such as Provider, Riverpod, or Bloc. A raw InheritedWidget still fits in a few cases: a small package that must avoid outside dependencies, a custom theme object, or a learning exercise. Writing one yourself reveals how Flutter propagates data and why context matters.
Practice Ideas
Create a ThemeScope that stores a boolean named isDark. Place it above a MaterialApp, read it in two distant widgets, and add a button that toggles the value through a stateful holder. Watch both widgets update together.
Reading Versus Listening
Widgets sometimes need the data only once, without rebuilding on later changes. The method getInheritedWidgetOfExactType reads the scope without registering a dependency, while dependOnInheritedWidgetOfExactType subscribes the widget to every change. Button handlers usually read once. Build methods subscribe. Choosing the right call avoids pointless rebuilds and keeps screens responsive.
