bloc_small 5.0.0
bloc_small: ^5.0.0 copied to clipboard
An easy-to-use Flutter package offering a streamlined BLoC pattern implementation for intuitive and efficient state management in Flutter apps.
bloc_small #
A lightweight, streamlined implementation of the BLoC pattern for Flutter state management. Built on flutter_bloc, bloc_small simplifies dependency injection, error handling, and async operations while maintaining full BLoC benefits.
Features #
- Simplified BLoC & Cubit — Easy-to-use state management via
MainBlocandMainCubit - Dependency Injection — Integrated GetIt setup with automatic
CommonBlocregistration - Error & Loading State Management — Built-in
blocCatch/cubitCatchand loading overlay helpers - Freezed Integration — Full support for immutable states and events
- ReactiveSubject API — RxDart-powered stream transformations (
map,switchMap,debounceTime, etc.) - auto_route Integration — Optional type-safe navigation with deep linking
- Stateless & Stateful Widgets —
BaseBlocPage/BaseCubitPagefor both patterns
Installation & Requirements #
Requirements:
- Flutter >=3.38.0
- Dart >=3.9.2
Add to pubspec.yaml:
dependencies:
bloc_small:
dev_dependencies:
build_runner:
auto_route_generator:
freezed:
injectable_generator:
Then run:
flutter pub run build_runner build --delete-conflicting-outputs
Run this command whenever you modify Freezed or Injectable annotations.
Core Concepts #
| Class | Purpose |
|---|---|
MainBloc |
Foundation for event-driven state management |
MainCubit |
Simplified state management without events |
MainBlocEvent |
Base class for all BLoC events |
MainBlocState |
Base class for all states |
CommonBloc |
App-wide loading and common state |
BLoC Example:
@lazySingleton
class CountBloc extends MainBloc<CountEvent, CountState> {
CountBloc() : super(const CountState.initial()) {
on<Increment>(_onIncrement);
}
Future<void> _onIncrement(Increment event, Emitter<CountState> emit) async {
await blocCatch(actions: () async {
await Future.delayed(Duration(seconds: 1));
emit(state.copyWith(count: state.count + 1));
});
}
}
Cubit Example:
@lazySingleton
class CountCubit extends MainCubit<CountState> {
CountCubit() : super(const CountState.initial());
Future<void> increment() async {
await cubitCatch(actions: () async {
emit(state.copyWith(count: state.count + 1));
});
}
}
Basic Usage #
1. Set Up Dependency Injection #
@InjectableInit()
void configureInjectionApp() {
getIt.registerCore(); // Registers CommonBloc — required
getIt.init(); // Registers your app dependencies
}
void main() {
WidgetsFlutterBinding.ensureInitialized();
configureInjectionApp();
runApp(MyApp());
}
Important:
getIt.registerCore()is required and registersCommonBlocas a lazy singleton. Omitting it will throw on first page load.
2. Define Events & States with Freezed #
abstract class CountEvent extends MainBlocEvent {
const CountEvent._();
}
@freezed
sealed class Increment extends CountEvent with _$Increment {
const Increment._() : super._();
const factory Increment() = _Increment;
}
@freezed
sealed class CountState extends MainBlocState with _$CountState {
const CountState._();
const factory CountState.initial({@Default(0) int count}) = _Initial;
}
3. Create a Page #
class CounterPage extends StatefulWidget {
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends BaseBlocPageState<CounterPage, CountBloc> {
@override
Widget buildPage(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: BlocBuilder<CountBloc, CountState>(
builder: (context, state) => Text('${state.count}'),
),
floatingActionButton: FloatingActionButton(
onPressed: () => bloc.add(Increment()),
child: const Icon(Icons.add),
),
);
}
}
Using Cubit #
Cubit simplifies state management by using direct method calls instead of events:
@lazySingleton
class CountCubit extends MainCubit<CountState> {
CountCubit() : super(const CountState.initial());
void increment() => emit(state.copyWith(count: state.count + 1));
}
Use BaseCubitPageState and BaseCubitPage for page widgets. Call methods directly:
floatingActionButton: FloatingActionButton(
onPressed: () => cubit.increment(),
child: const Icon(Icons.add),
)
| Aspect | BLoC | Cubit |
|---|---|---|
| Events | ✓ | ✗ |
| Complexity | Higher | Lower |
| Use Case | Complex logic | Simple updates |
Bloc Ownership Rules #
The DI container owns every bloc and cubit. Widgets consume, never close them.
-
Register page state managers as singleton/lazy singleton —
registerFactoryis not supported and will throw in debug builds. Factories create new instances per resolution, and pages never dispose what they didn't create.getIt.registerLazySingleton<CountBloc>(CountBloc.new); // ✓ // getIt.registerFactory<CountBloc>(CountBloc.new); // ✗ -
CommonBlocis app-wide — Registered viaregisterCore(). CallresetCore()in tests/hot-restart. -
Do not annotate your router for codegen —
registerAppRouterregisters the instance you provide. Annotating it with@LazySingletonwould register it twice, causinggetIt.init()to throw.@AutoRouterConfig() class AppRouter extends BaseAppRouter { } // No @LazySingleton
Using StatelessWidget #
BaseBlocPage and BaseCubitPage receive the state manager in buildPage:
class CounterPage extends BaseBlocPage<CountBloc> {
const CounterPage({super.key});
@override
Widget buildPage(BuildContext context, CountBloc bloc) {
return Scaffold(
body: BlocBuilder<CountBloc, CountState>(
builder: (context, state) => Text('${state.count}'),
),
floatingActionButton: FloatingActionButton(
onPressed: () => bloc.add(Increment()),
child: const Icon(Icons.add),
),
);
}
}
auto_route Integration #
- Create Router:
@AutoRouterConfig()
class AppRouter extends BaseAppRouter {
@override
List<AutoRoute> get routes => [
AutoRoute(page: HomeRoute.page, initial: true),
AutoRoute(page: SettingsRoute.page),
];
}
- Register & Configure:
void configureInjectionApp() {
getIt.registerAppRouter<AppRouter>(AppRouter(), enableNavigationLogs: true);
getIt.registerCore();
getIt.init();
}
class MyApp extends StatelessWidget {
final _router = getIt<AppRouter>();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _router.config(),
);
}
}
- Navigate:
final navigator = getIt.getNavigator();
navigator?.push(const HomeRoute());
Loading Overlay #
Wrap your page content with buildLoadingOverlay to display a loading indicator:
Widget buildPage(BuildContext context) {
return buildLoadingOverlay(
child: Scaffold(...),
loadingKey: 'myKey', // Optional: for multiple loading states
);
}
Control it from your bloc/cubit:
await blocCatch(actions: () async {
showLoading();
try {
// Your async operation
} finally {
hideLoading();
}
});
Error Handling #
Use blocCatch or cubitCatch to wrap async operations and handle errors automatically:
await blocCatch(
actions: () async {
// Your code here
},
onError: (error) {
print('Error: $error');
},
);
For standardized error handling, use BaseErrorHandlerMixin:
@lazySingleton
class CountBloc extends MainBloc<CountEvent, CountState> with BaseErrorHandlerMixin {
// ...
Future<void> _onIncrement(Increment event, Emitter<CountState> emit) async {
await blocCatch(
actions: () async { /* ... */ },
onError: handleError, // Uses mixin's error handler
);
}
}
The mixin provides automatic logging, error message generation, and loading state cleanup.
ReactiveSubject #
ReactiveSubject<T> wraps RxDart's BehaviorSubject/PublishSubject with a simplified API.
API Reference #
Constructors & Core:
| Method | Description |
|---|---|
ReactiveSubject({T? initialValue}) |
Creates with BehaviorSubject |
ReactiveSubject.broadcast() |
Creates with PublishSubject |
add(T value) |
Add new value |
dispose() |
Release resources |
Transformation:
| Method | Example |
|---|---|
map<R>() |
subject.map((i) => i * 2) |
where() |
subject.where((i) => i > 0) |
switchMap() |
subject.switchMap((i) => api.fetch(i)) |
debounceTime() |
subject.debounceTime(300.ms) |
distinct() |
subject.distinct() |
Example:
final subject = ReactiveSubject<int>(initialValue: 0);
final stream = subject
.map((i) => i * 2)
.debounceTime(Duration(milliseconds: 300))
.listen(print);
await subject.dispose();
Contributing #
Contributions welcome! Please open an issue or pull request on GitHub.
License #
MIT License. See LICENSE file for details.