Bloc

Why Use Bloc?

Bloc is built on top of Flutter's Provider and has been marked as a Favorite package by Flutter. It simplifies the code required when using Provider and is currently one of the best state management solutions for Flutter.

Bloc may seem complex for beginners, but compared to simpler state management solutions like GetX, Bloc offers better screen rendering optimization and more convenient state management. Both Bloc and Riverpod are based on Provider, so you can choose a state management tool based on your preference. Bloc also provides a simpler state management approach called Cubit, making it beneficial for both small-scale and large-scale projects.

For large-scale projects, Bloc is commonly used (Bloc concepts), but it requires writing a significant amount of code. To address this, Mason can be used as a solution. I will explain how I automate my Bloc code templates in the next section.

In addition to state management, Bloc offers other useful features that I frequently use, such as:

  • Caching and persisting Bloc states, which is useful for storing application settings.

  • Redo and Undo functionalities for state transitions.

For more details, refer to Bloc Packages.

Code Faster with Mason

You can check out my Bloc template at dr_bloc and create one for yourself. If you are not familiar with Mason, refer to this guide: Boost Your Flutter Development Efficiency with Mason.

Code Faster with Mason

My Bloc is designed as part of the MVVM architecture, where the UI communicates directly with the Bloc. The Bloc is responsible for retrieving data from repositories. You can explore the official Bloc architecture here. If you want to implement it within a clean architecture, check out this article: Maintain and Extend Code Easier with Clean Architecture.

Most of us get used to a specific coding style, but when switching to different projects, rewriting everything from scratch slows down development. For simple modules, Cubit is a great choice, but for larger and more complex modules, we need a more structured and explicit state management approach.

My Bloc Structure

I organize my Bloc into a single folder with two main parts:

  1. Bloc for handling state logic.

  2. Event and state combined into a single file (since they are not too long), reducing unnecessary files.

    • I also use json_serializable to facilitate state persistence using hydrated_bloc and replay_bloc.

This structure allows us to define state enums or default app settings at initialization.


/// Part I: State
// BlocSubject + State
// Learn more: https://bloclibrary.dev/naming-conventions/#single-class
@JsonSerializable(explicitToJson: true)
final class {{name.pascalCase()}}State extends Equatable {
    const {{name.pascalCase()}}State({
      this.user,
    })
    // : Setup Object: https://dart.dev/codelabs/dart-cheatsheet#initializer-lists
        ;
    
    // Default value
    static const {{name.pascalCase()}}State origin = {{name.pascalCase()}}State();
    
    // @JsonKey(
    //   includeFromJson: false,
    //   includeToJson: false
    // )
    final String? user;
    
    {{name.pascalCase()}}State copyWith({
        String? user,
        // User? Function()? user,
    }) {
    return {{name.pascalCase()}}State(
        user: user ?? this.user,
        // user: user != null ? user() : this.user,
        );
    }
    
    @override
    List<dynamic> get props => [
      user,
    ];
    
    factory {{name.pascalCase()}}State.fromJson(MyJsonMap json) => _${{name.pascalCase()}}StateFromJson(json);
    
    MyJsonMap toJson() => _${{name.pascalCase()}}StateToJson(this);
    
    @override
    String toString() {
      return '''{{name.pascalCase()}}State  ${toJson().myPrintMapJson}''';
    }
}

This Section Will Contain Bloc Events

/// Part II: Event
// Learn more: https://bloclibrary.dev/naming-conventions/#anatomy
// BlocSubject + Noun (optional) + Verb (event)
sealed class {{name.pascalCase()}}Event
// extends ReplayEvent
{
  const {{name.pascalCase()}}Event();
}
final class {{name.pascalCase()}}Pressed extends {{name.pascalCase()}}Event {
    const {{name.pascalCase()}}Pressed();
    // final String something;
    // const {{name.pascalCase()}}Pressed({required this.something});
}

This section will contain the Bloc, where you can customize your functions.

/// Part III: Bloc
// Learn more bloc: https://bloclibrary.dev/bloc-concepts/#bloc
class {{name.pascalCase()}}Bloc extends Bloc<{{name.pascalCase()}}Event, {{name.pascalCase()}}State> {
    {{name.pascalCase()}}Bloc() : super({{name.pascalCase()}}State.origin) {
      on<{{name.pascalCase()}}Pressed>(on{{name.pascalCase()}}Pressed);
    }
    
    Future<void> on{{name.pascalCase()}}Pressed({{name.pascalCase()}}Pressed event, Emitter<void> emit) async {
      emit(state);
    }
}

In the Bloc file, I have pre-written code for Cubit. You can uncomment it to use, or remove it if you find it unnecessary.

class {{name.pascalCase()}}Cubit extends Cubit<{{name.pascalCase()}}State> {
    {{name.pascalCase()}}Cubit() : super({{name.pascalCase()}}State.origin);
    
    void on{{name.pascalCase()}}Pressed() {
      emit(state);
    }
    
