flutter
/

Bloc Core Concepts: Events, States, and Widgets

Last Sync: Today

On this page

15
0%
Beginner
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutterBeginner

Bloc Core Concepts: Events, States, and Widgets

Before diving into complex state management with Bloc, it's essential to understand the core building blocks. This guide explains the key concepts: events, states, Cubit, Bloc, and the widgets that connect them to your UI.

The Three Pillars of Bloc

  • Events – Inputs to the Bloc (user actions, lifecycle events).
  • States – Outputs from the Bloc representing the current UI state.
  • Bloc/Cubit – The component that transforms events into states.

Events

Events are the way the UI communicates with the Bloc. They represent something that happened: a button press, a page load, a form submission. Events are typically defined as classes (often extending Equatable for easy comparison).

DARTRead-only
1
abstract class AuthEvent {}

class LoginSubmitted extends AuthEvent {
  final String email;
  final String password;
  LoginSubmitted(this.email, this.password);
}

class LogoutRequested extends AuthEvent {}

States

States represent the current condition of the UI. The Bloc emits states in response to events. UI widgets listen to states and rebuild accordingly.

DARTRead-only
1
abstract class AuthState {}

class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthSuccess extends AuthState {
  final String userId;
  AuthSuccess(this.userId);
}
class AuthFailure extends AuthState {
  final String message;
  AuthFailure(this.message);
}

Cubit (Simplified Bloc)

A Cubit is a class that extends Cubit<State> and uses methods to emit states. It’s the simplest way to use the BLoC pattern.

DARTRead-only
1
import 'package:flutter_bloc/flutter_bloc.dart';

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

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

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

Bloc (Event-Driven)

A Bloc extends Bloc<Event, State> and uses events and event handlers. It gives you more control, including event transformers (debounce, throttle) and advanced stream operations.

DARTRead-only
1
import 'package:flutter_bloc/flutter_bloc.dart';

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

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

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)));
  }
}

Connecting UI with BlocProvider

BlocProvider is a widget that provides a Bloc or Cubit to its descendants. It also automatically disposes the bloc when the widget is removed from the tree.

DARTRead-only
1
BlocProvider(
  create: (context) => CounterCubit(),
  child: MyHomePage(),
);

Rebuilding UI with BlocBuilder

BlocBuilder listens to a bloc and rebuilds when a new state is emitted. It requires you to specify the bloc type and the builder function that returns a widget based on the current state.

DARTRead-only
1
BlocBuilder<CounterCubit, CounterState>(
  builder: (context, state) {
    return Text('Count: ${state.value}');
  },
);

Side Effects with BlocListener

BlocListener is used for side effects like showing a snackbar, navigating to another screen, or logging. It does not rebuild the UI.

DARTRead-only
1
BlocListener<AuthBloc, AuthState>(
  listener: (context, state) {
    if (state is AuthSuccess) {
      Navigator.pushReplacementNamed(context, '/home');
    }
    if (state is AuthFailure) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(state.message)),
      );
    }
  },
  child: ...
);

Combining Builder and Listener with BlocConsumer

BlocConsumer combines BlocBuilder and BlocListener in one widget, useful when you need both rebuild and side effect handling.

DARTRead-only
1
BlocConsumer<AuthBloc, AuthState>(
  listener: (context, state) {
    // handle side effects
  },
  builder: (context, state) {
    // return UI based on state
  },
);

Accessing the Bloc: context.read and context.watch

Inside a widget that has a BlocProvider ancestor, you can access the bloc using context.read<T>() (to call methods) and context.watch<T>() (to listen to state changes).

DARTRead-only
1
// Dispatch an event without rebuilding
context.read<AuthBloc>().add(LoginSubmitted(email, password));

// Access state and rebuild when it changes
final state = context.watch<AuthBloc>().state;

Complete Example: Login Screen

Putting it all together: a simple login screen using Bloc.

