flutter_bloc Widgets: BlocProvider and BlocBuilder
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
BuildContextand 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);
}
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 aWidget. - buildWhen — optional predicate
(previous, current) => bool; returnfalseto skip a rebuild when only irrelevant parts of the state changed. - bloc — optional; if omitted,
BlocBuilderlooks up the nearestBlocProviderof the matching type viacontext.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
Tfrom the widget tree without subscribing. Use this inside event handlers, button callbacks, andinitState— 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. PreferBlocBuilderfor fine-grained rebuilds; usecontext.watchwhen rebuilding the whole widget is acceptable.
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),
);
},
)
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.