    @override
    void onChange(Change<{{name.pascalCase()}}State> change) {
      super.onChange(change);
      myLog.trace(change);
    }
    
    @override
    void onError(Object error, StackTrace stackTrace) {
      myLog.trace('$error, $stackTrace');
      super.onError(error, stackTrace);
    }
}

To make the Bloc work, we need to add it to the BlocProvider. Below is an example of declaring a Bloc before using it:

 MultiBlocProvider(
      providers: [
        BlocProvider (
          create: (BuildContext context) => ABloc(),
        ),
        BlocProvider (
          create: (BuildContext context) => BBloc(),
        ),
        ...
      ],
      child: const AppView(),
    );

Make sure to explore Bloc extensions for IDEs like Android Studio to speed up your development process. You can also create live templates to reuse frequently used code snippets efficiently.

Debugging and Logging Bloc Events

This feature is already covered in the Bloc documentation. Typically, it's only necessary for critical Blocs where you need to monitor event flow or debug errors.

/// Logging state area
  @override
  void onChange(Change<{{name.pascalCase()}}State> change) {
    super.onChange(change);
    // myLog.trace(change);
  }

  // capture information about what triggered the state change
  // Learn more: https://bloclibrary.dev/bloc-concepts/#:~:text=One%20key%20differentiating,overriding%20onTransition.
  @override
  void onTransition(Transition<{{name.pascalCase()}}Event, {{name.pascalCase()}}State> transition) {
    super.onTransition(transition);
    myLog.trace(transition);
  }

  @override
  void onEvent({{name.pascalCase()}}Event event) {
    super.onEvent(event);
    // myLog.trace(event);
  }

  @override
  void onError(Object error, StackTrace stackTrace) {
    myLog.trace('$error, $stackTrace');
    super.onError(error, stackTrace);
  }
  

Another way to always listen to these changes without writing them into each individual Bloc is available.

/// {@template app_bloc_observer}
/// Custom [BlocObserver] that observes all bloc and cubit state changes.
/// {@endtemplate}
class OurBlocObserver extends BlocObserver {
/// {@macro app_bloc_observer}
    const OurBlocObserver();
    @override
    void onChange(BlocBase<dynamic> bloc, Change<dynamic> change) {
        super.onChange(bloc, change);
        if (bloc is Cubit) {
          myLog.trace(change.toString().myPrintStringJson, flag: "Blog");
        }
    }

    @override
    void onTransition(
    Bloc<dynamic, dynamic> bloc,
    Transition<dynamic, dynamic> transition,
    ) {
      super.onTransition(bloc, transition);
      // myLog.trace((transition.toString().myToStringJson() ?? "").myPrintStringJson, flag: "Blog");
    }
    
    @override
    void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
      myLog.warning('${bloc.runtimeType} $error $stackTrace');
      super.onError(bloc, error, stackTrace);
    }
}

Call it after the application has fully initialized, as shown below:

WidgetsFlutterBinding.ensureInitialized();

Bloc.observer = const OurBlocObserver();

Persisting Bloc State to Device Storage

To save the Bloc state to device storage for the next app launch, refer to: 🔗 Replay Bloc Documentation

Step 1: Replace Event Definition

Change:

{{name.pascalCase()}}Event

To:

sealed class {{name.pascalCase()}}Event extends ReplayEvent {}

Step 2: Replace Bloc Definition

Change:

{{name.pascalCase()}}Bloc

To:

class {{name.pascalCase()}}Bloc extends HydratedBloc<{{name.pascalCase()}}Event, {{name.pascalCase()}}State> with ReplayBlocMixin {

Then, import these two libraries:

import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:replay_bloc/replay_bloc.dart';

Step 3: Add JSON Serialization Functions

Inside {{name.pascalCase()}}Bloc, implement the following:

@override
{{name.pascalCase()}}State? fromJson(MyJsonMap json) {
  return {{name.pascalCase()}}State.fromJson(json);
}

@override
MyJsonMap? toJson({{name.pascalCase()}}State state) {
  return state.toJson();
}

Step 4: Initialize HydratedBloc After App Launch

Make sure to initialize HydratedBloc after the app has fully loaded. If you're not using MyFlutter, replace it with the appropriate storage directory for your project. More details: Hydrated Bloc Usage

WidgetsFlutterBinding.ensureInitialized();

HydratedBloc.storage = await HydratedStorage.build(
  storageDirectory: kIsWeb
      ? HydratedStorage.webStorageDirectory
      : await MyFlutter.file.appDocumentsDir,
);

Calling Bloc from Anywhere in the Project

Here is an example of how I can access context from anywhere:

final isUsageTimeLimit = myNavigatorKey.currentContext!.read<SettingBloc>().state.setting.otherSetting.isUsageTimeLimit;

Check out my article on accessing context anywhere here to learn how to set it up.

Context Anywhere

This is how I use Bloc in my projects. You can adapt it to create your own code template that fits your projects, helping reduce development time while still leveraging the full power of Bloc. Buy Me a Coffee | Support Me on Ko-fi

Last updated