Flutter setState and Lifting State
You already know that setState() updates a widget. This topic digs deeper — what happens when two separate widgets need to share the same data, and how you solve that with a technique called lifting state up.
The Problem with Local State
Each StatefulWidget manages its own private state. This works when only one widget needs the data. When two sibling widgets need to react to the same change, local state is not enough.
App
├── CartButton (shows item count) ← needs the count
└── ProductList
└── ProductCard
└── "Add to Cart" button ← changes the count
Problem: CartButton and ProductCard are in different branches.
ProductCard's setState() cannot reach CartButton.
The Solution — Lift State Up
Move the shared data to the nearest common parent. The parent owns the state and passes it down to children. Children notify the parent when something changes using callback functions.
BEFORE LIFTING: AFTER LIFTING:
───────────────── ──────────────────────────────
App App ← owns cartCount state
├── CartButton ├── CartButton(count: cartCount)
│ └── [own state] └── ProductList
└── ProductList └── ProductCard(
└── ProductCard onAddToCart: _incrementCount
└── [own state] )
Lifting State — Complete Example
// Parent widget owns the state
class ShoppingApp extends StatefulWidget {
@override
State<ShoppingApp> createState() => _ShoppingAppState();
}
class _ShoppingAppState extends State<ShoppingApp> {
int cartCount = 0; // Shared state lives here
void _addToCart() {
setState(() {
cartCount++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Shop'),
actions: [
CartIcon(count: cartCount), // Passes count down
],
),
body: ProductCard(onAdd: _addToCart), // Passes callback down
);
}
}
// Child 1 — Displays the cart count
class CartIcon extends StatelessWidget {
final int count;
const CartIcon({required this.count});
@override
Widget build(BuildContext context) {
return Stack(
children: [
Icon(Icons.shopping_cart, size: 28),
if (count > 0)
Positioned(
right: 0,
top: 0,
child: CircleAvatar(
radius: 8,
backgroundColor: Colors.red,
child: Text('$count', style: TextStyle(fontSize: 10, color: Colors.white)),
),
),
],
);
}
}
// Child 2 — Triggers the change
class ProductCard extends StatelessWidget {
final VoidCallback onAdd;
const ProductCard({required this.onAdd});
@override
Widget build(BuildContext context) {
return Center(
child: ElevatedButton(
onPressed: onAdd, // Calls parent's _addToCart()
child: Text('Add to Cart'),
),
);
}
}
Data Flow Diagram
_ShoppingAppState (owns cartCount = 2)
│
├──► CartIcon(count: 2) ← data flows DOWN (prop)
│
└──► ProductCard(onAdd: fn) ← callback flows DOWN
│
│ [User taps "Add to Cart"]
│
└──► onAdd() → calls _addToCart() in parent
│
setState(cartCount++)
│
build() runs again
│
CartIcon now shows 3
VoidCallback vs Function
VoidCallback → a function that takes no arguments and returns nothing final VoidCallback onAdd; Function(int) → a function that takes one int argument final Function(int) onQuantityChange; // Calling them onAdd(); onQuantityChange(3);
When setState Runs Too Much
Calling setState() rebuilds the widget and all its children. For large widget trees, this can slow the app. Keep setState calls small and targeted by isolating frequently-changing widgets.
Bad: Large widget rebuilds on every keypress
────────────────────────────────────────────
class BigScreen extends StatefulWidget {
// TextField changes → entire screen rebuilds
}
Better: Isolate the changing part
────────────────────────────────────────────
class BigScreen extends StatelessWidget {
// Static content here — never rebuilds
}
class SearchField extends StatefulWidget {
// Only this small widget rebuilds on keypress
}
Limitations of setState and Lifting State
Lifting state works well for 2–3 levels of widget nesting. When data needs to reach widgets deep in the tree, passing it through every level becomes messy — this is called "prop drilling."
App (owns user data)
└── HomeScreen
└── Header
└── UserAvatar ← needs user data
(data passed through 4 levels of props)
For this situation, use a state management solution like Provider (covered in Topic 16). It lets any widget in the tree read shared data without prop drilling.
Summary
setState()triggers a rebuild of the current widget and its children.- Lift state to the nearest common parent when two sibling widgets share data.
- Pass data down through props; pass change notifications up through callbacks.
- For deeper trees, use Provider or another state management tool.
