Global State
Understanding state that is shared across many parts of your app — the foundation of scalable Flutter applications.
In contrast to local (ephemeral) state , global state (also called app state ) is data that is shared across many parts of your application. It's the state that multiple widgets need to read, update, or react to.
🌐 Global State Defined
Global state is data that is not contained within a single widget. It is accessible to multiple widgets across the widget tree and typically outlives individual widget instances. It often represents the core data of your application, such as user authentication, preferences, or the contents of a shopping cart.
- ✓ Used by multiple widgets in different parts of the app
- ✓ Often persisted across app restarts
- ✓ Managed with state management solutions (Riverpod, Provider, BLoC, etc.)
As your app grows, you'll encounter scenarios where the same piece of data is needed in multiple places. For example:
- User authentication status – needed in the login screen, profile page, and navigation drawer.
- Shopping cart items – accessed in the product list, cart page, and checkout screen.
- Theme settings – used throughout the app to adjust UI colors and styles.
Without a proper way to share this data, you'd be forced to pass it down the widget tree through constructors — a pattern known as prop drilling .
Prop drilling is the practice of passing data from a parent widget down to child widgets that don't actually need the data themselves, but need to forward it to a deeper widget that does.
⚠️ The Downsides of Prop Drilling
- Boilerplate – you have to add parameters to many intermediate widgets.
- Fragile code – changing the data structure requires updating many files.
- Hard to maintain – it's unclear which widgets depend on the data.
- Reduced testability – widgets become tightly coupled to the data flow.
Example of Prop Drilling
class
App
extends
StatelessWidget {
final
User user;
App({
required
this
.user});
@override
Widget
build
(BuildContext context) {
return
HomeScreen(user: user);
// Passing down
}
}
class
HomeScreen
extends
StatelessWidget {
final
User user;
HomeScreen({
required
this
.user});
@override
Widget
build
(BuildContext context) {
return
Scaffold(
body: ProfileWidget(user: user),
// Passing down again
);
}
}
class
ProfileWidget
extends
StatelessWidget {
final
User user;
ProfileWidget({
required
this
.user});
@override
Widget
build
(BuildContext context) {
return
Text(user.name);
// Finally used here
}
}
This is a simple example; imagine a deep tree with 10 intermediate widgets. It's tedious and error-prone.
Flutter provides several ways to make data available to many widgets without prop drilling. These solutions generally fall into three categories:
In this course, we'll focus on Riverpod because it's modern, robust, and recommended by the Flutter team for new projects. However, understanding the concept of global state is independent of the tool you choose.
To illustrate how global state works, here's a minimal example using the Provider package. This is a simple counter that can be accessed from multiple screens.
import
'package:flutter/material.dart'
;
import
'package:provider/provider.dart'
;
class
CounterModel
extends
ChangeNotifier {
int
_count =
0
;
int
get
count => _count;
void
increment
() {
_count++;
notifyListeners();
// 👈 Notify widgets that depend on this model
}
}
void
main
() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
// 👈 Provide the global state
child: MyApp(),
),
);
}
class
MyApp
extends
StatelessWidget {
@override
Widget
build
(BuildContext context) {
return
MaterialApp(
home: HomeScreen(),
);
}
}
class
HomeScreen
extends
StatelessWidget {
@override
Widget
build
(BuildContext context) {
final
counter = Provider.of<CounterModel>(context);
return
Scaffold(
appBar: AppBar(title: Text(
'Global Counter'
)),
body: Center(
child: Column(
children: [
Text(
'Count: ${counter.count}'
),
ElevatedButton(
onPressed: counter.increment,
child: Text(
'Add'
),
),
],
),
),
);
}
}
✅ How This Works
- CounterModel holds the global state and notifies listeners when it changes.
- ChangeNotifierProvider makes the model available to all widgets in its subtree.
- Provider.of<CounterModel>(context) gets the model and listens for changes.
-
When
increment()is called,notifyListeners()triggers a rebuild of any widget that depends on it.
📦 Local State
- Data used by a single widget
- No need to share across screens
- Temporary and not persisted
-
Managed with
setState()
🌐 Global State
- Data used by multiple widgets
- Shared across screens
- Often persisted (e.g., user session)
- Managed with state management solutions
💡 Rule of Thumb
If two or more widgets need to read or modify the same piece of data, it's likely global state . If the data is only relevant to a single widget, it's local state .
Not everything needs to be global. Overusing global state leads to unnecessary rebuilds and makes your app harder to reason about. Keep local state local when possible.
Start with local state and only promote to global when you find yourself sharing data between widgets.
If you're using a model with
ChangeNotifier
, you must call
notifyListeners()
after changing the state, or the UI won't update.
Wrap state mutations in methods that call
notifyListeners()
to ensure the UI reflects the new state.
If your global state holds resources like streams or controllers, you must dispose of them properly to prevent memory leaks.
Implement
dispose()
in your state classes and call it when the widget is removed from the tree.
🎯 Key Takeaway
Global state is shared across many parts of your app and is essential for building scalable applications. Avoid prop drilling by using a state management solution. In this course, we'll use Riverpod, but the concepts apply to any approach. Ask yourself: "Is this data needed in multiple places?"