bloc_small 5.0.0 copy "bloc_small: ^5.0.0" to clipboard
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 #

pub package MIT License

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 MainBloc and MainCubit
  • Dependency Injection — Integrated GetIt setup with automatic CommonBloc registration
  • Error & Loading State Management — Built-in blocCatch/cubitCatch and 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 WidgetsBaseBlocPage/BaseCubitPage for 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 registers CommonBloc as 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.

  1. Register page state managers as singleton/lazy singletonregisterFactory is 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);     // ✗
    
  2. CommonBloc is app-wide — Registered via registerCore(). Call resetCore() in tests/hot-restart.

  3. Do not annotate your router for codegenregisterAppRouter registers the instance you provide. Annotating it with @LazySingleton would register it twice, causing getIt.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 #

  1. Create Router:
@AutoRouterConfig()
class AppRouter extends BaseAppRouter {
  @override
  List<AutoRoute> get routes => [
    AutoRoute(page: HomeRoute.page, initial: true),
    AutoRoute(page: SettingsRoute.page),
  ];
}
  1. 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(),
    );
  }
}
  1. 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.

5
likes
160
points
242
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

An easy-to-use Flutter package offering a streamlined BLoC pattern implementation for intuitive and efficient state management in Flutter apps.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

auto_route, flutter, flutter_bloc, freezed_annotation, get_it, rxdart

More

Packages that depend on bloc_small