Skip to main content

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));
      }
    });
  }
}
Note: The BLoC pattern is particularly useful in large-scale applications where complex state logic needs to be managed efficiently.

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),
            ),
          ],
        ),
      ),
    );
  }
}
Note: The 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));
    });
  }
}
Note: The 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),
        ),
      ),
    );
  }
}
Note: BlocProvider is used to provide a Bloc instance to its descendants, and BlocBuilder rebuilds the UI whenever the Bloc's state changes.

Key Points

  • BlocProvider manages the lifecycle of a Bloc instance.
  • BlocBuilder listens to state changes and rebuilds the UI accordingly.
  • Use context.read<BlocType>() to access the Bloc instance in widgets that are not direct children of BlocProvider.

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}');
  },
)
Note: BlocListener is ideal for side effects like navigation or showing snack bars, while BlocConsumer is useful when you need both UI updates and side effects in one widget.

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'),
      },
    );
  }
}
Note: Riverpod is a state management solution that provides a more flexible and powerful alternative to 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()
Note: The 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
class CounterController extends GetxController {
  var count = 0;
  void increment() => count++;
}
Reactive Variables Automatic UI updates when variables change
var name = ''.obs;
void updateName(String value) => name.value = value;
Bindings Dependency injection for controllers
class HomeBinding extends Bindings {
  @override
  void dependencies() {
    Bind.lazySingleton<CounterController>(() => CounterController());
  }
}

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),
      ),
    );
  }
}
Note: GetX provides automatic widget rebuilds when reactive variables change, making it easy to build reactive UIs without manually managing subscriptions.

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));
  }
}
Note: MobX requires the mobx and flutter_mobx packages. Use build_runner to generate required code.
Warning: MobX is not as commonly used in modern Flutter apps compared to Riverpod or Bloc. Consider alternatives for new projects.

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;
}
Note: Flutter's Redux implementation typically uses packages like 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

Choosing the Right Solution

  1. Evaluate app size and complexity
  2. Consider team familiarity with patterns
  3. Assess performance and scalability needs
  4. Test with a small feature or component

Was this article helpful?