BLoC — what is it, Business Logic Component in Flutter

Author: IT Sectr Published: 2026-02-19 Reading time: 7 min

BLoC (Business Logic Component) — a state management pattern for Flutter, introduced by Google in 2018 at DartConf. BLoC separates business logic from the user interface through reactive streams (Stream): the UI sends an Event, BLoC processes it and returns a new State through Stream. According to pub.dev, the flutter_bloc package has gained over 11 thousand likes and is used in thousands of Flutter applications.

Key takeaways

  • Event — an input signal describing an action: button press, data loading
  • State — the output UI state: data loaded, error, loading
  • Bloc — the main class that receives Event and returns State via Stream
  • Cubit — a simplified version of Bloc without Event, calling functions directly
  • BlocProvider — a Flutter widget for injecting Bloc into the widget tree

What is BLoC?

BLoC (Business Logic Component) — an architectural pattern for Flutter in which business logic is extracted into a separate class, isolated from the UI. BLoC receives input data through a stream of events (Event) and produces output data through a stream of states (State). The presentation layer (Widget) only subscribes to the State stream and renders the UI, never executing business logic directly.

The BLoC concept is based on reactive programming and the Observer pattern. Each BLoC component is a separate module with a clear contract: a known set of Events (what can happen) and a known set of States (what can be displayed). A developer cannot "accidentally" change the state from the UI — only through a specific Event. This makes the code predictable and testable.

According to the Flutter Community 2025 survey, BLoC ranks second in popularity among state management solutions in Flutter after Provider. Key advantages: strong typing, logic isolation, built-in Stream support, rich ecosystem of utilities (BlocProvider, BlocListener, BlocSelector).

BLoC architecture: Event → Bloc → State

BLoC architecture is built around three entities: Event (input), Bloc (handler) and State (output). The Widget sends an Event via the add() method. Bloc receives the Event in the mapEventToState or on<Event> method, executes business logic and emits a new State via yield. The Widget receives the State through a Stream and rebuilds.

Dart
abstract class CounterEvent {}

class Increment extends CounterEvent {}
class Decrement extends CounterEvent {}

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0);

  @override
  Stream<int> mapEventToState(CounterEvent event) async* {
    if (event is Increment) {
      yield state + 1;
    } else if (event is Decrement) {
      yield state - 1;
    }
  }
}

Type safety: Bloc is parameterized with two types — Event and State. The Dart compiler checks that the Widget only calls declared Events and that Bloc only returns declared States. Runtime errors like "unknown Action" are eliminated.

Close and Dispose: Bloc implements the Closeable interface. When a widget is destroyed, Bloc automatically closes the Stream via the close() method. Reactive subscription leaks are impossible — BlocProvider manages the Bloc lifecycle, binding it to a route or page.

Bloc and Cubit: comparison

Cubit is a simplified implementation of Bloc without Event, introduced in the flutter_bloc 6.0 package. Cubit declares methods directly instead of Event classes: increment(), fetchData(). Internally, Cubit uses the same Stream-based mechanism but hides the Event layer. This reduces boilerplate by 40-50% for simple scenarios.

CharacteristicBlocCubit
Event classesRequiredNot needed
BoilerplateHighLow
Action trackingVia Event typeMethod name only
Best forComplex scenariosSimple states
AnalyticsAutomatic via EventManual

When to choose Cubit: state with 2-3 variants (loading, loaded, error), simple forms, counters, UI states (open/closed). When to choose Bloc: complex business logic with many actions: order processing, authorization, data synchronization. Bloc provides detailed tracing of each action through Event — every call is logged in BlocObserver.

BlocObserver — a global observer that tracks all Bloc and Cubit in the application. It allows logging Event, State, errors and transitions. Simply connect one instance: Bloc.observer = AppBlocObserver(), and the entire application state tracing is available centrally.

BlocProvider and BlocBuilder

BlocProvider — an InheritedWidget from flutter_bloc that provides a Bloc to child widgets. When a widget is initialized, BlocProvider creates a Bloc, and when destroyed — automatically closes it via close(). BlocProvider can be placed at the MaterialApp level (global Bloc) or at a specific route level (local Bloc).

Dart
BlocProvider(
  create: (context) => CounterBloc(),
  child: Column(
    children: [
      BlocBuilder<CounterBloc, int>(
        builder: (context, state) => Text('$state'),
      ),
      ElevatedButton(
        onPressed: () => context.read<CounterBloc>().add(Increment()),
        child: Text('+'),
      ),
    ],
  ),
)

