Introduction
State management in Flutter is one of those topics that seems simple at first, but gets complicated fast. If you've ever struggled to figure out the best way to share and manage data between widgets or across your app, you're not alone. With Flutter's reactive UI model, choosing the right approach can make your app faster, more maintainable, and easier to test.
This guide will walk you through:
What is State?
In Flutter, state refers to any data that affects the UI. For example:
If data changes and your UI needs to update, its state.
Types of State
Flutter state can be categorized into:
1. Local State
Use setState()
2. App-Wide (Shared) State
Use: A state management solution like Provider, Riverpod, or Bloc
Popular State Management Approaches
1. Best for Local UI State
Simple counter example:
class CounterScreen extends StatefulWidget {
@override
_CounterScreenState createState() => _CounterScreenState();
}
class _CounterScreenState extends State<CounterScreen> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Counter')),
body: Center(child: Text('Count: \$_count')),
floatingActionButton: FloatingActionButton(
onPressed: _increment,
child: Icon(Icons.add),
),
);
}
}
Pros
Cons
2. Provider - Flutter's Official Recommendation
Setup
# pubspec.yaml
dependencies:
flutter:sdk: flutter
provider: ^6.0.0
Create the Provider
class Counter with ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
Use in app
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => Counter(),
child: MyApp(),
),
);
}
In widget
final count = context.watch<Counter>().count;
context.read<Counter>().increment();
Pros
Scales well
Good documentation
Cons
3. Riverpod - A Better Provider
Setup
# pubspec.yaml
dependencies:
flutter_riverpod: ^2.5.1
Define a Provider
final counterProvider = StateProvider<int>((ref) => 0);
Wrap your app
void main() {
runApp(
);
ProviderScope(child: MyApp()),
}
Use in widgets
class HomeScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Scaffold(
body: Center(
child: Text('Count: \$count'),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: Icon(Icons.add),
),
);
}
}
Pros
Cons
4. Bloc - Best for Large-Scale Projects
Bloc (Business Logic Component) uses streams to separate logic and UI.
Setup
# pubspec.yaml
dependencies:
flutter_bloc: ^8.1.1
bloc: ^8.1.0
Create Bloc
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
}
Use BlocProvider and BlocBuilder:
void main() {
runApp(
BlocProvider(
create: (_) => CounterCubit(),
child: MyApp(),
),
);
}
BlocBuilder<CounterCubit, int>(
builder: (context, count) => Text('Count: \$count'),
)
Pros
Highly testable
Clean architecture
Cons
Boilerplate heavy
Learning curve
5. GetX - Quick and Minimal
Setup
# pubspec.yaml
dependencies:
get: ^4.6.5
Controller
class CounterController extends GetxController {
var count = 0.obs;
void increment() => count++;
}
Use in UI
final controller = Get.put(CounterController());
Obx(() => Text('Count: \${controller.count}'))
Pros
Includes routing and DI
Fast and easy
Cons
Summary Table
| Use case | Best option |
|---|
| Simple UI Updates | setState() |
| Shared App-wide state | Provider or Riverpod |
| Enterprise app structure | Bloc |
| Quick MVP or Prototype | GetX |