Every interactive Flutter application has state — data that changes over time and drives the UI. Managing that state correctly is the difference between a performant, maintainable app and one that is slow, buggy, and hard to extend.
What Is State?
In Flutter, state is data that represents the current condition of the app. When the state changes, Flutter rebuilds the affected parts of the widget tree to reflect the new data.
State Change --> Widget Rebuild --> Updated UI
State comes in two forms:
Ephemeral State
Ephemeral state (also called local or UI state) is temporary state that lives entirely within a single widget. It does not need to be shared with other widgets.
- Whether a checkbox is checked
- The current page in a carousel
- Whether a password field shows or hides text
Ephemeral state is managed with StatefulWidget and setState().
App State
App state (also called shared or global state) is data that needs to be accessible across multiple widgets or screens.
- The current authenticated user
- The items in a shopping cart
- User preferences and settings
- Data fetched from a remote API
App state requires a dedicated state management approach.
Why State Management Matters
Using setState() everywhere works for small apps, but it introduces significant problems as an app grows:
- Tight coupling — UI and business logic are mixed in the same widget
- Redundant rebuilds — calling
setState()rebuilds the entire subtree, even unchanged parts - Hard to test — business logic embedded in widgets cannot be easily unit tested
- Difficult to share — passing state deep through the widget tree (prop drilling) becomes unmanageable
A proper state management approach:
- Separates business logic from UI
- Rebuilds only the widgets that depend on changed data
- Enables independent testing of business logic
- Makes shared state accessible without prop drilling
setState — The Simplest Approach
setState() is the built-in Flutter mechanism for updating ephemeral state. It is available in any StatefulWidget and triggers a rebuild of the widget's subtree.
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _counter = 0;
void _increment() {
setState(() {
_counter++;
});
}
void _decrement() {
setState(() {
_counter--;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Count: $_counter',
style: const TextStyle(fontSize: 32),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(onPressed: _decrement, child: const Text('-')),
const SizedBox(width: 16),
ElevatedButton(onPressed: _increment, child: const Text('+')),
],
),
],
),
),
);
}
}
When to use setState:
- The state is used only by a single widget
- The state change is simple and local
- The app is small and the number of stateful interactions is limited
InheritedWidget — Passing State Down the Tree
InheritedWidget allows data to flow down the widget tree and react to changes. It is the low-level mechanism that powers most higher-level state management solutions.
class AppData extends InheritedWidget {
final int counter;
final VoidCallback increment;
const AppData({
super.key,
required this.counter,
required this.increment,
required super.child,
});
// Widgets that depend on this widget will rebuild when updateShouldNotify returns true
@override
bool updateShouldNotify(covariant AppData oldWidget) {
return oldWidget.counter != counter;
}
// Convenience accessor so descendant widgets can find this data
static AppData? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<AppData>();
}
}
A widget deep in the tree can access the data without it being passed through every intermediate widget:
// Anywhere in the widget subtree:
final data = AppData.of(context);
Text('Count: ${data?.counter}')
InheritedWidget is powerful but verbose to implement directly. Most real apps use a package that builds on top of it.
Provider — The Recommended Approach
Provider is the most widely adopted state management package in the Flutter ecosystem. It is built on InheritedWidget and ChangeNotifier but eliminates the boilerplate.
flutter pub add provider
Define a ChangeNotifier
import 'package:flutter/foundation.dart';
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // Triggers rebuild of all listening widgets
}
void decrement() {
_count--;
notifyListeners();
}
}
Provide the Model
Wrap the widget tree with a ChangeNotifierProvider at the level where the state should be accessible:
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: const MyApp(),
),
);
}
Consume the Model
Use Consumer or context.watch to subscribe to changes:
// Using Consumer (rebuilds only the Consumer's subtree)
Consumer<CounterModel>(
builder: (context, model, child) {
return Text('Count: ${model.count}');
},
)
// Using context.watch (rebuilds the entire build method)
final count = context.watch<CounterModel>().count;
// Using context.read (no subscription — for actions only)
context.read<CounterModel>().increment();
State Management Comparison
| Approach | Complexity | Best For |
|---|---|---|
setState | Low | Single-widget local state |
InheritedWidget | Medium | Manual, low-dependency sharing |
| Provider | Low-Medium | Most apps — simple global state |
| Riverpod | Medium | Testable, composable state |
| Bloc | High | Large apps with complex event-driven logic |
Choosing the Right Approach
Is the state used only by one widget?
|
+-- Yes --> setState()
|
+-- No: Is the app small / medium with simple shared state?
|
+-- Yes --> Provider
|
+-- No: Do you need testable, event-driven state machines?
|
+-- Yes --> Bloc
|
+-- No --> Riverpod
The most important principle: separate your business logic from your UI. Whether you use Provider, Riverpod, or Bloc, the goal is always the same — your widgets should describe what to show, not how to calculate it.