How to Use Flutter Bloc for State Management in Flutter Apps?

Relia Software

Relia Software

Khang Ho

Relia Software

Software Development

Flutter Bloc is a state management library that helps developers manage and organize an application's states using the BLoC (Business Logic Component) pattern.

How to Use Flutter Bloc for State Management in Flutter Apps?

Table of Contents

In Flutter, state management is crucial. It is a very important aspect for building responsive and dynamic mobile applications, it allows developers to efficiently manage and update the state of their apps. 

There are different ways to approach state management in Flutter, ranging from simple methods like setState to more complex solutions like Provider, Riverpod, and Redux. Among these, Flutter Bloc stands out as a powerful library implementing the BLoC (Business Logic Component) pattern. In this blog, let's delve into the world of Flutter Bloc for mastering state management in your Flutter app development.

>> Read more:

What is Flutter Bloc?

Flutter Bloc is a state management library that helps developers manage and organize an application's states using the BLoC (Business Logic Component) pattern. It separates business logic from the UI, providing better, well-structured and maintainable codebase. Bloc uses events to trigger state changes and streams to emit new states, ensuring a reactive and predictable flow of data throughout the application.

Benefits of Using Flutter Bloc

  • Separation of Concerns: Bloc promotes a clear separation between business logic and UI code. This makes the application easier to manage, test, and maintain.
  • Predictability: By using events to trigger state changes and emitting new states via streams, Bloc ensures that the application's behavior is predictable and easier to debug.
  • Testability: The clear distinction between events and states makes it straightforward to write unit tests for Blocs, improving the reliability of the application.
  • Reactivity: Bloc uses reactive programming principles, allowing the UI to react to state changes in real-time, providing a smooth and dynamic user experience.
  • Scalability: Bloc's modular approach makes it easy to scale the application by adding new features without affecting existing functionality. Each Bloc manages a specific part of the application's state, enabling a well-organized code structure.

Functionalities of Flutter Bloc

  • Event Handling: Events represent user actions, network responses, or other triggers. Blocs receive, process and map them to new states.
  • State Emission: Based on the received events, Blocs emit new states then the UI updates accordingly.
  • BlocProvider: A widget that provides a Bloc to its subtree, ensuring that the Bloc is accessible to the relevant parts of the UI.
  • BlocBuilder: A widget that listens to state changes in a Bloc and rebuilds the UI when a new state is emitted.
  • BlocListener: A widget that listens for state changes and performs side effects (e.g., navigation, showing dialogs) in response to state changes without rebuilding the UI.

Flutter Bloc Components

Events

Events represent the inputs or actions that trigger state changes within an application. Events can include system events, user interactions, or any other occasions that require the application to update its state. Events are the communication mechanism between UI and Bloc, they ensures that the application responds dynamically to different actions.

Types of Events: Events in Bloc applications can be categorized into several types, based on their sources or the nature of the actions they represent. Some common types include:

  • User Interaction Events:
    • Button Presses: e.g., IncrementButtonPressed, SubmitForm.
    • Gestures: e.g., SwipeLeft, Tap.
  • Form Events:
    • Text Field Changes: e.g., EmailChanged, PasswordChanged.
    • Form Submission: e.g., FormSubmitted.
  • Network Requests:
    • API Calls: e.g., FetchData, SubmitData.
    • Responses: e.g., DataLoaded, DataError.
  • System Events:
    • Lifecycle Events: e.g., AppStarted, AppResumed.
    • Connectivity Changes: e.g., ConnectivityChanged.
  • Timer Events:
    • Tick Events: e.g., TimerTicked, TimerCompleted.

Creating Events: Creating custom events for Bloc application is rather simple, here’s a detailed guide on that:

  • Step 1: Define a base event class: Create an abstract or sealed class that can be used as the base for all other events for a specific Bloc. This keeps the events in your app organized nicely in groups.
abstract class CounterEvent {}
  • Step 2: Create specific event classes: Create a class that extends the base event class, each class represents a particular action that the Bloc needs to handle.
class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}
  • Step 3: Add data to the events (if needed): If the actions of the events carry data, include fields and constructor to your events. This allows you to pass necessary information when the event is triggered.
class SubmitFormEvent extends CounterEvent {
	final String username;
	final String password;
	SubmitFormEvent({required this.username, required this.password});
}

States

States are basically the snapshots of your app’s status at specific time. They represent the current state of the UI and hold the data necessary to render it correctly

