flutter
/

Bloc Introduction: State Management with BLoC Pattern

Last Sync: Today

On this page

14
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

Bloc Introduction: State Management with BLoC Pattern

What is Bloc?

If you're building scalable Flutter apps and struggling with messy state management, Bloc is one of the most powerful solutions. It helps you write predictable, testable, and maintainable code by separating business logic from the UI. Bloc (Business Logic Component) is a state management library that uses streams to react to events and emit new states. The library flutter_bloc provides widgets to easily integrate Bloc/Cubit into your Flutter app.

Real-World Use Cases

  • Login Form – Handle loading, success, and error states with API validation.
  • Shopping Cart – Manage items, quantities, totals, and checkout flow.
  • Multi‑step Forms – Maintain state across steps (e.g., insurance quote forms).
  • API Data Fetching – Show loading indicators, handle errors, and refresh data.
  • Authentication – Track user login status and protect routes.

Bloc Data Flow

The flow is unidirectional: UI → Event → Bloc → State → UI. This ensures predictable state changes and makes testing straightforward.

Core Concepts

  • Events – Inputs to the Bloc (user actions, lifecycle events).
  • States – Outputs from the Bloc representing the current UI state.
  • Bloc/Cubit – The component that receives events, transforms them, and emits new states.
  • BlocProvider – A widget that provides a Bloc/Cubit to its descendants.
  • BlocBuilder – A widget that rebuilds when the state changes.
  • BlocListener – A widget that responds to state changes (e.g., show snackbar, navigation).

Cubit vs Bloc

FeatureCubitBloc
Event definitionMethodsEvents (classes)
BoilerplateMinimalMore
ComplexitySimpleAdvanced
Error handlingInside methodsVia `on<Event>`
Best forSimple UI stateComplex flows, debouncing, transforms

Cubit is a simplified version of Bloc that uses methods to emit states directly. Bloc uses event classes and an event handler, which is more verbose but powerful for advanced use cases.

Setting Up flutter_bloc

Add the dependency to your pubspec.yaml:

YAMLRead-only
1
dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^8.1.5
  equatable: ^2.0.5  # Optional but recommended

Run flutter pub get to install.

Creating a Counter Cubit

Let's create a simple counter cubit. We'll define a state class and a cubit that exposes methods to change the state.

DARTRead-only
1
class CounterState {
  final int value;
  CounterState(this.value);
}
DARTRead-only
1
import 'package:flutter_bloc/flutter_bloc.dart';

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

  void increment() => emit(CounterState(state.value + 1));
  void decrement() => emit(CounterState(state.value - 1));
}

Using Cubit in UI

Wrap your widget with BlocProvider to provide the cubit, then use BlocBuilder to rebuild on state changes.

DARTRead-only
1
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: (_) => CounterCubit(),
        child: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Bloc Counter')),
      body: Center(
        child: BlocBuilder<CounterCubit, CounterState>(
          builder: (context, state) {
            return Text(
              'Count: ${state.value}',
              style: TextStyle(fontSize: 24),
            );
          },
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () => context.read<CounterCubit>().increment(),
            child: Icon(Icons.add),
          ),
          SizedBox(height: 10),
          FloatingActionButton(
            onPressed: () => context.read<CounterCubit>().decrement(),
            child: Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}

Creating a Bloc (Event-Driven)

For more complex flows, use Bloc with events. Define events, states, and an event handler.

DARTRead-only
1
abstract class CounterEvent {}
class Increment extends CounterEvent {}
class Decrement extends CounterEvent {}
DARTRead-only
1
class CounterState {
  final int value;
  CounterState(this.value);
}
DARTRead-only
1
import 'package:flutter_bloc/flutter_bloc.dart';

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

Using Bloc with BlocConsumer

BlocConsumer combines BlocBuilder and BlocListener for both UI rebuilds and side effects.

DARTRead-only
1
BlocConsumer<CounterBloc, CounterState>(
  listener: (context, state) {
    if (state.value == 5) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Reached 5!')),
      );
    }
  },
  builder: (context, state) {
    return Text('Count: ${state.value}');
  },
);

