Lean DI for Bloc
Bloc and Cubit integration for Lean DI, with state observable through
context.watch and context.select.
Register your Blocs and Cubits as Lean DI dependencies, and build your UI
with BlocBuilder, BlocSelector, BlocListener and BlocConsumer - the
same widgets you know from flutter_bloc, resolving their bloc from the
nearest DepsProvider. For registering and scoping dependencies in general,
see flutter_lean_di.
Installation
flutter pub add flutter_lean_di_bloc bloc
Quick start
import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_lean_di_bloc/flutter_lean_di_bloc.dart';
void main() => runApp(const MaterialApp(home: CounterPage()));
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
}
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
// CounterCubit is created on first use and closed together with this
// DepsProvider.
return DepsProvider(
register: [BlocDependency<CounterCubit>((deps, _) => CounterCubit())],
builder: (context, _) => Scaffold(
body: Center(
// Rebuilds with every new state.
child: BlocBuilder<CounterCubit, int>(
builder: (context, count) => Text('$count'),
),
),
floatingActionButton: FloatingActionButton(
// Reads CounterCubit without listening to it.
onPressed: () => context.get<CounterCubit>().increment(),
child: const Icon(Icons.add),
),
),
);
}
}
Registering blocs
BlocDependency registers a Bloc or Cubit like any other Dependency,
and closes it with close() when it's removed - e.g. when its DepsProvider
is disposed:
DepsProvider(
register: [
// Lazily created on first use, closed with the DepsProvider.
BlocDependency<CounterCubit>((deps, _) => CounterCubit()),
// Depending on other dependencies, created as soon as it's registered,
// and created again when userId changes.
BlocDependency<ProfileBloc>(
(deps, _) => ProfileBloc(deps.get<UserRepository>(), userId),
lazy: false,
cacheKey: userId,
),
// With custom cleanup instead of the default close().
BlocDependency<SessionCubit>(
(deps, _) => SessionCubit(),
dispose: (cubit) async {
await cubit.logOut();
await cubit.close();
},
),
],
child: const ProfileScreen(),
);
A bloc registered this way also works with flutter_lean_di's own context
lookups - context.watch and context.select react to every state it emits:
final count = context.watch<CounterCubit>().state;
final isEven = context.select<CounterCubit, bool>((cubit) => cubit.state.isEven);
Widgets
| Widget | Use it to |
|---|---|
BlocBuilder<TBloc, TState> |
Build from the bloc's state, optionally filtered with buildWhen. |
BlocSelector<TBloc, TState, TSelected> |
Build from a part of the state, rebuilding only when it changes. |
BlocListener<TBloc, TState> |
Run side effects on new states, optionally filtered with listenWhen. |
BlocConsumer<TBloc, TState> |
Do both for the same state changes. |
Each of them resolves TBloc from the nearest DepsProvider, and follows a
new instance when one is registered under TBloc - e.g. after replace(), or
a changed cacheKey. Pass bloc to use a specific instance instead:
BlocBuilder<CounterCubit, int>(
bloc: myCounterCubit,
builder: (context, count) => Text('$count'),
);
BlocBuilder
Rebuilds with every new state, or only with the ones buildWhen accepts:
BlocBuilder<CounterCubit, int>(
buildWhen: (previous, current) => current.isEven,
builder: (context, count) => Text('$count'),
);
BlocSelector
Rebuilds only when the value returned by selector changes (compared with
==). Prefer it over BlocBuilder with a hand-written buildWhen when only
part of the state matters:
BlocSelector<ProfileBloc, ProfileState, String>(
selector: (state) => state.name,
builder: (context, name) => Text(name),
);
BlocListener
Calls listener for new states - optionally filtered with listenWhen -
without rebuilding anything. Use it for one-off side effects, like navigation
or showing a dialog:
BlocListener<CounterCubit, int>(
listenWhen: (previous, current) => current == 10,
listener: (context, count) => showDialog<void>(
context: context,
builder: (_) => const AlertDialog(title: Text('You reached 10!')),
),
child: const CounterView(),
);
BlocConsumer
Combines BlocBuilder and BlocListener, for when the same state changes
need both a rebuild and a side effect:
BlocConsumer<CounterCubit, int>(
listenWhen: (previous, current) => current == 10,
listener: (context, count) => ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Reached 10!')),
),
builder: (context, count) => Text('$count'),
);
Coming from flutter_bloc
flutter_bloc |
flutter_lean_di_bloc |
|---|---|
BlocProvider(create: ...) |
DepsProvider(register: [BlocDependency(...)]) |
MultiBlocProvider |
One DepsProvider with several dependencies in register |
BlocProvider.value |
Dependency.value(bloc, createObserver: BlocDependencyObserver.new) |
context.read<T>() |
context.get<T>() |
context.watch<T>(), context.select |
Same |
BlocBuilder, BlocSelector, BlocListener, BlocConsumer |
Same names and parameters |
Like BlocProvider.value, a Dependency.value never closes the bloc - that's
up to whoever created it. createObserver makes context.watch and
context.select react to its states, as they do for a BlocDependency.
The widgets have the same names as flutter_bloc's, so don't import both
packages in the same file.
See also
flutter_lean_di-DepsProvider, reading dependencies fromcontext, app-wide, per-screen and shared dependencies, testing and debugging.lean_di- the underlying container, usable in plain Dart.bloc-BlocandCubitthemselves.
🛠️ Maintained by LeanCode
This package is built with 💙 by LeanCode. We are top-tier experts focused on Flutter Enterprise solutions.
Why LeanCode?
-
Creators of Patrol – the next-gen testing framework for Flutter.
-
Production-Ready – We use this package in apps with millions of users.
-
Full-Cycle Product Development – We take your product from scratch to long-term maintenance.