Immutability of States:

  • Predictability: Immutable states make sure that data remains unchanged after its creation. This means you can rely on the states to be consistent, making it easier to understand the flow of data and how your app behaves.
  • Testability: Immutable states are easier to test because their values always stay the same, no unexpected changes. This allows tests to check for specific state transitions without dealing with any side effects or unintentional changes.
  • Debugging: Since states are snapshots of the application's data at specific points, it’s much easier to debug. You can easily trace back through state changes to find where errors might have occurred.

Achieving Immutability:

  • Using copyWith: copyWith is a common method in Dart to create a new instance of an object with some modified properties while keeping the rest unchanged.
class CounterState {
  final int counter;

  CounterState({required this.counter});

  CounterState copyWith({int? counter}) {
    return CounterState(
      counter: counter ?? this.counter,
    );
  }
}

Usage:

final state = CounterState(counter: 0);
final newState = state.copyWith(counter: state.counter + 1);
  • Using the freezed Package: The freezed package simplifies how you create immutable data classes, it provides build-in support for copyWith and it has other features like equality and serialization.

Step 1: Add freezed and build_runner to your pubspec.yaml:

dependencies:
freezed: ^2.0.0

dev_dependencies:
build_runner: ^2.1.0

Step 2: Define your state class: (don’t worry about the errors the IDE is showing - they will be resolved once you run the code generation command)

import 'package:freezed_annotation/freezed_annotation.dart';

part 'counter_state.freezed.dart';

@freezed
class CounterState with _$CounterState {
  const factory CounterState({required int counter}) = _CounterState;
}

Step 3: Run build_runner command to generate your code:

flutter pub run build_runner build

BlocDelegate

BlocDelegate (or BlocObserver in recent versions of the Bloc library) is a mechanism that allows you to customize and monitor the behavior of all Blocs within your app. It provides hooks for observing events, state transitions, and errors, giving you a centralized way to manage logging, error handling, and other concerns.

Key Functions:

  • onEvent: whenever an event is added to a Bloc
  • onTransition: whenever a state transition occurs in a Bloc
  • onError: whenever an error is thrown in a Bloc

Example Usage:

import 'package:flutter_bloc/flutter_bloc.dart';

class MyBlocObserver extends BlocObserver {
  @override
  void onEvent(Bloc bloc, Object? event) {
    super.onEvent(bloc, event);
    print('Event: $event');
  }

  @override
  void onTransition(Bloc bloc, Transition transition) {
    super.onTransition(bloc, transition);
    print('Transition: $transition');
  }

  @override
  void onError(Bloc bloc, Object error, StackTrace stackTrace) {
    super.onError(bloc, error, stackTrace);
    print('Error: $error');
  }
}

Set it up in the app’s main entry point to use the BlocObserver:

void main() {
  Bloc.observer = MyBlocObserver();
  runApp(MyApp());
}

When to use: BlocDelegate (or BlocObserver) is primarily useful for advanced scenarios and debugging. Here are some cases where it might be beneficial:

  • Debugging and Logging: You can use it to log events and state transitions making it easier to understand how your Blocs behave or to identify issues
  • Global Error Handling: BlocDelegate provide you a global callback to catch and handle errors from any Blocs within your app, this makes it simpler to manage errors.
  • Analytics and Monitoring: Track user interactions and state changes for analytics purposes.
  • Performance Tuning: You can monitor the frequency of state transitions and events, this helps identify potential for performance improvements.

Flutter Bloc Lifecycle

Phase 1: Creation

A Bloc instance is usually created by instantiating the Bloc class with a initial state. This typically happens when the Bloc is first used within the application, such as when a screen is initialized or a widget is mounted.

final myBloc = MyBloc(initialState: InitialState());

Phase 2: Event Handling

  • Receiving Events: add your events by using add()
  • Processing Events: the Bloc will listen to event changes and process them. It also may transform the events, map them to state changes or perform some business logic if needed.
myBloc.add(MyEvent());

Phase 3: State Emission

  • State Calculation: After processing an event, Bloc will calculate a new state based on the event and the current state. This calculation might involve applying some business logic or merging changes with the current state.
  • State Emission: When the new state is created, Bloc will emit it to notify any listeners (such as updating some UI component). This emission will trigger a UI rebuild.
class MyBloc extends Bloc<MyEvent, MyState> {
  MyBloc() : super(InitialState()) {
    on<CounterEvent>((event, emit) {
	    //calculateNewState is your methods to determine a new state
	    //such as increase the counter by 1
	    final newState = calculateNewState();
	    emit(newState);
    });
  }
}

Phase 4: Closing A Bloc