BlocBuilder — a widget that rebuilds the UI on each new State. BlocListener — for side effects (process a State once, without rebuilding the UI): show a SnackBar, navigate to another screen. BlocConsumer — a combination of Builder and Listener for cases where both a rebuild and a side effect are needed. BlocSelector — for selective rebuilding only when a specific State field changes.

MultiBlocProvider — a widget for nested BlocProviders without increasing nesting levels. A Flutter application with 10-15 Blocs uses MultiBlocProvider at the root level to register all Blocs available to the entire application: AuthenticationBloc, CartBloc, SettingsBloc.

Testing BLoC

BLoC is tested in isolation without Flutter widgets. Simply import the Dart package flutter_test and the bloc_test package. The test scenario: create a Bloc, add an Event, check the State. blocTest — a utility that automates the sequence: build → act → expect.

Dart
blocTest<CounterBloc, int>(
  'emits [1] when Increment is added',
  build: () => CounterBloc(),
  act: (bloc) => bloc.add(Increment()),
  expect: () => [1],
)

Mocking: A Bloc that depends on a repository or API is tested with mocks via mocktail. The repository is mocked at the abstraction level, and the Bloc receives mocked dependencies through the constructor. Hydrated Bloc — an extension for automatic state persistence/restoration in local storage. It is tested with HydratedBlocStorage and a temporary file storage.

BLoC in production

Folders and files: a typical Flutter project structure with BLoC: bloc/counter_bloc.dart, bloc/counter_event.dart, bloc/counter_state.dart. For 30+ screens, feature-based grouping is recommended: features/auth/bloc/, features/cart/bloc/. Each Bloc is a separate file, each Event and State — either in separate files or in one file with the Bloc.

Performance: BLoC does not create overhead for empty Streams. BlocBuilder uses buildWhen to filter rebuilds — the widget only updates when a specific condition changes. Close ensures that inactive Blocs do not consume memory. According to Flutter DevTools, BLoC adds less than 1% to the bundle size.

Migration from Provider: BLoC easily coexists with Provider in the same project. Gradual migration: first replace the most complex Providers with Bloc, then the rest. BlocProvider is compatible with the Provider tree: old widgets can use Provider, new ones — BlocProvider, within a single application.

Frequently asked questions

How is BLoC different from Provider in Flutter?

BLoC uses Event + Stream for business logic isolation and strong typing. Provider is a wrapper around InheritedWidget for simple dependency injection and ChangeNotifier. BLoC is better suited for complex scenarios with multiple states, Provider — for local UI state. BLoC requires more boilerplate but provides full traceability through Events.

What is Hydrated Bloc?

Hydrated Bloc is an extension from the hydrated_bloc package that automatically saves the last State to local storage (Hive by default). When the application restarts, the Bloc restores the saved state instead of the initial one. This solves the persistence problem without manual save calls: login, cart, settings are saved automatically between sessions.

How to handle errors in BLoC?

An error in BLoC is handled through try-catch inside mapEventToState or on<Event>. On error, Bloc returns an error State: yield LoadError(error.message). On the UI, BlocListener or BlocConsumer checks the State for the error type and shows a SnackBar or dialog. BlocObserver globally logs all unhandled exceptions.

Can BLoC be used with other frameworks?

BLoC is a Flutter-specific pattern as it uses Dart Stream and Flutter widgets. The Event → Bloc → State concept can be adapted for AngularDart and Server-side Dart, but the core ecosystem (BlocProvider, BlocBuilder, BlocObserver) is tied to Flutter. For React Native, use Redux or MobX; for SwiftUI, use Combine + MVVM.

What to choose: Bloc or Cubit?

Cubit — for simple states (counter, toggle, form with 2-3 fields). Bloc — for complex logic (news feed, order processing, authorization). The main rule: if you need tracing of every action (Event) for analytics or debugging — choose Bloc. If methods that change state are sufficient — choose Cubit. Both patterns can coexist in the same project.

Summary

  • BLoC — Flutter state management pattern via Event → Stream → State
  • Event — action (press, load), State — reaction (data, error, loading)
  • Cubit — simplified version without Event, up to 50% less boilerplate
  • BlocProvider — injecting Bloc into the widget tree with automatic close
  • BlocObserver — global monitoring of all Bloc and Cubit in the application
  • Hydrated Bloc — automatic state persistence via Hive
  • blocTest — utility for unit testing Bloc in isolation from Flutter

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also