Using Provider to Simplify InheritedWidget and Control Rebuilds in Flutter
Learn how Provider reduces boilerplate, lets you inject dependencies, and gives fine‑grained control over when widgets rebuild.
14 May 2026, 14:00 UTC

The problem: prop drilling and unnecessary rebuilds
When you need to share data — such as a user theme, authentication token, or a simple counter — across many layers of a Flutter widget tree, the straightforward approach is to pass the value through constructors at each intermediate widget. This "prop drilling" creates boilerplate, makes refactoring harder, and often forces parent widgets to rebuild even when they don’t use the data.
In addition, if you place the shared data high in the tree and call setState on a large parent, the entire subtree may be repainted, hurting performance.
How InheritedWidget solves the data‑passing problem
An InheritedWidget is a special widget that Flutter can look up efficiently via context.dependOnInheritedWidgetOfExactType<T>(). When the data inside an InheritedWidget changes, Flutter notifies only the widgets that actually depend on it, allowing the rest of the tree to stay unchanged.
The downside is the amount of boilerplate you must write: you need to define a subclass, implement updateShouldNotify, and provide a static of method for consumers.
Provider: a wrapper that removes the boilerplate
The provider package (currently at version 6.x) builds on InheritedWidget and gives you a declarative way to provide objects:
ChangeNotifierProvidersupplies aChangeNotifierand callsnotifyListenersfor you.ProviderorFutureProvidercan supply plain values or async results.- Consumers use
context.watch<T>()to rebuild when the value changes, orcontext.read<T>()for a one‑time read that does not trigger rebuilds.
Because the package handles the InheritedWidget subclass and the notification logic, you can focus on the data you want to share.
Worked example: a counter with fine‑grained rebuild control
We’ll create a minimal app that shows a counter value and two buttons: one increments the counter, the other toggles a theme that is unrelated to the counter. Only the counter display should rebuild when the counter changes.
// main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => Counter(),
child: MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Provider Counter')),
body: const Center(child: CounterDisplay()),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () => context.read<Counter>().increment(),
tooltip: 'Increment',
child: const Icon(Icons.add),
),
const SizedBox(height: 12),
FloatingActionButton(
onPressed: () => context.read<ThemeChanger>().toggle(),
tooltip: 'Toggle theme',
child: const Icon(Icons.brightness_6),
),
],
),
),
),
);
}
}
class Counter extends ChangeNotifier {
int _value = 0;
int get value => _value;
void increment() {
_value++;
notifyListeners();
}
}
class ThemeChanger extends ChangeNotifier {
bool _dark = false;
bool get dark => _dark;
void toggle() {
_dark = !_dark;
notifyListeners();
}
}
class CounterDisplay extends StatelessWidget {
const CounterDisplay({super.key});
@override
Widget build(BuildContext context) {
// watch triggers a rebuild only when Counter.value changes
final count = context.watch<Counter>().value;
// read is used for the theme because we don’t want to rebuild on theme toggles
final isDark = context.read<ThemeChanger>().dark;
return Text(
'Count: $count',
style: TextStyle(
fontSize: 32,
color: isDark ? Colors.white : Colors.black,
),
);
}
}
To run the example:
- Open a terminal in your project folder.
- Run
flutter pub add provider(no special permissions needed). - Execute
flutter runon an emulator or device.
While the app is running, open Flutter DevTools → Widget Rebuild Stats. You’ll see that pressing the "Increment" button causes only the CounterDisplay widget to rebuild; pressing the "Toggle theme" button does not trigger a rebuild of the counter display because we used context.read.
Trade‑offs and limitations
Provider is convenient, but it is not a silver bullet:
- Over‑providing at the root: If you place a large, monolithic
ChangeNotifierat the top of the tree and callnotifyListenersfrequently, many widgets thatwatchit will rebuild, potentially causing UI jank. The remedy is to split concerns into multiple providers, each scoped to the subtree that actually needs it. - Misusing
watch: Callingcontext.watch<T>()inside a build method of a widget that does not need to react to the data forces unnecessary rebuilds. Usecontext.readfor one‑off accesses or consider theSelectorwidget to listen to only specific fields. - Understanding the underlying mechanism: When debugging rebuild cycles, knowing that Provider ultimately uses an
InheritedWidgethelps you trace why a widget is being notified.
Practical way to check the result: after any change, verify with DevTools Widget Rebuild Stats or add a temporary print('${runtimeType} built') inside the build method and watch the console.
Actionable takeaway
Start by wrapping the smallest piece of state that multiple widgets need in a Provider scoped to the appropriate subtree. Use context.watch only for UI that must react to changes, and context.read for events like button presses. Keep providers focused and avoid placing large, frequently changing objects at the very root unless you have measured that the rebuild cost is acceptable. This approach gives you the dependency‑injection benefits of InheritedWidget while keeping rebuild performance under control.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.