Best Practices

  • Use Equatable – Override == and hashCode to avoid unnecessary rebuilds. Extend Equatable in your states and events.
  • Keep states immutable – Never mutate a state; always create new instances.
  • Use BlocProvider.value wisely – Only for existing instances, not for creation inside build.
  • Use context.read for events, context.watch for state – Prevents unnecessary rebuilds.
  • Separate business logic – Keep UI pure; move logic to bloc/cubit.
  • Handle errors gracefully – Use onError in bloc to catch exceptions.

Common Mistakes

  • ❌ Calling emit outside the bloc/cubit – Only allowed inside the bloc.
  • ❌ Not using Equatable – Causes unnecessary rebuilds because states are considered different even when values are equal.
  • ❌ Creating bloc inside build – Recreates on every rebuild, losing state. ✅ Use BlocProvider(create: ...).
  • ❌ Using BlocBuilder for side effects – Should use BlocListener.
  • ❌ Not disposing controllers/streams – Use close() or rely on BlocProvider cleanup.

Next Steps

  • 👉 Learn more about Cubit in Flutter Cubit Guide
  • 👉 Master Bloc Events and Transforms in Advanced Bloc
  • 👉 Explore Bloc Architecture for large apps
  • 👉 Test your blocs with bloc_test

Conclusion

Bloc is a robust state management solution that enforces separation of concerns and makes your app testable and scalable. Whether you choose the simplicity of Cubit or the power of Bloc with events, flutter_bloc provides a clear pattern for building reactive Flutter applications.

Try it yourself

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

void main() => runApp(MyApp());

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

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

  void increment() => emit(CounterState(state.value + 1));
  void decrement() => emit(CounterState(state.value - 1));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider(
        create: (_) => CounterCubit(),
        child: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Bloc Counter')),
      body: Center(
        child: BlocBuilder<CounterCubit, CounterState>(
          builder: (context, state) {
            return Text(
              'Count: ${state.value}',
              style: TextStyle(fontSize: 24),
            );
          },
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () => context.read<CounterCubit>().increment(),
            child: Icon(Icons.add),
          ),
          SizedBox(height: 10),
          FloatingActionButton(
            onPressed: () => context.read<CounterCubit>().decrement(),
            child: Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}

Test Your Knowledge

Q1
of 3

Which widget provides a bloc to its descendants?

A
BlocBuilder
B
BlocProvider
C
BlocListener
D
BlocConsumer
Q2
of 3

What is the main difference between Cubit and Bloc?

A
Cubit uses events, Bloc uses methods
B
Cubit uses methods, Bloc uses events
C
Cubit is for async, Bloc is for sync
D
Cubit cannot emit states
Q3
of 3

How do you prevent unnecessary rebuilds when states are equal?

A
Use BlocBuilder
B
Use Equatable in states
C
Use BlocListener
D
Use context.watch

Frequently Asked Questions

What is the difference between Cubit and Bloc in Flutter?

Cubit uses methods to emit states directly, while Bloc uses event classes and on<Event> handlers. Cubit is simpler, Bloc is more powerful for complex flows like debouncing and advanced error handling.

Do I need to use Equatable with Bloc?

Not strictly, but it's highly recommended to prevent unnecessary rebuilds by ensuring state equality is correctly compared. Equatable reduces boilerplate for overriding == and hashCode.

How do I pass arguments to a bloc?

You can pass them via the constructor and use BlocProvider(create: (context) => MyBloc(arg)).

Can I use Bloc with Firebase or REST APIs?

Yes, you can call async functions inside your bloc/cubit and emit loading/success/error states. Use on<Event> with emit.forEach or emit in the handler.

How do I dispose a bloc?

BlocProvider automatically disposes the bloc when the widget is removed. You can also override close() to clean up resources like streams or controllers.

Previous

getx flutter web

Next

bloc architecture

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.