Flutter Bloc Package

Pub build Star on Github Flutter Website Flutter Samples License: MIT Bloc Library


Bloc Suite is a comprehensive package that extends the functionality of the Flutter Bloc library. It provides additional utilities, widgets, and patterns to simplify state management in Flutter applications.

Features

  • BlocWidget: A base widget that simplifies building UI components that depend on a BLoC.
  • BlocSelectorWidget: A specialized widget that optimizes rebuilds by selecting specific parts of a Bloc's state.
  • LifecycleBloc: A specialized Bloc that automatically handles lifecycle callbacks for events.
  • FlutterBlocObserver: A BlocObserver that provides detailed logging capabilities for Flutter Bloc events and state changes.
  • BlocEventTransformers: A collection of event transformers for handling event streams.
  • ReplayBloc: A specialized Bloc which supports undo and redo operations.

BlocWidget

The BlocWidget is a base widget that simplifies building UI components that depend on a BLoC. It handles the complexity of BLoC subscription and state management internally.

Example

class CounterBloc extends Cubit<int> {
  CounterBloc() : super(0);

  void increment() => emit(state + 1);
}

class CounterWidget extends BlocWidget<CounterBloc, int> {
  const CounterWidget({super.key});

  @override
  Widget build(BuildContext context, CounterBloc bloc, int state) {
    return ...your_code;
  }
}

BlocSelectorWidget

The BlocSelectorWidget is a specialized widget that optimizes rebuilds by selecting specific parts of a Bloc's state.

Example

class CounterState {
  final int counterValue;
  CounterState(this.counterValue);
}

class CounterBloc extends Cubit<CounterState> {
  CounterBloc() : super(CounterState(0));

  void increment() => emit(CounterState(state.counterValue + 1));
}

class CounterScreen extends BlocSelectorWidget<CounterBloc, CounterState, int> {
  CounterScreen() : super(
    bloc: CounterBloc(),
    selector: (state) => state.counterValue,
  );

  @override
  Widget build(context, bloc, value) {
    return ...your_code;
  }
}

LifecycleBloc

The LifecycleBloc is a specialized Bloc that automatically handles lifecycle callbacks for events.

Example

class CounterState {
  final int value;
  const CounterState(this.value);
}

sealed class CounterEvent extends LifecycleEvent {
  const CounterEvent({super.onCompleted,super.onSuccess, super.onError});
}

class Increment extends CounterEvent {
  const Increment({ super.onCompleted, super.onSuccess, super.onError});
}

class CounterBloc extends LifecycleBloc<CounterEvent, CounterState> {
  CounterBloc() : super(CounterState(0)) {
    on<Increment>(
      (event, emit) => emit(CounterState(state.value + 1)),

      // optional
      transformer: BlocEventTransformer.throttle(
        const Duration(milliseconds: 500),
      ),
    );
  }
}

class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final bloc = context.read<CounterBloc>();
    return Scaffold(
      body: Center(
        child: BlocBuilder<CounterBloc, CounterState>(
          builder: (context, state) {
            return Text('Count: ${state.value}');
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => bloc.add(
          Increment(
            onSuccess: () => print('Counter increased'),
            onCompleted: () => print('Operation completed'),
            onError: (error) => print('Error: $error'),
          ),
        ),
        child: Icon(Icons.add),
      ),
    );
  }
}

BlocEventTransformer

The BlocEventTransformer class provides a collection of event transformers that help control the flow of events in BLoC (Business Logic Component) patterns. These transformers allow you to manipulate events before they are processed by the BLoC's event handlers.

Choosing the Right Transformer

Transformer Sequential Distinct Best Use Case
delay ✅ Always 🔄 Configurable Postpone processing (e.g., brief loading indicators).
debounce ✅ Always 🔄 Configurable Wait for inactivity before processing (e.g., search inputs, form validation).
throttle ✅ Always 🔄 Configurable Control frequent events (e.g., scrolling, resizing).
restartable ✅ Always 🔄 Configurable Cancel outdated events when new ones arrive (e.g., live search API calls).
sequential ✅ Always 🔄 Configurable Ensure ordered execution (e.g., sending messages one by one).
droppable ✅ Always 🔄 Configurable Process only the latest event, dropping older ones (e.g., UI animations).
skip 🔄 Configurable 🔄 Configurable Ignore initial events (e.g., first trigger in a lifecycle).
distinct 🔄 Configurable ✅ Always Avoid duplicate event processing (e.g., filtering unchanged user actions).
take 🔄 Configurable 🔄 Configurable Limit the number of events (e.g., first N items in a list).

1. delay

Delays the processing of each event by a specified duration, shifting the entire sequence forward in time.

Parameters:

  • duration: The time to delay each event.
  • distinct: When true, skips events that are equal to the previous event. Default: false

Usage:

on<ExampleEvent>(
  _handleEvent,
  transformer: BlocEventTransformer.delay(const Duration(seconds: 1)),
);

Visual Representation:

Input:  --a--------b---c--->
Output: ----a--------b---c->  (with 1s delay)

2. debounce

Debouncing is useful when the event is frequently being triggered in a short interval of time Useful for handling rapidly firing events like search input.

Parameters:

  • duration: The time window to wait for inactivity.

Usage:

on<SearchQueryEvent>(
  _handleSearchQuery,
  transformer: BlocEventTransformer.debounce(const Duration(milliseconds: 300)),
);

Visual Representation:

Input:  --a-ab-bc--c------d->
Output: --------c---------d->  (with 300ms debounce)

3. throttle

Limits how often an event can be processed.

