Flutter Lesson 37 of 83 4 min read
State in Flutter: setState and Lifting State Up
Understand state in Flutter: ephemeral versus app state, how setState works, lifting state up to share it, and passing callbacks down.
On this page
State is any data that can change while the app runs and that the UI depends on: the text in a field, whether a box is ticked, the items in a cart, the signed-in user.
Flutter is declarative. You do not write “change this label to 5”. You write a build method that says what the screen looks like for any state, then you change the state and Flutter rebuilds.
UI = build(state)
Two kinds of state #
| Ephemeral state | App state | |
|---|---|---|
| Used by | One widget | Many widgets or screens |
| Examples | Current tab, a checkbox, an animation’s progress | Signed-in user, cart, settings |
| Lives in | A StatefulWidget | Above the widgets that need it |
| Tool | setState | Provider, Riverpod, Bloc and others |
There is no hard line. State often starts in one widget and moves up when a second widget needs it.
The problem: two widgets, one piece of data #
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: ShopPage()));
class ShopPage extends StatefulWidget {
const ShopPage({super.key});
@override
State<ShopPage> createState() => _ShopPageState();
}
class _ShopPageState extends State<ShopPage> {
final List<String> _cart = [];
void _add(String item) => setState(() => _cart.add(item));
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Shop'),
actions: [CartBadge(count: _cart.length)],
),
body: ProductList(onAdd: _add),
);
}
}
class CartBadge extends StatelessWidget {
const CartBadge({super.key, required this.count});
final int count;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(right: 16),
child: Badge(label: Text('$count'), child: const Icon(Icons.shopping_cart)),
);
}
}
class ProductList extends StatelessWidget {
const ProductList({super.key, required this.onAdd});
final ValueChanged<String> onAdd;
@override
Widget build(BuildContext context) {
return ListView(
children: [
for (final name in ['Pen', 'Notebook', 'Bag'])
ListTile(
title: Text(name),
trailing: IconButton(
icon: const Icon(Icons.add_shopping_cart),
onPressed: () => onAdd(name),
),
),
],
);
}
}
The badge and the list are siblings. Neither can own the cart, because the other could not reach it. So the cart lives in their common parent, ShopPage.
Lifting state up #
The pattern has two halves:
- Data flows down. The parent passes values to children through constructors (
count: _cart.length). - Events flow up. Children report what happened through callbacks (
onAdd: _add).
The children are stateless. They only display what they are given and announce what the user did.
How setState works #
- You change a field inside
setState. - Flutter marks that
Stateobject as needing a rebuild. - On the next frame, its
buildruns, and so do thebuildmethods of the children beneath it. - Flutter compares the new widgets with the old and updates only what differs on screen.
Rebuilding is cheap. The real work of layout and painting is done only for what changed.
Where setState stops being enough #
Lifting state works well for a screen. It becomes painful when:
- The state is needed on many screens, so it must live above the navigator, and you pass it through every constructor in between. This is called prop drilling.
- A change near the top rebuilds a very large tree.
- The logic and the UI are tangled in one class, which is hard to test.
The rest of this chapter is about tools that solve these problems. They all do the same two jobs:
- Make state available to widgets far down the tree without passing it by hand.
- Rebuild only the widgets that use the state when it changes.
Keep state out of widgets where you can #
Even with plain setState, move the logic into an ordinary Dart class.
class Cart {
final List<String> _items = [];
List<String> get items => List.unmodifiable(_items);
int get count => _items.length;
void add(String item) => _items.add(item);
void remove(String item) => _items.remove(item);
}
A class like this can be tested without any Flutter code, and it is the first step towards every pattern that follows.
Try it yourself #
Extend the shop example. Add a second screen that lists the cart’s items with a remove button on each, and pass it what it needs from ShopPage. Notice how much has to be handed through the constructor. That discomfort is what the next lessons remove.