Dart Tutorial

Flutter Lesson 42 of 83 4 min read

Bloc and Cubit in Flutter

Learn the Bloc pattern in Flutter with flutter_bloc: Cubit, Bloc with events, BlocProvider, BlocBuilder, BlocListener and state classes.

On this page

Bloc (Business Logic Component) is a state management pattern built on a strict idea: the UI sends events in, and gets states out. Nothing else passes between them. That strictness makes large apps predictable and easy to test, which is why Bloc is popular with bigger teams.

flutter pub add flutter_bloc

The package offers two tools. Start with the simpler one.

Cubit #

A Cubit holds a state and has methods that emit new states.

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

class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0); // the initial state

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

void main() {
  runApp(
    MaterialApp(
      home: BlocProvider(
        create: (context) => CounterCubit(),
        child: const CounterPage(),
      ),
    ),
  );
}

class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Cubit counter')),
      body: Center(
        child: BlocBuilder<CounterCubit, int>(
          builder: (context, count) {
            return Text('$count', style: const TextStyle(fontSize: 48));
          },
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            heroTag: 'add',
            onPressed: () => context.read<CounterCubit>().increment(),
            child: const Icon(Icons.add),
          ),
          const SizedBox(height: 12),
          FloatingActionButton(
            heroTag: 'remove',
            onPressed: () => context.read<CounterCubit>().decrement(),
            child: const Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}
PieceRole
Cubit<int>Holds an int state
emit(newState)Publishes a new state
BlocProviderCreates the cubit, makes it available below, and closes it when removed
BlocBuilderRebuilds its builder on every new state
context.read<CounterCubit>()Gets the cubit to call a method

State classes #

Real state is rarely one number. Model each situation the screen can be in. A sealed class makes the compiler check that the UI handles every case.

sealed class ProductsState {}

class ProductsLoading extends ProductsState {}

class ProductsLoaded extends ProductsState {
  ProductsLoaded(this.products);
  final List<String> products;
}

class ProductsFailed extends ProductsState {
  ProductsFailed(this.message);
  final String message;
}

class ProductsCubit extends Cubit<ProductsState> {
  ProductsCubit(this._repository) : super(ProductsLoading());

  final ProductRepository _repository;

  Future<void> load() async {
    emit(ProductsLoading());
    try {
      emit(ProductsLoaded(await _repository.fetchAll()));
    } catch (e) {
      emit(ProductsFailed('Could not load products'));
    }
  }
}
BlocBuilder<ProductsCubit, ProductsState>(
  builder: (context, state) {
    return switch (state) {
      ProductsLoading() => const Center(child: CircularProgressIndicator()),
      ProductsFailed(:final message) => Center(child: Text(message)),
      ProductsLoaded(:final products) => ListView(
          children: [for (final p in products) ListTile(title: Text(p))],
        ),
    };
  },
)

A new state is emitted only if it is not equal to the current one. For classes with fields, either emit new instances as above, or give them value equality with the equatable package.

Bloc: events in, states out #

A Bloc adds one more layer. The UI does not call methods. It adds events, and the bloc registers a handler for each event type.

// Events
sealed class CounterEvent {}

class Incremented extends CounterEvent {}

class Decremented extends CounterEvent {}

// Bloc
class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<Incremented>((event, emit) => emit(state + 1));
    on<Decremented>((event, emit) => emit(state - 1));
  }
}

// In the UI
context.read<CounterBloc>().add(Incremented());

Why bother with the extra code?

  • Every change has a name, so logs read like a story: Incremented, LoginSubmitted, CartCleared.
  • Events can be transformed: debounce a search field, drop taps while a request is running.
  • The same event can come from the UI, a timer or a push notification.
CubitBloc
Triggered byMethod callsEvents
CodeLessMore
TraceabilityStates onlyEvents and states
Good defaultYesWhen you need event handling features

Start with Cubit. Move a class to Bloc when you feel the need.

Reacting without rebuilding: BlocListener #

Builders are for drawing. For one-off actions such as a snackbar, a dialog or navigation, use a listener. It runs once per state change.

BlocListener<LoginCubit, LoginState>(
  listener: (context, state) {
    if (state is LoginFailed) {
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(state.message)));
    }
    if (state is LoginSucceeded) {
      Navigator.of(context).pushReplacementNamed('/home');
    }
  },
  child: const LoginForm(),
)

BlocConsumer combines a listener and a builder.

Rebuilding less #

// Rebuild only when a condition holds.
BlocBuilder<CartCubit, CartState>(
  buildWhen: (previous, current) => previous.count != current.count,
  builder: (context, state) => Text('${state.count}'),
)

// Or select a single value.
final count = context.select((CartCubit cubit) => cubit.state.count);

Several blocs #

MultiBlocProvider(
  providers: [
    BlocProvider(create: (_) => AuthCubit()),
    BlocProvider(create: (context) => ProductsCubit(context.read<ProductRepository>())..load()),
  ],
  child: const MyApp(),
)

RepositoryProvider supplies plain objects such as repositories to the blocs below it.

Testing #

Blocs contain no UI, so they are tested as plain Dart. The bloc_test package makes it short.

blocTest<CounterCubit, int>(
  'emits [1] when increment is called',
  build: () => CounterCubit(),
  act: (cubit) => cubit.increment(),
  expect: () => [1],
);

Try it yourself #

Build a login screen with a LoginCubit whose states are LoginInitial, LoginLoading, LoginSucceeded and LoginFailed. Disable the button while loading, show a snackbar on failure with a BlocListener, and navigate on success.

Practise in the playground Updated by Santosh Adhikari