DARTRead-only
1
class LoginPage extends StatelessWidget {
  final emailController = TextEditingController();
  final passwordController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Login')),
      body: BlocConsumer<AuthBloc, AuthState>(
        listener: (context, state) {
          if (state is AuthSuccess) {
            Navigator.pushReplacementNamed(context, '/home');
          }
          if (state is AuthFailure) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text(state.message)),
            );
          }
        },
        builder: (context, state) {
          if (state is AuthLoading) {
            return Center(child: CircularProgressIndicator());
          }
          return Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              children: [
                TextField(
                  controller: emailController,
                  decoration: InputDecoration(labelText: 'Email'),
                ),
                TextField(
                  controller: passwordController,
                  decoration: InputDecoration(labelText: 'Password'),
                  obscureText: true,
                ),
                SizedBox(height: 20),
                ElevatedButton(
                  onPressed: () {
                    context.read<AuthBloc>().add(
                      LoginSubmitted(
                        emailController.text,
                        passwordController.text,
                      ),
                    );
                  },
                  child: Text('Login'),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}

Best Practices

  • Use Equatable – Simplify state/event equality checks to avoid unnecessary rebuilds.
  • Place BlocProvider at the appropriate level – Higher level for shared blocs, lower level for scoped ones.
  • Prefer context.read for event dispatching – Avoids unnecessary rebuilds.
  • Use BlocListener for side effects – Keep side effects out of BlocBuilder.
  • Keep states immutable – Always create new state objects; never mutate existing ones.
  • Separate business logic – Keep UI widgets free of business logic; put it in the bloc/cubit.

Common Mistakes

  • ❌ Creating BlocProvider inside build – Recreates the bloc on every rebuild, losing state. ✅ Use BlocProvider at a higher level or outside build.
  • ❌ Using BlocBuilder for side effects – Can cause infinite loops. ✅ Use BlocListener.
  • ❌ Calling emit after bloc is closed – Throws an error. ✅ Check isClosed before emitting if needed.
  • ❌ Not providing a bloc – context.read will throw an error. ✅ Ensure a BlocProvider exists higher in the tree.

Next Steps

Now that you understand the core concepts, explore:

  • Cubit vs Bloc – When to use each.
  • Bloc Architecture – Structuring your app.
  • Testing Blocs – Write reliable tests.

Conclusion

Bloc’s core concepts—events, states, Cubit, Bloc, and the provider widgets—give you a solid foundation for building reactive Flutter apps. By understanding how they interact, you can create predictable, testable, and maintainable state management.

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 Core Concepts')),
      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

What widget should you use to rebuild the UI when the bloc state changes?

A
BlocProvider
B
BlocBuilder
C
BlocListener
D
BlocConsumer
Q2
of 3

Which method is used to dispatch an event to a Bloc?

A
context.bloc.add()
B
context.read<Bloc>().add()
C
Bloc.add()
D
emit()
Q3
of 3

What is the purpose of `BlocListener`?

A
To rebuild UI on state changes
B
To provide the bloc to the widget tree
C
To perform side effects (navigation, snackbars) without rebuilding
D
To combine builder and listener

Frequently Asked Questions

What is the difference between `context.read` and `context.watch`?

context.read<T>() returns the bloc instance without listening to state changes. Use it for dispatching events or calling methods. context.watch<T>() returns the bloc and also causes the widget to rebuild when the state changes. Prefer read for event dispatching to avoid unnecessary rebuilds.

When should I use `BlocConsumer` instead of separate `BlocBuilder` and `BlocListener`?

BlocConsumer is convenient when both rebuild and side effect logic are closely related. However, using separate widgets can improve readability when the builder and listener are large or reused.

Can I use Bloc without `BlocProvider`?

Yes, you can instantiate a bloc manually and pass it down manually, but BlocProvider simplifies dependency management and ensures proper disposal.

How do I handle errors in a bloc?

Catch exceptions inside event handlers or methods and emit an error state. For Bloc, you can also override onError to log or react to unhandled errors.

What is the purpose of `Equatable` in Bloc?

Equatable overrides == and hashCode for you, which helps Bloc avoid unnecessary rebuilds when states are identical but not the same instance. It's highly recommended.

Previous

bloc why bloc

Next

bloc events

Related Content

Need help?

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