It is important to close a Bloc instance to avoid memory leaks and make sure that resources are released when they’re no longer needed.

  • Dispose Bloc in Widgets: If a Bloc is used in a Stateful widget, you can dispose it in the disposed() method.
@override
void dispose() {
	myBloc.close();
	super.dispose();
}
  • Use Close Method: Explicitly call close() when a Bloc is no longer needed to release resource.
myBloc.close();

By following this lifecycle, you can manage your Bloc instances efficiently, handle events and state properly, ultimately leading to a well-structured and maintainable codebase.

>> Read more: 18 Flutter Project Ideas For Newbie & Experts

Implementing Bloc in A Flutter Project: Step-by-Step Guide

Step 1: Defining Events and States

Events are inputs that drive state changes in your Bloc, while States represent various conditions of your application.

Example: Counter Events and States

  • Define Events:
abstract class CounterEvent {}

class IncrementEvent extends CounterEvent {}

class DecrementEvent extends CounterEvent {}
  • Define States:
abstract class CounterState {
  final int counter;
  const CounterState(this.counter);
}

class CounterInitial extends CounterState {
  const CounterInitial(int counter) : super(counter);
}

class CounterValueChanged extends CounterState {
  const CounterValueChanged(int counter) : super(counter);
}

Step 2: Creating a Bloc Class

The Bloc class contains business logic that handles incoming events and updates states accordingly.

Example: CounterBloc

import 'package:bloc/bloc.dart';

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(const CounterInitial(0)) {
	  on<IncrementEvent>((event, emit) {
		  emit(state.counter + 1);
	  });
	  
	  on<DecrementEvent>((event, emit) {
		  emit(state.counter - 1);
	  });
  }
}

Step 3: Event Handling and State Updates

Events are added to the Bloc to trigger state changes, and the Bloc processes these events to update the state.

Example: Adding Events

// Adding an increment event
counterBloc.add(IncrementEvent());

// Adding a decrement event
counterBloc.add(DecrementEvent());

Step 4: Building the UI with BlocProvider

Use BlocProvider to add the Bloc to the widget tree, and BlocBuilder to rebuild the UI based on state changes.

Example: Counter UI

  • Setup BlocProvider in the main widget:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider(
        create: (context) => CounterBloc(),
        child: CounterPage(),
      ),
    );
  }
}
  • Build the UI with BlocBuilder:
class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counterBloc = BlocProvider.of<CounterBloc>(context);

    return Scaffold(
      appBar: AppBar(title: Text('Counter')),
      body: Center(
        child: BlocBuilder<CounterBloc, CounterState>(
          builder: (context, state) {
            if (state is CounterInitial || state is CounterValueChanged) {
              return Text('Counter: ${state.counter}');
            }
            return CircularProgressIndicator();
          },
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () => counterBloc.add(IncrementEvent()),
            child: Icon(Icons.add),
          ),
          SizedBox(height: 8),
          FloatingActionButton(
            onPressed: () => counterBloc.add(DecrementEvent()),
            child: Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}

Advanced Bloc Concepts

MultiBlocProvider: Managing Multiple Blocs

MultiBlocProvider is a widget provided by the flutter_bloc package that allows you to merge multiple Bloc instances to a subtree of your widget hierarchy. MultiBlocProvider improves the readability and eliminates the need to nest multiple BlocProviders. This is useful for managing various Blocs in a structured and scalable way, especially in complex applications where different parts of the UI rely on different Blocs.

Use Case:

Use MultiBlocProvider when you need multiple Bloc instances to manage different aspects of your state. For example, you might have a CounterBloc to manage counter-related state and a UserBloc to manage user-related state. Instead of nesting multiple BlocProvider widgets, you can use a single MultiBlocProvider to provide both Blocs efficiently.

Example

Use MultiBlocProvider to go from:

BlocProvider<BlocA>(
  create: (BuildContext context) => BlocA(),
  child: BlocProvider<BlocB>(
    create: (BuildContext context) => BlocB(),
    child: BlocProvider<BlocC>(
      create: (BuildContext context) => BlocC(),
      child: ChildA(),
    )
  )
)

to:

MultiBlocProvider(
  providers: [
    BlocProvider<BlocA>(
      create: (BuildContext context) => BlocA(),
    ),
    BlocProvider<BlocB>(
      create: (BuildContext context) => BlocB(),
    ),
    BlocProvider<BlocC>(
      create: (BuildContext context) => BlocC(),
    ),
  ],
  child: ChildA(),
)

BlocProvider: Simplifying Bloc Access in the UI

BlocProvider is a widget that allows developer to inject Bloc instances into the widget tree, making them available to child widgets. This provides access to the Bloc instances for any widget within the subtree without the need to manually pass them through the widget hierachy.

Benefits:

  • No Manual Passing: Reduces boilerplate code and makes the widget tree cleaner, simpler and easier to manage.
  • Easy Access: Child widgets can easily access the Bloc instance using the context.read<BlocType>() or context.watch<BlocType>() methods, promoting a clean and easy-to-read codebase.

Best Practices:

  • Create Bloc Instances at the Appropriate Level:

Highest Level Needed: Create Bloc instances at the highest level in the widget tree where they are needed by multiple children. This makes sure that all necessary widgets will have access to them.

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        BlocProvider<CounterBloc>(
          create: (context) => CounterBloc(),
        ),
        BlocProvider<UserBloc>(
          create: (context) => UserBloc()..add(LoadUserEvent()),
        ),
      ],
      child: MaterialApp(
        home: HomePage(),
      ),
    );
  }
}
  • Consider Using a Provider Package for More Complex Scenarios:

