Implementing State Management Patterns
1. Understanding BLoC Pattern
The Business Logic Component (BLoC) pattern is a state management architecture used in Flutter to separate business logic from UI code. It promotes testability, reusability, and maintainability by decoupling the UI from the application's core logic.
| Aspect | Description |
|---|---|
| State Management | Separates UI logic from business logic using streams and events. |
| Data Flow | Unidirectional data flow from events to states. |
| Core Components | Bloc, Event, State |
| Stream-Based | Uses Stream<T> to emit state changes. |
Example: Basic BLoC Implementation
// Event
abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}
// State
abstract class CounterState {}
class CounterInitial extends CounterState {}
class CounterValue extends CounterState {
final int value;
CounterValue(this.value);
}
// Bloc
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterInitial()) {
on<IncrementEvent>((event, emit) {
if (state is CounterValue) {
emit(CounterValue((state as CounterValue).value + 1));
} else {
emit(CounterValue(1));
}
});
}
}
Key Concepts
- Events: Represent user actions or system triggers.
- States: Immutable representations of the UI's current state.
- Bloc: The core component that processes events and emits states.
2. Using flutter_bloc Package
The flutter_bloc package is a popular state management solution for Flutter that follows the BLoC (Business Logic Component) pattern. It helps separate business logic from UI code, making applications more testable and maintainable.
| Feature | Description | Usage |
|---|---|---|
| Bloc | Core class for handling business logic and state changes. | Bloc<Event, State> |
| BlocBuilder | Widget that rebuilds UI based on state changes. | BlocBuilder<CounterBloc, CounterState>(builder: (context, state) {...}) |
| BlocListener | Widget that listens to state changes but does not rebuild UI. | BlocListener<CounterBloc, CounterState>(listener: (context, state) {...}) |
| BlocProvider | Provides a Bloc instance to child widgets. | BlocProvider<CounterBloc>(create: (context) => CounterBloc(), child: ...) |
Example: Basic Counter with flutter_bloc
// CounterBloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
abstract class CounterEvent {}
class Increment extends CounterEvent {}
class Decrement extends CounterEvent {}
abstract class CounterState {}
class CounterInitial extends CounterState {}
class CounterValue extends CounterState {
final int value;
CounterValue(this.value);
}
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterInitial()) {
on<Increment>((event, emit) {
if (state is CounterValue) {
emit(CounterValue(state.value + 1));
} else {
emit(CounterValue(1));
}
});
on<Decrement>((event, emit) {
if (state is CounterValue) {
emit(CounterValue(state.value - 1));
} else {
emit(CounterValue(-1));
}
});
}
}
// CounterPage.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CounterBloc(),
child: Scaffold(
appBar: AppBar(title: Text('Counter')),
body: Center(
child: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
if (state is CounterInitial) {
return Text('0');
} else if (state is CounterValue) {
return Text('${state.value}');
}
return Text('Unknown state');
},
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () => context.read<CounterBloc>().add(Increment()),
child: Icon(Icons.add),
),
FloatingActionButton(
onPressed: () => context.read<CounterBloc>().add(Decrement()),
child: Icon(Icons.remove),
),
],
),
),
);
}
}
flutter_bloc package requires the bloc core library to function. Ensure you have both packages added to your pubspec.yaml.
Key Concepts in flutter_bloc
- BLoC Pattern: Separates business logic from UI.
- Events: Represent user actions or system triggers that cause state changes.
- States: Immutable representations of the application's current state.
- BlocBuilder: Rebuilds UI when the state changes.
- BlocListener: Handles side effects like logging or navigation without rebuilding.
3. Creating Bloc Classes
| Property | Description | Example |
|---|---|---|
| Bloc<Event, State> | Base class for creating Blocs | class CounterBloc extends Bloc<CounterEvent, CounterState> |
| BlocObserver | Observer for Bloc lifecycle events | class MyBlocObserver extends BlocObserver |
Example: Basic Bloc Class
import 'package:flutter_bloc/flutter_bloc.dart';
abstract class CounterEvent {}
class Increment extends CounterEvent {}
class Decrement extends CounterEvent {}
abstract class CounterState {
int get count;
}
class CounterStateInitial implements CounterState {
@override
int get count => 0;
}
class CounterStateIncremented implements CounterState {
final int count;
CounterStateIncremented(this.count);
}
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterStateInitial()) {
on<Increment>((event, emit) {
emit(CounterStateIncremented((state as CounterStateInitial).count + 1));
});
on<Decrement>((event, emit) {
emit(CounterStateIncremented((state as CounterStateInitial).count - 1));
});
}
}
Bloc class requires two generic types: the event type and the state type.
Key Concepts
- Bloc<Event, State> is the base class for all Blocs
- Events are handled using
on<Event> - State changes are emitted using
emit()
4. Using BlocProvider and BlocBuilder
Example: Basic Bloc Setup
// CounterBloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncremented>(increment);
}
void increment(
CounterIncremented event,
Emitter<int> emit,
) {
emit(state + 1);
}
}
abstract class CounterEvent {}
class CounterIncremented extends CounterEvent {}
| Widget | Description | Usage |
|---|---|---|
| BlocProvider | Provides a Bloc instance to its descendants | BlocProvider<CounterBloc>(create: (context) => CounterBloc(), child: ...) |
| BlocBuilder | Rebuilds UI when Bloc state changes | BlocBuilder<CounterBloc, int>(builder: (context, state) => ...) |
Example: Using BlocProvider and BlocBuilder
// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
void main() {
runApp(
BlocProvider(
create: (context) => CounterBloc(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Bloc Example')),
body: Center(
child: BlocBuilder<CounterBloc, int>(
builder: (context, count) {
return Text('Count: $count');
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read<CounterBloc>().add(CounterIncremented());
},
child: Icon(Icons.add),
),
),
);
}
}
BlocProvider is used to provide a Bloc instance to its descendants, and BlocBuilder rebuilds the UI whenever the Bloc's state changes.
Key Points
BlocProvidermanages the lifecycle of a Bloc instance.BlocBuilderlistens to state changes and rebuilds the UI accordingly.- Use
context.read<BlocType>()to access the Bloc instance in widgets that are not direct children ofBlocProvider.
5. Using BlocListener and BlocConsumer
| Feature | Description | Usage |
|---|---|---|
| BlocListener | Used to listen to state changes and perform side effects | Executes a callback whenever the Bloc emits a new state |
| BlocConsumer | Combines BlocListener and BlocBuilder for state updates and side effects | Used to rebuild UI when state changes and perform side effects |
Example: BlocListener for Navigation
BlocListener<AuthBloc, AuthState>(
listener: (context, state) {
if (state is Authenticated) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => HomeScreen()),
);
}
},
child: Scaffold(
body: Center(child: Text('Login Screen')),
),
)
Example: BlocConsumer for UI and Side Effects
BlocConsumer<CounterBloc, CounterState>(
listener: (context, state) {
if (state is CounterIncremented) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Counter: ${state.value}')),
);
}
},
builder: (context, state) {
if (state is CounterInitial) {
return Text('Counter: 0');
}
return Text('Counter: ${state.value}');
},
)
6. Using Riverpod Package
Example: Basic Riverpod Provider Setup
// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomeScreen(),
);
}
}
| Feature | Description | Usage |
|---|---|---|
| Provider | Basic state provider for simple values | final counterProvider = Provider<int>((ref) => 0); |
| AsyncProvider | For asynchronous data fetching | final userProvider = AsyncProvider<User>(fetchUser); |
| StateProvider | For mutable state management | final countProvider = StateProvider<int>((ref) => 0); |
| ChangeNotifierProvider | For state with ChangeNotifier | final notifierProvider = ChangeNotifierProvider<MyNotifier>(ref => MyNotifier()); |
Example: Using StateProvider to Manage Counter
// providers.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
final countProvider = StateProvider<int>((ref) => 0);
// CounterScreen.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class CounterScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(countProvider);
return Scaffold(
appBar: AppBar(title: Text('Riverpod Counter')),
body: Center(
child: Text('Count: $count'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(countProvider.notifier).state++;
},
child: Icon(Icons.add),
),
);
}
}
Example: AsyncProvider for Fetching Data
// services.dart
import 'dart:io';
import 'package:http/http.dart' as http;
Future<String> fetchUserData(String userId) async {
final response = await http.get(Uri.parse('https://api.example.com/users/$userId'));
if (response.statusCode == 200) {
return response.body;
} else {
throw Exception('Failed to load user');
}
}
// providers.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'services.dart';
final userProvider = AsyncProvider<String>((ref) async {
return fetchUserData('123');
});
// UserScreen.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class UserScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider);
return Scaffold(
appBar: AppBar(title: Text('User Data')),
body: switch (userAsync) {
AsyncData(:final value) => Text(value),
AsyncLoading() => Center(child: CircularProgressIndicator()),
AsyncError(:final error) => Text('Error: $error'),
},
);
}
}
Provider. It allows for better testability, no context dependency, and more predictable state updates.
Key Riverpod Concepts
- ProviderScope: The root widget that wraps the app to provide Riverpod context.
- ConsumerWidget: A widget that can read providers using
ref.watch. - StateProvider: For managing mutable state.
- AsyncProvider: For handling asynchronous data fetching.
7. Creating Providers
Example: Basic Provider Setup
// models/user.dart
class User with ChangeNotifier {
String _name = '';
String get name => _name;
void updateName(String newName) {
_name = newName;
notifyListeners();
}
}
// main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'models/user.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => User(),
child: MyApp(),
),
);
}
| Property | Description | Usage |
|---|---|---|
| create | Factory function to create the provider instance | create: (context) => MyProvider() |
| child | Child widget tree that will have access to the provider | child: MyApp() |
ChangeNotifierProvider is part of the provider package. Make sure to add it to your pubspec.yaml.
Example: Using Provider in a Widget
class UserDisplay extends StatelessWidget {
@override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
return Text('Hello, ${user.name}!');
}
}
8. Using GetX Package
GetX is a lightweight and powerful state management solution for Flutter that provides automatic dependency injection, routing, and reactive state management.
| Feature | Description | Usage |
|---|---|---|
| GetX Controller | State management class extending GetxController | |
| Reactive Variables | Automatic UI updates when variables change | |
| Bindings | Dependency injection for controllers | |
Example: Counter with GetX
import 'package:get/get.dart';
class CounterController extends GetxController {
var count = 0.obs;
void increment() => count++;
}
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final controller = Get.put(CounterController());
return Scaffold(
appBar: AppBar(title: Text('GetX Counter')),
body: Center(
child: Obx(() => Text('${controller.count}')),
),
floatingActionButton: FloatingActionButton(
onPressed: controller.increment,
child: Icon(Icons.add),
),
);
}
}
Key Features of GetX
- Reactive Programming - Automatic UI updates
- Dependency Injection - Built-in DI with lazy loading
- Routing - Simple navigation and route management
- State Management - No boilerplate required
9. Using MobX Package
MobX is a reactive state management solution for Flutter that simplifies state handling by automatically tracking dependencies and updating UIs when state changes.
| Feature | Description | Notes |
|---|---|---|
| @observable | Mark fields as observable to track changes | Used with @observable annotation |
| @action | Mark methods that modify state | Ensures proper change tracking |
| @computed | Mark properties that depend on observables | Automatically recomputed when dependencies change |
Example: Basic MobX Store
@observable
int count = 0;
@action
void increment() {
count++;
}
@computed
String get displayText => "Count: $count";
Example: Using Store in Widget
class CounterWidget extends StatelessWidget {
final store = CounterStore();
@override
Widget build(BuildContext context) {
return Obx(() => Text(store.displayText));
}
}
mobx and flutter_mobx packages. Use build_runner to generate required code.
10. Using Redux Pattern
| Concept | Description | Usage |
|---|---|---|
| Store | Centralized state container | Manages global app state |
| Actions | Immutable objects describing state changes | Triggered by user interactions or events |
| Reducers | Pure functions that specify how state changes in response to actions | Return new state based on action type |
Example: Basic Redux Setup
// Store
class AppStore {
final _state = "initial";
String get state => _state;
void dispatch(String action) {
if (action == "UPDATE") {
_state = "updated";
}
}
}
// Action
class UpdateAction {
final String payload;
UpdateAction(this.payload);
}
// Reducer
String reducer(String state, UpdateAction action) {
return action.payload;
}
flutter_redux or redux for full support.
Redux Pattern Benefits
- Centralized state management
- Predictable state transitions
- Easier debugging and testing
- Unidirectional data flow
11. Choosing State Management Solutions
State Management Solution Comparison
- Provider: Built-in, easy to learn, good for small to medium apps. NEW in Flutter 3.19.
- BLoC / Riverpod: More scalable, better for large apps with complex state logic. Riverpod is more flexible than BLoC.
- GetX: Simple and fast, but can be less predictable in large apps. DEPRECATED in favor of Riverpod.
- Redux: Predictable, good for large apps with complex state logic. Requires more boilerplate.
| Solution | Use Case | Pros | Cons |
|---|---|---|---|
| Provider | Small to medium apps | Easy to learn, built-in, good for simple state | Can become unwieldy in large apps |
| BLoC / Riverpod | Large apps with complex logic | Scalable, testable, flexible | Steeper learning curve |
| GetX | Quick prototyping | Fast, simple syntax | Less predictable in large apps |
| Redux | Large apps with complex state logic | Predictable, centralized state | Boilerplate-heavy, steep learning curve |