Skip to content

State changes you can trace

Keep each operation in its event, derive UI from one explicit state contract, and choose what overlapping work means instead of inheriting accidental concurrency.

FF Bloc is a small foundation over bloc, flutter_bloc, bloc_concurrency, equatable, and GetIt. It keeps application-specific repositories, models, and widgets in your project while standardizing the event-to-state runtime around them.

Event-local operations

Every event implements applyAsync and emits its own state stream. A feature is read by following one event instead of searching a central handler.

Explicit UI state

FFState derives loading, error, empty, or data status and provides exhaustive when plus focused whenOrElse rendering.

Deliberate concurrency

Select sequential, concurrent, droppable, or restartable behavior in the bloc constructor. Sequential remains the compatible default.

Owned lifecycle

The bloc cancels repository subscriptions before closing and implements GetIt's Disposable contract without replacing standard BlocProvider ownership.

Five-minute example

Install the runtime package and the Flutter widgets used by the application:

flutter pub add ff_bloc flutter_bloc

Keep the operation in the event:

class LoadProfileEvent extends ProfileEvent {
  @override
  Stream<ProfileState> applyAsync({required ProfileBloc bloc}) async* {
    yield bloc.state.copyWithoutError(isLoading: true);
    final profile = await bloc.repository.loadProfile();
    yield bloc.state.copyWithoutError(
      isLoading: false,
      data: profile,
    );
  }
}

Choose the overlap policy at the bloc boundary:

class ProfileBloc extends FFBloc<ProfileEvent, ProfileState> {
  ProfileBloc({required this.repository})
      : super(
          initialState: const ProfileState(),
          eventConcurrency: FFEventConcurrency.restartable,
        );

  final ProfileRepository repository;

  @override
  ProfileState onErrorState(Object error) =>
      state.copy(isLoading: false, error: error);
}

Use restartable only for latest-result workflows

Restartable stops listening to the previous event stream. It cannot undo an HTTP request, database write, or other side effect that already started.

One feature, two responsibilities

The event owns the operation

Input, repository call, intermediate loading state, and successful state emissions stay together in applyAsync.

The bloc owns runtime policy

Initial state, concurrency, error conversion, observers, long-lived subscriptions, and disposal stay at the feature boundary.

The UI remains regular flutter_bloc: provide the bloc, dispatch events, and render the derived state.

Choose your next step

Requirements

  • Dart 2.18.4 or newer
  • Flutter 3.3 or newer
  • flutter_bloc 8 or 9

FF Bloc is available under the MIT License.