51 State management (Counter App)
51.1 Prop Drilling
Prop drilling occurs when you need to pass data from a parent widget down through multiple layers of child widgets, even when intermediate widgets don’t actually use that data themselves. Each widget in the chain must accept and forward the data to its children.
This is similar to passing props through multiple React components or passing parameters through nested function calls in Python.
51.1.1 Simple Example
Let’s say we have a counter app where the count value needs to reach a deeply nested button:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());}
class MyApp extends StatefulWidget {
@override
=> _MyAppState();
_MyAppState createState() }
class _MyAppState extends State<MyApp> {
int counter = 0;
void incrementCounter() {
{
setState(() ++;
counter});
}
@override
{
Widget build(BuildContext context) return MaterialApp(
: HomePage(counter: counter, onIncrement: incrementCounter),
home
);}
}
class HomePage extends StatelessWidget {
final int counter;
final VoidCallback onIncrement;
{required this.counter, required this.onIncrement});
HomePage(
@override
{
Widget build(BuildContext context) return Scaffold(
: AppBar(title: Text('Counter App')),
appBar: MainContent(counter: counter, onIncrement: onIncrement),
body
);}
}
class MainContent extends StatelessWidget {
final int counter;
final VoidCallback onIncrement;
{required this.counter, required this.onIncrement});
MainContent(
@override
{
Widget build(BuildContext context) return Center(
: CounterSection(counter: counter, onIncrement: onIncrement),
child
);}
}
class CounterSection extends StatelessWidget {
final int counter;
final VoidCallback onIncrement;
{required this.counter, required this.onIncrement});
CounterSection(
@override
{
Widget build(BuildContext context) return Column(
: MainAxisAlignment.center,
mainAxisAlignment: [
children'Count: $counter'),
Text(: 20),
SizedBox(height: onIncrement),
CounterButton(onIncrement,
]
);}
}
class CounterButton extends StatelessWidget {
final VoidCallback onIncrement;
{required this.onIncrement});
CounterButton(
@override
{
Widget build(BuildContext context) return ElevatedButton(onPressed: onIncrement, child: Text('Increment'));
}
}
51.1.2 Widget Tree Diagram
MyApp (StatefulWidget)
├── counter: 0
├── incrementCounter()
└── HomePage
├── counter: 0 (passed down)
├── onIncrement: incrementCounter (passed down)
└── MainContent
├── counter: 0 (passed down)
├── onIncrement: incrementCounter (passed down)
└── CounterSection
├── counter: 0 (passed down)
├── onIncrement: incrementCounter (passed down)
├── Text('Count: $counter') (uses counter)
└── CounterButton
└── onIncrement: incrementCounter (finally used!)
51.1.3 Data Flow Diagram
State Management (MyApp)
|
| counter: int
| incrementCounter: function
▼
HomePage
|
| (props drilling - just passing through)
▼
MainContent
|
| (props drilling - just passing through)
▼
CounterSection
|
├─────────────────────┐
| |
| counter (used) | onIncrement (passed down)
▼ ▼
Text Widget CounterButton
|
| onIncrement (finally used!)
▼
User taps button
|
▼
Calls incrementCounter()
|
▼
Updates state in MyApp
|
▼
Rebuilds entire tree
51.1.4 Problems with Prop Drilling
Notice how HomePage
and MainContent
don’t actually use the counter or increment function - they just pass it through. This creates several issues:
- Verbose code: Every intermediate widget needs to accept and pass props
- Tight coupling: Changes to data structure require updates to many widgets
- Maintenance burden: Adding new data means updating the entire chain
- Performance: Unnecessary rebuilds of intermediate widgets
This is why Flutter provides state management solutions like Provider, Riverpod, Bloc, and others to avoid prop drilling. These allow widgets to access shared state directly without passing data through every level.
51.2 InheritedWidget Solution
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());}
// InheritedWidget to share counter state
class CounterInheritedWidget extends InheritedWidget {
final int counter;
final VoidCallback onIncrement;
const CounterInheritedWidget({
? key,
Keythis.counter,
required this.onIncrement,
required ,
required Widget child}) : super(key: key, child: child);
// Static method to access the inherited widget from any descendant
static CounterInheritedWidget? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<CounterInheritedWidget>();
}
@override
bool updateShouldNotify(CounterInheritedWidget oldWidget) {
// Rebuild dependents when counter changes
return counter != oldWidget.counter;
}
}
class MyApp extends StatefulWidget {
@override
=> _MyAppState();
_MyAppState createState() }
class _MyAppState extends State<MyApp> {
int counter = 0;
void incrementCounter() {
{
setState(() ++;
counter});
}
@override
{
Widget build(BuildContext context) return MaterialApp(
: CounterInheritedWidget(
home: counter,
counter: incrementCounter,
onIncrement: HomePage(),
child,
)
);}
}
class HomePage extends StatelessWidget {
@override
{
Widget build(BuildContext context) // No need to pass props anymore!
return Scaffold(
: AppBar(title: Text('Counter App')),
appBar: MainContent(),
body
);}
}
class MainContent extends StatelessWidget {
@override
{
Widget build(BuildContext context) // No need to pass props anymore!
return Center(
: CounterSection(),
child
);}
}
class CounterSection extends StatelessWidget {
@override
{
Widget build(BuildContext context) // Access inherited data directly
final inheritedWidget = CounterInheritedWidget.of(context)!;
return Column(
: MainAxisAlignment.center,
mainAxisAlignment: [
children'Count: ${inheritedWidget.counter}'),
Text(: 20),
SizedBox(height,
CounterButton(),
]
);}
}
class CounterButton extends StatelessWidget {
@override
{
Widget build(BuildContext context) // Access inherited callback directly
final inheritedWidget = CounterInheritedWidget.of(context)!;
return ElevatedButton(
: inheritedWidget.onIncrement,
onPressed: Text('Increment'),
child
);}
}
51.2.1 Widget Tree Diagram with InheritedWidget
MyApp (StatefulWidget)
├── counter: 0
├── incrementCounter()
└── CounterInheritedWidget
├── counter: 0 (stored here)
├── onIncrement: incrementCounter (stored here)
└── HomePage
└── MainContent (clean - no props!)
└── CounterSection
├── accesses InheritedWidget ──┐
├── Text('Count: $counter') │
└── CounterButton │
└── accesses InheritedWidget ──┘
│
└── Uses onIncrement callback
51.2.2 Data Flow and Callback Diagram
State Management (MyApp)
|
| counter: int
| incrementCounter: function
▼
CounterInheritedWidget (Data Hub)
|
| Stores and broadcasts:
| - counter
| - onIncrement
|
├─────────────────────────────────────┐
| |
| Widget tree continues | Direct access via
| (no prop drilling!) | context.dependOnInheritedWidget
| |
▼ ▼
HomePage ┌─────────────────┐
| │ Any descendant │
▼ │ widget can: │
MainContent │ 1. Access data │
| │ 2. Get callback│
▼ └─────────────────┘
CounterSection ──────────────────────────┘
| │
├─ Text (reads counter) ◄────────────┘
| │
└─ CounterButton ◄───────────────────┘
|
| User taps → calls onIncrement
▼
incrementCounter() in MyApp
|
▼
setState() updates counter
|
▼
InheritedWidget.updateShouldNotify()
|
▼
Only widgets that depend on InheritedWidget rebuild
51.2.3 How InheritedWidget Works
1. Data Broadcasting:
The CounterInheritedWidget
acts like a radio tower - it broadcasts data to all widgets below it in the tree.
2. Selective Access:
final inheritedWidget = CounterInheritedWidget.of(context)!;
Any descendant widget can “tune in” to get the data directly, without intermediate widgets needing to know about it.
3. Efficient Updates:
@override
bool updateShouldNotify(CounterInheritedWidget oldWidget) {
return counter != oldWidget.counter; // Only rebuild when counter changes
}
Flutter only rebuilds widgets that actually depend on the InheritedWidget when the data changes.
51.2.4 Key Benefits Over Prop Drilling
Clean Intermediate Widgets:
HomePage
andMainContent
are now completely clean- They don’t need to know about or pass any data
- Easier to maintain and test
Direct Access:
- Widgets that need data can access it directly
- No need to thread parameters through multiple layers
- Similar to how you might use global variables in Python, but safer
Performance:
- Only widgets that actually use the data get rebuilt
- Intermediate widgets remain unchanged
- Flutter’s dependency tracking handles the optimization
Flexible Architecture:
- Easy to add new data without changing intermediate widgets
- Widgets can choose what data they need
- Decoupled components
This is the foundation that more advanced state management solutions like Provider are built upon. Provider is essentially a more ergonomic wrapper around InheritedWidget.