Advanced State Management (Bloc & Riverpod)

flutter_bloc Widgets: BlocProvider and BlocBuilder

16 min Lesson 5 of 14

flutter_bloc Widgets: BlocProvider and BlocBuilder

The flutter_bloc package ships a set of widgets that wire your BLoC classes into the Flutter widget tree. The two most fundamental widgets are BlocProvider, which injects a BLoC so any descendant can access it, and BlocBuilder, which rebuilds only the subtree it wraps whenever the BLoC emits a new state. Mastering these two widgets is the first concrete step toward writing clean, testable, and reactive BLoC-driven UIs.

BlocProvider: Dependency Injection via the Widget Tree

BlocProvider is an InheritedWidget-based widget that creates a BLoC (or Cubit) and makes it available to all widgets below it in the tree. It owns the instance and automatically calls close() on it when the provider is removed from the tree, preventing memory leaks.

  • create — a factory that receives a BuildContext and returns a new BLoC instance.
  • child — the subtree that will have access to the BLoC.
  • lazy — defaults to true; the BLoC is only created when first accessed.

Providing a CounterCubit to a Screen

// main.dart — wrap the route or screen in BlocProvider
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'counter_cubit.dart';
import 'counter_page.dart';

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

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => CounterCubit(),   // created here, disposed here
      child: const CounterPage(),
    );
  }
}

// counter_cubit.dart
class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);
}
Note: Always create a BLoC inside BlocProvider.create, never pass an already-constructed instance you own elsewhere — that breaks the automatic disposal contract. If you need to share an existing instance (e.g., pass it between routes), use BlocProvider.value and manage its lifecycle yourself.

BlocBuilder: Reactive, Targeted UI Rebuilds

BlocBuilder<B, S> listens to a BLoC of type B that emits states of type S. Each time a new state is emitted, its builder callback is called with the current BuildContext and the latest state, and only the widget subtree it returns is rebuilt — nothing above it is touched.

  • builder — required; receives (context, state) and returns a Widget.
  • buildWhen — optional predicate (previous, current) => bool; return false to skip a rebuild when only irrelevant parts of the state changed.
  • bloc — optional; if omitted, BlocBuilder looks up the nearest BlocProvider of the matching type via context.read.

BlocBuilder Rebuilding Only the Counter Display

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('BloC Counter')),
      body: Center(
        // Only this subtree rebuilds when CounterCubit emits
        child: BlocBuilder<CounterCubit, int>(
          builder: (context, count) {
            return Text(
              '$count',
              style: Theme.of(context).textTheme.displayLarge,
            );
          },
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            // context.read does NOT subscribe — safe to call in callbacks
            onPressed: () => context.read<CounterCubit>().increment(),
            child: const Icon(Icons.add),
          ),
          const SizedBox(height: 8),
          FloatingActionButton(
            onPressed: () => context.read<CounterCubit>().decrement(),
            child: const Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}

context.read vs context.watch

The flutter_bloc package exposes two convenience extensions on BuildContext that complement the widgets:

  • context.read<T>() — looks up a BLoC or Cubit of type T from the widget tree without subscribing. Use this inside event handlers, button callbacks, and initState — anywhere you call a method on the BLoC but do not need a rebuild.
  • context.watch<T>() — looks up the BLoC and subscribes to its stream, causing the current widget to rebuild on every new state. Equivalent to wrapping the widget in a BlocBuilder. Prefer BlocBuilder for fine-grained rebuilds; use context.watch when rebuilding the whole widget is acceptable.
Warning: Never call context.watch inside a callback, initState, or any method that is not build. It registers a subscription on the BuildContext and will throw if called outside the build phase. Use context.read for those cases.

buildWhen: Fine-Grained Rebuild Control

When your state object grows larger, you can prevent unnecessary rebuilds by supplying a buildWhen predicate. The builder is only called when the predicate returns true.

Skipping Rebuilds with buildWhen

// Suppose the state is a more complex class
class ShopState {
  final int cartCount;
  final bool isLoading;
  final List<String> items;

  const ShopState({
    required this.cartCount,
    required this.isLoading,
    required this.items,
  });
}

// The cart badge only cares about cartCount changes
BlocBuilder<ShopBloc, ShopState>(
  buildWhen: (previous, current) =>
      previous.cartCount != current.cartCount,
  builder: (context, state) {
    return Badge(
      label: Text('${state.cartCount}'),
      child: const Icon(Icons.shopping_cart),
    );
  },
)
Tip: Place BlocBuilder as deep in the widget tree as possible, wrapping only the widgets that actually depend on the state. A BlocBuilder high up in the tree will cause large subtrees to rebuild on every state emission — the same performance mistake as misusing setState on an ancestor widget.

Summary

Use BlocProvider to inject a BLoC into the subtree that needs it, taking advantage of its automatic lifecycle management. Use BlocBuilder to surgically rebuild only the widget that depends on BLoC state, and supply buildWhen to skip rebuilds when the relevant slice of state has not changed. Reach for context.read in event callbacks and context.watch (or BlocBuilder) in the build method. Together these two widgets provide a clear, testable, and performant bridge between your business logic and your UI.