6 Ways to Manage State in Flutter Apps (With Code Examples)
How to manage state in Flutter
In this article, we’ll explore six popular ways of managing state in Flutter apps, including real examples and best practices:
State management is one of the most important - and debated - topics in Flutter development. It determines how your app handles changes in data and updates the UI efficiently. Flutter gives you several strategies for managing state — from simple to highly scalable.
The strategies for state management in Flutter are:
- 🧱
setState()
— The built-in, simplest approach - 🏗️ InheritedWidget — Flutter’s foundation for state propagation
- 🪄 Provider — The recommended solution for most apps
- 🔒 Riverpod — Modern, compile-safe evolution of Provider
- 📦 Bloc — For scalable, enterprise-grade applications
- ⚡ GetX — Lightweight all-in-one solution
1. Using setState()
— The Basics
The simplest way to manage state in Flutter is by using the built-in setState()
method in a StatefulWidget
.
This approach is ideal for local UI state — where the state belongs to one widget and doesn’t need to be shared across the app.
Example: Counter App with setState()
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: CounterScreen());
}
}
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('setState Counter')),
body: Center(
child: Text('Count: $_count', style: TextStyle(fontSize: 24)),
),
floatingActionButton: FloatingActionButton(
onPressed: _increment,
child: Icon(Icons.add),
),
);
}
}
Pros
- Very simple to implement
- Great for local or temporary UI state
- No external dependencies
** Cons**
- Doesn’t scale for large apps
- Hard to share state between widgets
- Logic mixed with UI
Use it when:
- Prototyping or building small widgets
- Handling isolated UI state (e.g., toggling a button, showing a modal)
2. InheritedWidget — Flutter’s Foundation
InheritedWidget
is the low-level mechanism that Flutter uses to propagate data down the widget tree. Most state management solutions (including Provider) are built on top of it.
Understanding InheritedWidget helps you grasp how Flutter’s state management works under the hood.
Example: Theme Manager with InheritedWidget
import 'package:flutter/material.dart';
// The InheritedWidget that holds the theme data
class AppTheme extends InheritedWidget {
final bool isDarkMode;
final Function toggleTheme;
const AppTheme({
Key? key,
required this.isDarkMode,
required this.toggleTheme,
required Widget child,
}) : super(key: key, child: child);
// Access method to get the nearest AppTheme up the widget tree
static AppTheme? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<AppTheme>();
}
@override
bool updateShouldNotify(AppTheme oldWidget) {
return isDarkMode != oldWidget.isDarkMode;
}
}
// Stateful wrapper to manage state changes
class ThemeManager extends StatefulWidget {
final Widget child;
const ThemeManager({Key? key, required this.child}) : super(key: key);
@override
_ThemeManagerState createState() => _ThemeManagerState();
}
class _ThemeManagerState extends State<ThemeManager> {
bool _isDarkMode = false;
void _toggleTheme() {
setState(() {
_isDarkMode = !_isDarkMode;
});
}
@override
Widget build(BuildContext context) {
return AppTheme(
isDarkMode: _isDarkMode,
toggleTheme: _toggleTheme,
child: widget.child,
);
}
}
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ThemeManager(
child: Builder(
builder: (context) {
final theme = AppTheme.of(context);
return MaterialApp(
theme: theme!.isDarkMode ? ThemeData.dark() : ThemeData.light(),
home: HomeScreen(),
);
},
),
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = AppTheme.of(context);
return Scaffold(
appBar: AppBar(title: Text('InheritedWidget Theme')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Current Theme: ${theme!.isDarkMode ? "Dark" : "Light"}',
style: TextStyle(fontSize: 20),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => theme.toggleTheme(),
child: Text('Toggle Theme'),
),
],
),
),
);
}
}
Pros
- Built into Flutter — no external dependencies
- Efficient widget rebuilds
- Foundation for understanding other solutions
- Direct control over propagation logic
Cons
- Verbose boilerplate code
- Requires wrapper StatefulWidget
- Easy to make mistakes
- Not beginner-friendly
Use it when:
- Learning how Flutter’s state management works internally
- Building custom state management solutions
- You need very specific control over state propagation
3. Provider — The Flutter-Recommended Solution
When your app’s state needs to be shared between multiple widgets, Provider comes to the rescue.
Provider is based on Inversion of Control — instead of widgets owning the state, a provider exposes it for others to consume. Flutter’s team officially recommends it for medium-sized apps.
Setup
Add this to your pubspec.yaml
:
dependencies:
provider: ^6.0.5
Example: Counter App with Provider
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: MyApp(),
),
);
}
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: CounterScreen());
}
}
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counter = context.watch<CounterModel>();
return Scaffold(
appBar: AppBar(title: Text('Provider Counter')),
body: Center(
child: Text('Count: ${counter.count}', style: TextStyle(fontSize: 24)),
),
floatingActionButton: FloatingActionButton(
onPressed: context.read<CounterModel>().increment,
child: Icon(Icons.add),
),
);
}
}
Pros
- Reactive and efficient updates
- Clean separation of UI and logic
- Well-documented and community-supported
** Cons**
- Slightly more boilerplate than
setState()
- Nested providers can get complex
Use it when:
- You need shared state across multiple widgets
- You want a reactive pattern without complexity
- Your app is growing beyond prototype size
4. Riverpod — Provider’s Modern Evolution
Riverpod is a complete rewrite of Provider that removes its dependency on BuildContext and adds compile-time safety. It’s designed to solve Provider’s limitations while keeping the same philosophy.
Setup
Add this to your pubspec.yaml
:
dependencies:
flutter_riverpod: ^2.4.0
Example: User Profile with Riverpod
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Model
class UserProfile {
final String name;
final int age;
UserProfile({required this.name, required this.age});
UserProfile copyWith({String? name, int? age}) {
return UserProfile(
name: name ?? this.name,
age: age ?? this.age,
);
}
}
// State Notifier
class ProfileNotifier extends StateNotifier<UserProfile> {
ProfileNotifier() : super(UserProfile(name: 'Guest', age: 0));
void updateName(String name) {
state = state.copyWith(name: name);
}
void updateAge(int age) {
state = state.copyWith(age: age);
}
}
// Provider
final profileProvider = StateNotifierProvider<ProfileNotifier, UserProfile>((ref) {
return ProfileNotifier();
});
// Computed provider
final greetingProvider = Provider<String>((ref) {
final profile = ref.watch(profileProvider);
return 'Hello, ${profile.name}! You are ${profile.age} years old.';
});
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: ProfileScreen());
}
}
class ProfileScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final profile = ref.watch(profileProvider);
final greeting = ref.watch(greetingProvider);
return Scaffold(
appBar: AppBar(title: Text('Riverpod Profile')),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(greeting, style: TextStyle(fontSize: 20)),
SizedBox(height: 30),
TextField(
decoration: InputDecoration(labelText: 'Name'),
onChanged: (value) {
ref.read(profileProvider.notifier).updateName(value);
},
),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Age: ${profile.age}', style: TextStyle(fontSize: 18)),
SizedBox(width: 20),
ElevatedButton(
onPressed: () {
ref.read(profileProvider.notifier).updateAge(profile.age + 1);
},
child: Text('+'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () {
if (profile.age > 0) {
ref.read(profileProvider.notifier).updateAge(profile.age - 1);
}
},
child: Text('-'),
),
],
),
],
),
),
);
}
}
Pros
- No BuildContext dependency — access state anywhere
- Compile-time safety — catch errors before runtime
- Better testability
- Powerful state composition
- No memory leaks
Cons
- Different API from traditional Flutter patterns
- Steeper learning curve than Provider
- Smaller community (but growing fast)
Use it when:
- Starting a new Flutter project
- You want type-safety and compile-time guarantees
- Complex state dependencies and compositions
- Working on medium-to-large scale apps
5. BLoC (Business Logic Component) — For Enterprise Apps
The BLoC pattern is a more advanced architecture that completely separates the business logic from the UI using streams.
It’s ideal for large-scale or enterprise applications, where predictable and testable state transitions are essential.
Setup
Add this dependency to your project:
dependencies:
flutter_bloc: ^9.0.0
Example: Counter App with BLoC
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// Events
abstract class CounterEvent {}
class Increment extends CounterEvent {}
// Bloc
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<Increment>((event, emit) => emit(state + 1));
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CounterBloc(),
child: MaterialApp(home: CounterScreen()),
);
}
}
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Bloc Counter')),
body: Center(
child: BlocBuilder<CounterBloc, int>(
builder: (context, count) {
return Text('Count: $count', style: TextStyle(fontSize: 24));
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<CounterBloc>().add(Increment()),
child: Icon(Icons.add),
),
);
}
}
Pros
- Scalable and maintainable for complex apps
- Clear separation of layers
- Easy to unit test
Cons
- More boilerplate
- Learning curve is steeper
Use it when:
- Building enterprise or long-term projects
- You need predictable and testable logic
- Multiple devs work on different app modules
6. GetX — The All-in-One Lightweight Solution
GetX is a minimalist yet powerful state management solution that also includes routing, dependency injection, and utilities. It’s known for having the least boilerplate of all solutions.
Setup
Add this to your pubspec.yaml
:
dependencies:
get: ^4.6.5
Example: Shopping List with GetX
import 'package:flutter/material.dart';
import 'package:get/get.dart';
// Controller
class ShoppingController extends GetxController {
// Observable list
var items = <String>[].obs;
// Observable counter
var totalItems = 0.obs;
void addItem(String item) {
if (item.isNotEmpty) {
items.add(item);
totalItems.value = items.length;
}
}
void removeItem(int index) {
items.removeAt(index);
totalItems.value = items.length;
}
void clearAll() {
items.clear();
totalItems.value = 0;
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'GetX Shopping List',
home: ShoppingScreen(),
);
}
}
class ShoppingScreen extends StatelessWidget {
// Initialize controller
final ShoppingController controller = Get.put(ShoppingController());
final TextEditingController textController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GetX Shopping List'),
actions: [
Obx(() => Padding(
padding: EdgeInsets.all(16),
child: Center(
child: Text(
'Items: ${controller.totalItems}',
style: TextStyle(fontSize: 18),
),
),
)),
],
),
body: Column(
children: [
Padding(
padding: EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: TextField(
controller: textController,
decoration: InputDecoration(
hintText: 'Enter item name',
border: OutlineInputBorder(),
),
),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () {
controller.addItem(textController.text);
textController.clear();
},
child: Icon(Icons.add),
),
],
),
),
Expanded(
child: Obx(() {
if (controller.items.isEmpty) {
return Center(
child: Text('No items in your list'),
);
}
return ListView.builder(
itemCount: controller.items.length,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text(controller.items[index]),
trailing: IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed: () => controller.removeItem(index),
),
);
},
);
}),
),
if (controller.items.isNotEmpty)
Padding(
padding: EdgeInsets.all(16),
child: ElevatedButton(
onPressed: () {
Get.defaultDialog(
title: 'Clear List',
middleText: 'Are you sure you want to clear all items?',
textConfirm: 'Yes',
textCancel: 'No',
onConfirm: () {
controller.clearAll();
Get.back();
},
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
minimumSize: Size(double.infinity, 50),
),
child: Text('Clear All'),
),
),
],
),
);
}
}
Pros
- Minimal boilerplate — fastest development
- All-in-one solution (state, routing, DI, snackbars, dialogs)
- No BuildContext needed
- Very lightweight and fast
- Easy to learn
Cons
- Less “Flutter-like” compared to Provider/BLoC
- Can lead to tight coupling if not careful
- Smaller ecosystem than Provider
- Some developers find it “too magical”
Use it when:
- Rapid prototyping or MVP development
- Small to medium apps
- You want minimal boilerplate
- Team prefers simplicity over strict architecture
Choosing the Right State Management Solution
Solution | Complexity | Boilerplate | Learning Curve | Best For |
---|---|---|---|---|
🧱 setState() |
Low | Minimal | Easy | Simple local state, prototypes |
🏗️ InheritedWidget |
Medium | High | Medium | Learning Flutter internals, custom solutions |
🪄 Provider |
Low-Medium | Low | Easy | Most apps, shared state |
🔒 Riverpod |
Medium | Low-Medium | Medium | Modern apps, type safety |
📦 BLoC |
High | High | Steep | Enterprise apps, complex business logic |
⚡ GetX |
Low | Minimal | Easy | Rapid development, MVPs |
Quick Decision Guide:
App Complexity | Recommended Approach | Example Use Case |
---|---|---|
🪶 Simple | setState() or GetX |
Toggle buttons, animations, small widgets |
⚖️ Medium | Provider or Riverpod |
Shared themes, user settings, data caching |
🏗️ Complex | BLoC or Riverpod |
E-commerce, chat apps, financial dashboards |
Final Thoughts
There’s no one-size-fits-all approach to state management in Flutter.
Here’s a practical approach:
- Start small with
setState()
for simple widgets and prototypes - Learn the fundamentals with InheritedWidget to understand how Flutter works internally
- Graduate to Provider as your app grows and needs shared state
- Consider Riverpod for new projects where type safety and modern patterns matter
- Adopt BLoC for large enterprise applications with complex business logic
- Try GetX when rapid development and minimal boilerplate are priorities
Key Takeaways:
✅ Don’t over-engineer — use setState()
until you need more
✅ Provider and Riverpod are excellent choices for most applications
✅ BLoC shines in large teams and complex apps
✅ GetX is great for speed, but be mindful of coupling
✅ Understanding InheritedWidget helps you master any solution
The key is to balance simplicity, scalability, and maintainability — and choose the right tool for your specific needs, team expertise, and project requirements.
Flutter State Management Libraries Links
- Flutter Official Documentation on State Management
- Provider Package
- Riverpod Documentation
- BLoC Library
- GetX Documentation