For more advanced dependency management scenarios, consider using the provider package in conjunction with flutter_bloc. This combination can help manage complex dependency injection and ensure that Blocs and other dependencies are properly provided and disposed of.

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        Provider(create: (context) => SomeDependency()),
        BlocProvider<CounterBloc>(
          create: (context) => CounterBloc(),
        ),
        BlocProvider<UserBloc>(
          create: (context) => UserBloc()..add(LoadUserEvent()),
        ),
      ],
      child: MaterialApp(
        home: HomePage(),
      ),
    );
  }
}

Real-World Examples

Fetching Data App

This example demonstrates how to use Bloc for fetching data from an API and displaying it in the UI.

Step 1: Define Events and States

  • Post Events:
abstract class PostEvent {}

class FetchPosts extends PostEvent {}
  • Post States:
abstract class PostState {}

class PostInitial extends PostState {}

class PostLoading extends PostState {}

class PostLoaded extends PostState {
  final List<Post> posts;
  PostLoaded(this.posts);
}

class PostError extends PostState {
  final String message;
  PostError(this.message);
}

class Post {
  final int id;
  final String title;
  final String body;

  Post({required this.id, required this.title, required this.body});

  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      id: json['id'],
      title: json['title'],
      body: json['body'],
    );
  }
}

Step 2: Create PostBloc

import 'package:bloc/bloc.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;

class PostBloc extends Bloc<PostEvent, PostState> {
  PostBloc() : super(PostInitial()) {
	  on<FetchPosts>((event, emit) async {
		  emit(PostLoading();
		  try {
			  final posts = await _fetchPosts();
        emit(PostLoaded(posts));
		  } catch (e) {
			  emit(PostError("Failed to fetch posts");
      }
	  });
  }

  Future<List<Post>> _fetchPosts() async {
    final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'));
    if (response.statusCode == 200) {
      final List<dynamic> postJson = json.decode(response.body);
      return postJson.map((json) => Post.fromJson(json)).toList();
    } else {
      throw Exception('Failed to load posts');
    }
  }
}

Step 3: Build the UI with BlocProvider

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider(
        create: (context) => PostBloc()..add(FetchPosts()),
        child: PostPage(),
      ),
    );
  }
}

class PostPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Posts')),
      body: BlocBuilder<PostBloc, PostState>(
        builder: (context, state) {
          if (state is PostInitial) {
            return Center(child: Text('Please wait...'));
          } else if (state is PostLoading) {
            return Center(child: CircularProgressIndicator());
          } else if (state is PostLoaded) {
            return ListView.builder(
              itemCount: state.posts.length,
              itemBuilder: (context, index) {
                final post = state.posts[index];
                return ListTile(
                  title: Text(post.title),
                  subtitle: Text(post.body),
                );
              },
            );
          } else if (state is PostError) {
            return Center(child: Text(state.message));
          } else {
            return Container();
          }
        },
      ),
    );
  }
}

>> You may consider: 

Conclusion

Bloc simplifies state management in Flutter apps. It uses Events (user actions, network calls) to trigger State changes (app data updates). Blocs process these Events, update States, and keep the UI in sync. This approach offers a well-structured, maintainable, and scalable architecture with benefits like: clear separation of concerns (business logic vs UI); predictable and testable state changes; reactive UI updates with streams; and easy scaling for new features. Overall, Bloc empowers developers to build robust and maintainable Flutter applications.

If you need comprehensive documents, here are resources for further learning:

>>> Follow and Contact Relia Software for more information!

  • coding
  • Mobile App Development