Parameters:

  • duration: The minimum time between processed events.
  • leading: If true, process the first event in a burst (default: true).
  • trailing: If true, process the last event in a burst (default: false).

Usage:

on<ScrollEvent>(
  _handleScrollEvent,
  transformer: BlocEventTransformer.throttle(
    const Duration(milliseconds: 200),
    trailing: true,
  ),
);

Visual Representation:

Input:  --a-ab-bc--c-------d-->
Output: --a----b----c-------d->  (with 200ms throttle, leading: true)

4. restartable

Cancels any in-progress event processing when a new event arrives. Useful for operations that can be abandoned if newer data is available.

Usage:

on<FetchDataEvent>(
  _handleFetchData,
  transformer: BlocEventTransformer.restartable(),
);

Visual Representation:

Input:     --a-----b---c--->
Processing: --[a]--X
                  --[b]X
                      --[c]--->
Output:     ---------x----x--->

5. sequential

Processes events one after another, ensuring order is maintained. Events are queued and handled in sequence.

Usage:

on<SaveDataEvent>(
  _handleSaveData,
  transformer: BlocEventTransformer.sequential(),
);

Visual Representation:

Input:      --a--b--c-------->
Processing: --[a]--[b]--[c]-->
Output:     ----x----x----x-->

6. droppable

Ignores new events until the current event processing is complete.

Usage:

on<ProcessIntensiveEvent>(
  _handleIntensiveTask,
  transformer: BlocEventTransformer.droppable(),
);

Visual Representation:

Input:      --a--b--c-------->
Processing: ---[a]-------->
Output:     --------x--------->  (b and c are dropped)

7. skip

Skips a specified number of initial events.

Parameters:

  • count: The number of events to skip.

Usage:

on<PageLoadEvent>(
  _handlePageLoad,
  transformer: BlocEventTransformer.skip(1), // Skip first event
);

Visual Representation:

Input:  --a--b--c--d--e-->
Output: -----b--c--d--e-->  (with count: 1)

7. distinct

Skips events if they are equal to the previous event.

Usage:

on<FilterEvent>(
  _handleFilterEvent,
  transformer: BlocEventTransformer.distinct(),
);

Visual Representation:

Input:  --a--a--b--b--c--c-->
Output: --a-----b-----c----->  (duplicate events are skipped)

8. take

Creates an event transformer that takes a specified number of events.

Usage:

on<LoadEvent>(
  _handleLoadEvent,
  transformer: BlocEventTransformer.take(3), // Take first 3 events
);

Visual Representation:

Input:  --a--b--c--d--e-->
Output: --a--b--c-------->  (with limit: 3)

FlutterBlocObserver

The FlutterBlocObserver is a BlocObserver that provides detailed logging capabilities for Flutter Bloc events and state changes.

Example

void main() {
  Bloc.observer = FlutterBlocObserver(
    enabled: true,
    printEvents: true,
    printTransitions: true,
    printChanges: true,
    printCreations: true,
    printClosings: true,
  );
  runApp(MyApp());
}
...more

Flutter Bloc Observer

ReplayBloc

This section is inspired by the replay_bloc package from the official Bloc library. For more detailed information and examples, please refer to the original repository.

Creating a ReplayCubit

class CounterCubit extends ReplayCubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);
}

Using a ReplayCubit

void main() {
  final cubit = CounterCubit();

  // trigger a state change
  cubit.increment();
  print(cubit.state); // 1

  // undo the change
  cubit.undo();
  print(cubit.state); // 0

  // redo the change
  cubit.redo();
  print(cubit.state); // 1
}

ReplayCubitMixin

If you wish to be able to use a ReplayCubit in conjuction with a different type of cubit like HydratedCubit, you can use the ReplayCubitMixin.

class CounterCubit extends HydratedCubit<int> with ReplayCubitMixin {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);

  @override
  int fromJson(Map<String, dynamic> json) => json['value'] as int;

  @override
  Map<String, int> toJson(int state) => {'value': state};
}

Creating a ReplayBloc

class CounterEvent extends ReplayEvent {}
class CounterIncrementPressed extends CounterEvent {}
class CounterDecrementPressed extends CounterEvent {}

class CounterBloc extends ReplayBloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<CounterIncrementPressed>((event, emit) => emit(state + 1));
    on<CounterDecrementPressed>((event, emit) => emit(state - 1));
  }
}

Using a ReplayBloc

void main() {
  // trigger a state change
  final bloc = CounterBloc()..add(CounterIncrementPressed());

  // wait for state to update
  await bloc.stream.first;
  print(bloc.state); // 1

  // undo the change
  bloc.undo();
  print(bloc.state); // 0

  // redo the change
  bloc.redo();
  print(bloc.state); // 1
}

ReplayBlocMixin

If you wish to be able to use a ReplayBloc in conjuction with a different type of cubit like HydratedBloc, you can use the ReplayBlocMixin.

sealed class CounterEvent with ReplayEvent {}
final class CounterIncrementPressed extends CounterEvent {}
final class CounterDecrementPressed extends CounterEvent {}

class CounterBloc extends HydratedBloc<CounterEvent, int> with ReplayBlocMixin {
  CounterBloc() : super(0) {
    on<CounterIncrementPressed>((event, emit) => emit(state + 1));
    on<CounterDecrementPressed>((event, emit) => emit(state - 1));
  }

  @override
  int fromJson(Map<String, dynamic> json) => json['value'] as int;

  @override
  Map<String, int> toJson(int state) => {'value': state};
}

Contributing

Contributions are welcome! Please open an issue or submit a pull request on GitHub.

License

This project is licensed under the MIT License.

Libraries

bloc_suite