State management is one of the most critical aspects of building Flutter applications. As your app grows in complexity, managing state effectively becomes crucial for maintaining clean, scalable, and testable code. In this article, we'll explore various state management approaches in Flutter, from simple to sophisticated, with practical examples.
What is State in Flutter?
State refers to any data that can change during the lifetime of your app. This includes:
- User input (form fields, search queries)
- API responses (user data, products list)
- UI state (loading indicators, selected items)
- Application settings (theme, language preferences)
Flutter categorizes state into two types:
- Ephemeral State (Local State): State that only affects a single widget (e.g., current page in a PageView)
- App State (Shared State): State that needs to be shared across multiple widgets (e.g., user authentication status)
1. setState() - The Simplest Approach
setState()
is Flutter's built-in method for managing local state within a StatefulWidget.
When to Use
- Simple, widget-specific state
- Quick prototypes
- State that doesn't need to be shared across widgets
Example: Counter App
import 'package:flutter/material.dart';
class CounterPage extends StatefulWidget {
const CounterPage({Key? key}) : super(key: key);
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int _counter = 0;
bool _isLoading = false;
void _incrementCounter() {
setState(() {
_counter++;
});
}
Future<void> _asyncIncrement() async {
setState(() => _isLoading = true);
// Simulate async operation
await Future.delayed(const Duration(seconds: 1));
setState(() {
_counter++;
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('setState Example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Counter Value:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
if (_isLoading)
const CircularProgressIndicator()
else
ElevatedButton(
onPressed: _asyncIncrement,
child: const Text('Async Increment'),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
child: const Icon(Icons.add),
),
);
}
}
Pros & Cons
✅ Simple and straightforward
✅ No external dependencies
✅ Good for small, isolated components
❌ Difficult to share state
❌ Can lead to prop drilling
❌ Hard to test business logic
2. InheritedWidget - Flutter's Foundation
InheritedWidget is the low-level mechanism that Flutter uses to propagate information down the widget tree. Most state management solutions are built on top of it.
When to Use
- When you need to pass data down the widget tree
- Building your own state management solution
- Understanding how other solutions work under the hood
Example: Theme Manager
import 'package:flutter/material.dart';
// The InheritedWidget
class ThemeProvider extends InheritedWidget {
final ThemeMode themeMode;
final VoidCallback toggleTheme;
const ThemeProvider({
Key? key,
required this.themeMode,
required this.toggleTheme,
required Widget child,
}) : super(key: key, child: child);
static ThemeProvider? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ThemeProvider>();
}
@override
bool updateShouldNotify(ThemeProvider oldWidget) {
return themeMode != oldWidget.themeMode;
}
}
// Stateful wrapper
class ThemeManager extends StatefulWidget {
final Widget child;
const ThemeManager({Key? key, required this.child}) : super(key: key);
@override
State<ThemeManager> createState() => _ThemeManagerState();
}
class _ThemeManagerState extends State<ThemeManager> {
ThemeMode _themeMode = ThemeMode.light;
void _toggleTheme() {
setState(() {
_themeMode = _themeMode == ThemeMode.light
? ThemeMode.dark
: ThemeMode.light;
});
}
@override
Widget build(BuildContext context) {
return ThemeProvider(
themeMode: _themeMode,
toggleTheme: _toggleTheme,
child: widget.child,
);
}
}
// Usage in a widget
class ThemedButton extends StatelessWidget {
const ThemedButton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final themeProvider = ThemeProvider.of(context);
return ElevatedButton(
onPressed: themeProvider?.toggleTheme,
child: Text(
'Current: ${themeProvider?.themeMode == ThemeMode.light ? "Light" : "Dark"}',
),
);
}
}
Pros & Cons
✅ Built into Flutter
✅ Efficient rebuilds
✅ Foundation for understanding other patterns
❌ Verbose boilerplate
❌ Requires wrapper StatefulWidget
❌ Easy to make mistakes
3. Provider - The Recommended Solution
Provider is the officially recommended state management solution by the Flutter team. It's a wrapper around InheritedWidget that's easier to use and more flexible.
When to Use
- Most Flutter applications
- When you need dependency injection
- When you want a balance between simplicity and power
Example: Shopping Cart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// Model
class Product {
final String id;
final String name;
final double price;
Product({required this.id, required this.name, required this.price});
}
// State Management Class
class CartModel extends ChangeNotifier {
final List<Product> _items = [];
List<Product> get items => List.unmodifiable(_items);
int get itemCount => _items.length;
double get totalPrice => _items.fold(0, (sum, item) => sum + item.price);
void addItem(Product product) {
_items.add(product);
notifyListeners();
}
void removeItem(String productId) {
_items.removeWhere((item) => item.id == productId);
notifyListeners();
}
void clear() {
_items.clear();
notifyListeners();
}
}
// Main App Setup
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CartModel(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: const ProductListPage(),
);
}
}
// Product List Page
class ProductListPage extends StatelessWidget {
const ProductListPage({Key? key}) : super(key: key);
final List<Product> products = const [
Product(id: '1', name: 'Laptop', price: 999.99),
Product(id: '2', name: 'Mouse', price: 29.99),
Product(id: '3', name: 'Keyboard', price: 79.99),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Products'),
actions: [
// Using Consumer for specific rebuilds
Consumer<CartModel>(
builder: (context, cart, child) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Text(
'Cart: ${cart.itemCount}',
style: const TextStyle(fontSize: 16),
),
),
);
},
),
IconButton(
icon: const Icon(Icons.shopping_cart),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const CartPage()),
);
},
),
],
),
body: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
title: Text(product.name),
subtitle: Text('\$${product.price.toStringAsFixed(2)}'),
trailing: IconButton(
icon: const Icon(Icons.add_shopping_cart),
onPressed: () {
// Using Provider.of with listen: false for actions
Provider.of<CartModel>(context, listen: false)
.addItem(product);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${product.name} added to cart'),
duration: const Duration(seconds: 1),
),
);
},
),
);
},
),
);
}
}
// Cart Page
class CartPage extends StatelessWidget {
const CartPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Shopping Cart')),
body: Consumer<CartModel>(
builder: (context, cart, child) {
if (cart.itemCount == 0) {
return const Center(
child: Text('Your cart is empty'),
);
}
return Column(
children: [
Expanded(
child: ListView.builder(
itemCount: cart.items.length,
itemBuilder: (context, index) {
final item = cart.items[index];
return ListTile(
title: Text(item.name),
subtitle: Text('\$${item.price.toStringAsFixed(2)}'),
trailing: IconButton(
icon: const Icon(Icons.remove_circle),
onPressed: () => cart.removeItem(item.id),
),
);
},
),
),
Container(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Total:',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Text(
'\$${cart.totalPrice.toStringAsFixed(2)}',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
cart.clear();
Navigator.pop(context);
},
child: const Text('Checkout'),
),
],
),
),
],
);
},
),
);
}
}
Key Provider Patterns
- ChangeNotifierProvider: For classes that extend ChangeNotifier
- Consumer: For widgets that need to rebuild when state changes
-
Provider.of(): For accessing state (use
listen: false
for actions) - MultiProvider: For providing multiple state objects
- ProxyProvider: For providers that depend on other providers
Pros & Cons
✅ Simple and intuitive API
✅ Good performance with granular rebuilds
✅ Officially recommended
✅ Great for dependency injection
❌ Requires understanding of context
❌ Can be verbose for complex state
4. Riverpod - Provider's Evolution
Riverpod is a complete rewrite of Provider that removes its dependency on BuildContext and adds compile-time safety.
When to Use
- New projects
- When you want compile-time safety
- When you need advanced features like state combinations
Example: User Authentication
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Models
class User {
final String id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
}
enum AuthState { initial, loading, authenticated, unauthenticated }
class AuthData {
final AuthState state;
final User? user;
final String? error;
AuthData({required this.state, this.user, this.error});
AuthData copyWith({AuthState? state, User? user, String? error}) {
return AuthData(
state: state ?? this.state,
user: user ?? this.user,
error: error ?? this.error,
);
}
}
// Providers
class AuthNotifier extends StateNotifier<AuthData> {
AuthNotifier() : super(AuthData(state: AuthState.initial));
Future<void> login(String email, String password) async {
state = state.copyWith(state: AuthState.loading);
try {
// Simulate API call
await Future.delayed(const Duration(seconds: 2));
if (email == 'test@example.com' && password == 'password') {
final user = User(
id: '1',
name: 'John Doe',
email: email,
);
state = AuthData(state: AuthState.authenticated, user: user);
} else {
state = AuthData(
state: AuthState.unauthenticated,
error: 'Invalid credentials',
);
}
} catch (e) {
state = AuthData(
state: AuthState.unauthenticated,
error: e.toString(),
);
}
}
void logout() {
state = AuthData(state: AuthState.unauthenticated);
}
}
final authProvider = StateNotifierProvider<AuthNotifier, AuthData>((ref) {
return AuthNotifier();
});
// Computed/Derived state
final isAuthenticatedProvider = Provider<bool>((ref) {
final authData = ref.watch(authProvider);
return authData.state == AuthState.authenticated;
});
// Main App
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Riverpod Auth Example',
home: const AuthWrapper(),
);
}
}
// Auth Wrapper
class AuthWrapper extends ConsumerWidget {
const AuthWrapper({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final isAuthenticated = ref.watch(isAuthenticatedProvider);
return isAuthenticated ? const HomePage() : const LoginPage();
}
}
// Login Page
class LoginPage extends ConsumerStatefulWidget {
const LoginPage({Key? key}) : super(key: key);
@override
ConsumerState<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends ConsumerState<LoginPage> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final authData = ref.watch(authProvider);
final isLoading = authData.state == AuthState.loading;
return Scaffold(
appBar: AppBar(title: const Text('Login')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
hintText: 'test@example.com',
),
enabled: !isLoading,
),
const SizedBox(height: 16),
TextField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Password',
hintText: 'password',
),
obscureText: true,
enabled: !isLoading,
),
const SizedBox(height: 24),
if (authData.error != null)
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text(
authData.error!,
style: const TextStyle(color: Colors.red),
),
),
ElevatedButton(
onPressed: isLoading
? null
: () {
ref.read(authProvider.notifier).login(
_emailController.text,
_passwordController.text,
);
},
child: isLoading
? const CircularProgressIndicator()
: const Text('Login'),
),
],
),
),
);
}
}
// Home Page
class HomePage extends ConsumerWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(authProvider).user;
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () {
ref.read(authProvider.notifier).logout();
},
),
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Welcome, ${user?.name ?? "User"}!',
style: const TextStyle(fontSize: 24),
),
const SizedBox(height: 8),
Text(
user?.email ?? '',
style: const TextStyle(color: Colors.grey),
),
],
),
),
);
}
}
Riverpod Provider Types
- Provider: For immutable values
- StateProvider: For simple state that can change
- StateNotifierProvider: For complex state logic
- FutureProvider: For async operations
- StreamProvider: For streams
Pros & Cons
✅ Compile-time safety
✅ No BuildContext dependency
✅ Better testability
✅ Powerful state composition
❌ Steeper learning curve
❌ Different from standard Flutter patterns
5. BLoC (Business Logic Component)
BLoC pattern uses streams to manage state and is particularly popular in enterprise applications.
When to Use
- Large, complex applications
- When you need strict separation of business logic
- Team with reactive programming experience
Example: Todo List with BLoC
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// Models
class Todo {
final String id;
final String title;
final bool isCompleted;
Todo({
required this.id,
required this.title,
this.isCompleted = false,
});
Todo copyWith({String? id, String? title, bool? isCompleted}) {
return Todo(
id: id ?? this.id,
title: title ?? this.title,
isCompleted: isCompleted ?? this.isCompleted,
);
}
}
// Events
abstract class TodoEvent {}
class AddTodo extends TodoEvent {
final String title;
AddTodo(this.title);
}
class ToggleTodo extends TodoEvent {
final String id;
ToggleTodo(this.id);
}
class DeleteTodo extends TodoEvent {
final String id;
DeleteTodo(this.id);
}
class LoadTodos extends TodoEvent {}
// States
abstract class TodoState {}
class TodoInitial extends TodoState {}
class TodoLoading extends TodoState {}
class TodoLoaded extends TodoState {
final List<Todo> todos;
TodoLoaded(this.todos);
}
class TodoError extends TodoState {
final String message;
TodoError(this.message);
}
// BLoC
class TodoBloc extends Bloc<TodoEvent, TodoState> {
TodoBloc() : super(TodoInitial()) {
on<LoadTodos>(_onLoadTodos);
on<AddTodo>(_onAddTodo);
on<ToggleTodo>(_onToggleTodo);
on<DeleteTodo>(_onDeleteTodo);
}
Future<void> _onLoadTodos(LoadTodos event, Emitter<TodoState> emit) async {
emit(TodoLoading());
try {
// Simulate API call
await Future.delayed(const Duration(seconds: 1));
emit(TodoLoaded([]));
} catch (e) {
emit(TodoError(e.toString()));
}
}
Future<void> _onAddTodo(AddTodo event, Emitter<TodoState> emit) async {
if (state is TodoLoaded) {
final currentState = state as TodoLoaded;
final newTodo = Todo(
id: DateTime.now().toString(),
title: event.title,
);
emit(TodoLoaded([...currentState.todos, newTodo]));
}
}
Future<void> _onToggleTodo(ToggleTodo event, Emitter<TodoState> emit) async {
if (state is TodoLoaded) {
final currentState = state as TodoLoaded;
final updatedTodos = currentState.todos.map((todo) {
return todo.id == event.id
? todo.copyWith(isCompleted: !todo.isCompleted)
: todo;
}).toList();
emit(TodoLoaded(updatedTodos));
}
}
Future<void> _onDeleteTodo(DeleteTodo event, Emitter<TodoState> emit) async {
if (state is TodoLoaded) {
final currentState = state as TodoLoaded;
final updatedTodos = currentState.todos
.where((todo) => todo.id != event.id)
.toList();
emit(TodoLoaded(updatedTodos));
}
}
}
// Main App
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'BLoC Todo Example',
home: BlocProvider(
create: (context) => TodoBloc()..add(LoadTodos()),
child: const TodoPage(),
),
);
}
}
// Todo Page
class TodoPage extends StatelessWidget {
const TodoPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('BLoC Todo List')),
body: BlocBuilder<TodoBloc, TodoState>(
builder: (context, state) {
if (state is TodoLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is TodoError) {
return Center(child: Text('Error: ${state.message}'));
}
if (state is TodoLoaded) {
if (state.todos.isEmpty) {
return const Center(child: Text('No todos yet!'));
}
return ListView.builder(
itemCount: state.todos.length,
itemBuilder: (context, index) {
final todo = state.todos[index];
return ListTile(
leading: Checkbox(
value: todo.isCompleted,
onChanged: (_) {
context.read<TodoBloc>().add(ToggleTodo(todo.id));
},
),
title: Text(
todo.title,
style: TextStyle(
decoration: todo.isCompleted
? TextDecoration.lineThrough
: null,
),
),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
context.read<TodoBloc>().add(DeleteTodo(todo.id));
},
),
);
},
);
}
return const SizedBox.shrink();
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddTodoDialog(context),
child: const Icon(Icons.add),
),
);
}
void _showAddTodoDialog(BuildContext context) {
final controller = TextEditingController();
showDialog(
context: context,
builder: (dialogContext) {
return AlertDialog(
title: const Text('Add Todo'),
content: TextField(
controller: controller,
decoration: const InputDecoration(hintText: 'Enter todo title'),
autofocus: true,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
if (controller.text.isNotEmpty) {
context.read<TodoBloc>().add(AddTodo(controller.text));
Navigator.pop(dialogContext);
}
},
child: const Text('Add'),
),
],
);
},
);
}
}
Pros & Cons
✅ Clear separation of concerns
✅ Highly testable
✅ Great for large teams
✅ Powerful stream-based architecture
❌ Verbose boilerplate
❌ Steep learning curve
❌ Can be overkill for simple apps
6. GetX - The All-in-One Solution
GetX is a lightweight yet powerful state management solution that also includes routing and dependency injection.
When to Use
- Rapid development
- When you want minimal boilerplate
- Simple to medium complexity apps
Example: Theme Switcher with GetX
import 'package:flutter/material.dart';
import 'package:get/get.dart';
// Controller
class ThemeController extends GetxController {
var isDarkMode = false.obs;
var counter = 0.obs;
void toggleTheme() {
isDarkMode.value = !isDarkMode.value;
Get.changeThemeMode(isDarkMode.value ? ThemeMode.dark : ThemeMode.light);
}
void increment() {
counter.value++;
}
void reset() {
counter.value = 0;
}
}
// Main App
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'GetX Example',
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: ThemeMode.light,
home: const HomePage(),
);
}
}
// Home Page
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// Initialize controller
final ThemeController controller = Get.put(ThemeController());
return Scaffold(
appBar: AppBar(
title: const Text('GetX Example'),
actions: [
Obx(() => IconButton(
icon: Icon(
controller.isDarkMode.value
? Icons.light_mode
: Icons.dark_mode,
),
onPressed: controller.toggleTheme,
)),
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Counter Value:',
style: TextStyle(fontSize: 20),
),
Obx(() => Text(
'${controller.counter.value}',
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
),
)),
const SizedBox(height: 32),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: controller.increment,
child: const Text('Increment'),
),
const SizedBox(width: 16),
ElevatedButton(
onPressed: controller.reset,
child: const Text('Reset'),
),
],
),
const SizedBox(height: 32),
ElevatedButton(
onPressed: () {
Get.to(() => const SecondPage());
},
child: const Text('Go to Second Page'),
),
],
),
),
);
}
}
// Second Page (showing persistent state)
class SecondPage extends StatelessWidget {
const SecondPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final ThemeController controller = Get.find();
return Scaffold(
appBar: AppBar(title: const Text('Second Page')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Counter persists across pages:',
style: TextStyle(fontSize: 18),
),
Obx(() => Text(
'${controller.counter.value}',
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
),
)),
const SizedBox(height: 32),
ElevatedButton(
onPressed: () {
Get.back();
},
child: const Text('Go Back'),
),
],
),
),
);
}
}
Pros & Cons
✅ Minimal boilerplate
✅ Fast development
✅ Includes routing and DI
✅ Good performance
❌ Less Flutter-like API
❌ Can lead to tight coupling
❌ Less community support than Provider
Comparison Table
Solution | Complexity | Boilerplate | Learning Curve | Use Case |
---|---|---|---|---|
setState | Low | Minimal | Easy | Simple local state |
InheritedWidget | Medium | High | Medium | Understanding Flutter internals |
Provider | Low-Medium | Low | Easy | Most applications |
Riverpod | Medium | Low-Medium | Medium | Modern, type-safe apps |
BLoC | High | High | Steep | Enterprise apps |
GetX | Low | Minimal | Easy | Rapid development |
Best Practices
1. Start Simple
Don't over-engineer. Use setState()
for truly local state, and graduate to more complex solutions as needed.
2. Separate Business Logic
Keep your business logic separate from your UI code, regardless of which state management solution you choose.
3. Immutable State
Prefer immutable state objects. Use copyWith()
methods for updates.
class AppState {
final bool isLoading;
final List<String> items;
AppState({required this.isLoading, required this.items});
AppState copyWith({bool? isLoading, List<String>? items}) {
return AppState(
isLoading: isLoading ?? this.isLoading,
items: items ?? this.items,
);
}
}
4. Optimize Rebuilds
Use Consumer
, Selector
, or Obx
to rebuild only the widgets that need updating.
// Good - only rebuilds when counter changes
Consumer<CartModel>(
builder: (context, cart, child) => Text('${cart.itemCount}'),
)
// Bad - rebuilds entire widget tree
Provider.of<CartModel>(context).itemCount
5. Test Your State Logic
State management solutions should make testing easier, not harder.
void main() {
test('Counter increments', () {
final controller = CounterController();
expect(controller.counter.value, 0);
controller.increment();
expect(controller.counter.value, 1);
});
}
Conclusion
There's no one-size-fits-all solution for state management in Flutter. The best choice depends on:
- App complexity: Simple apps can use setState, complex apps might need BLoC
- Team experience: Choose what your team knows or can learn quickly
- Project requirements: Consider testability, scalability, and maintainability
- Performance needs: Some solutions are more optimized than others
For most applications, Provider or Riverpod offer the best balance of simplicity and power. They're officially recommended, well-documented, and have strong community support.
Remember: the goal is to write maintainable, testable code that solves real problems. Choose the solution that helps you achieve that goal, not the one that's most popular or complex.
Top comments (0)