bloc_manager 1.0.2 copy "bloc_manager: ^1.0.2" to clipboard
bloc_manager: ^1.0.2 copied to clipboard

A Flutter BLoC management package that eliminates boilerplate state-management code. Provides a sealed BaseState hierarchy, BlocManager widget with auto loading/error/success handling, and CacheableBl [...]

bloc_manager #

pub.dev License: MIT

A Flutter BLoC management package by Abubakar Issa that eliminates boilerplate state-management code by providing a ready-made sealed-state hierarchy, a declarative BlocManager widget, and reusable mixins for caching, pagination, and pull-to-refresh.

  • BaseState<T> – sealed state hierarchy (InitialState, LoadingState, SuccessState, ErrorState, LoadedState, EmptyState and async variants).
  • BaseCubit<S> / BaseBloc<E,S> – base classes with emitLoading(), emitSuccess(), emitError(), and executeAsync() helpers.
  • BlocManager<B,S> – a BlocConsumer wrapper that automatically shows loading overlays, error snackbars, and success snackbars.
  • MixinsCacheableBlocMixin, PaginationBlocMixin, RefreshableBlocMixin.

Installation #

dependencies:
  bloc_manager: ^1.0.0
flutter pub get

Or via a local path during development:

dependencies:
  bloc_manager:
    path: ../bloc_manager

How to Use #

1 · States — no subclassing needed #

You do not need to declare any custom state classes. Just pick from the sealed hierarchy:

import 'package:bloc_manager/bloc_manager.dart';

// Loading
emit(const LoadingState<User>());

// Data ready
emit(LoadedState<User>(data: user, lastUpdated: DateTime.now()));

// Write operation done
emit(SuccessState<User>(successMessage: 'Profile saved!'));

// Failed
emit(ErrorState<User>(errorMessage: 'Network error', errorCode: 'NET_01'));

// Empty result
emit(const EmptyState<User>(message: 'No results found'));

2 · BaseCubit — async made simple #

class UserCubit extends BaseCubit<BaseState<User>> {
  UserCubit(this._repo) : super(const InitialState());
  final UserRepository _repo;

  // executeAsync: emits LoadingState → runs action → emits result
  Future<void> loadUser(String id) => executeAsync(
    () => _repo.fetchUser(id),
    onSuccess: (user) =>
        LoadedState(data: user, lastUpdated: DateTime.now()),
    loadingMessage: 'Loading profile…',
  );

  Future<void> updateUser(String name) => executeAsync(
    () => _repo.update(name),
    successMessage: 'Profile updated!', // auto emits SuccessState
  );

  // Fine-grained helpers are also available directly:
  void somethingFailed(String msg) =>
      emitError(msg, errorCode: 'E01');
}

executeAsync signature:

Future<void> executeAsync<T>(
  Future<T> Function() action, {
  State Function(T result)? onSuccess,  // return your custom state
  void Function(Exception e)? onError,  // override error handling
  String? loadingMessage,
  String? successMessage,
})

3 · BlocManager — declarative UI wiring #

Replaces the manual BlocConsumer + loading-check + snackbar boilerplate:

BlocManager<UserCubit, BaseState<User>>(
  bloc: context.read<UserCubit>(),
  onSuccess: (ctx, state) => Navigator.of(ctx).pop(),
  onError: (ctx, state) => MyAnalytics.log(state.errorMessage),
  child: UserFormWidget(),
)

This auto-wires:

  • Full-screen spinner during LoadingState
  • Red snackbar on ErrorState (disable with showResultErrorNotifications: false)
  • Green snackbar on SuccessState (opt-in with showResultSuccessNotifications: true)
  • onSuccess called for both SuccessState and LoadedState

All parameters

Parameter Type Default Description
bloc B required The BLoC/Cubit to observe
child Widget required Screen content
builder (ctx, state) → Widget? null Custom builder; replaces child when set
listener (ctx, state) → void? null Fires on every meaningful state change
onSuccess (ctx, state) → void? null Called on SuccessState or LoadedState
onError (ctx, state) → void? null Called on ErrorState
showLoadingIndicator bool true Full-screen overlay during LoadingState
showResultErrorNotifications bool true Auto red snackbar on error
showResultSuccessNotifications bool false Auto green snackbar on success
enablePullToRefresh bool false Wraps content in RefreshIndicator
onRefresh Future<void> Function()? null Pull-to-refresh callback
loadingWidget Widget? null Custom spinner (default: SpinKitCircle)
loadingColor Color? null Overlay tint colour
errorSnackbarColor Color #B00020 Error snackbar background
successSnackbarColor Color #388E3C Success snackbar background

Preventing duplicate snackbars from global BLoCs

When one BLoC is mounted at multiple points in the tree, silence all but one:

// Root / silent
BlocManager<AuthCubit, BaseState<AuthData>>(
  bloc: sl<AuthCubit>(),
  showResultErrorNotifications: false,
  showLoadingIndicator: false,
  child: child,
)

// Login screen / active
BlocManager<AuthCubit, BaseState<AuthData>>(
  bloc: sl<AuthCubit>(),
  onSuccess: (ctx, _) => Navigator.pushReplacementNamed(ctx, '/home'),
  child: LoginForm(),
)

4 · PaginationBlocMixin — infinite scroll #

class ProductsCubit extends BaseCubit<BaseState<List<Product>>>
    with PaginationBlocMixin<Product, BaseState<List<Product>>> {

  Future<void> load() async {
    initializePagination(pageSize: 20);
    await loadFirstPage();
  }

  Future<void> loadMore() => loadNextPage(); // no-op at last page

  @override
  Future<PaginatedResult<Product>> onLoadPage({
    required int page, required int pageSize,
  }) => _repo.fetchProducts(page: page, pageSize: pageSize);

  @override
  Future<void> onPageLoaded(PaginatedResult<Product> result, int page) async {
    final prev = state.data ?? [];
    emit(LoadedState(
      data: page == 1 ? result.items : [...prev, ...result.items],
      lastUpdated: DateTime.now(),
    ));
    updatePaginationInfo(
      totalItems: result.totalItems,
      hasNextPage: result.hasNextPage,
      loadedPage: page,
    );
  }
}

Wire scroll detection:

NotificationListener<ScrollNotification>(
  onNotification: (n) {
    if (cubit.shouldLoadMore(n.metrics.pixels, n.metrics.maxScrollExtent)) {
      cubit.loadMore();
    }
    return false;
  },
  child: ListView.builder(…),
)

5 · CacheableBlocMixin — in-memory TTL cache #

class ProfileCubit extends BaseCubit<BaseState<Profile>>
    with CacheableBlocMixin<BaseState<Profile>> {

  @override String get cacheKey => 'user_profile';
  @override Duration get cacheTimeout => const Duration(minutes: 10);

  @override
  Map<String, dynamic>? stateToJson(BaseState<Profile> state) =>
      state is LoadedState ? (state.data as Profile).toJson() : null;

  @override
  BaseState<Profile>? stateFromJson(Map<String, dynamic> json) =>
      LoadedState(data: Profile.fromJson(json), lastUpdated: DateTime.now());

  Future<void> load() async {
    final cached = await loadStateFromCache();
    if (cached != null) { emit(cached); return; }
    await executeAsync(_repo.fetchProfile,
      onSuccess: (p) {
        final s = LoadedState(data: p, lastUpdated: DateTime.now());
        saveStateToCache(s);
        return s;
      },
    );
  }
}

6 · RefreshableBlocMixin — pull-to-refresh + auto-refresh #

class FeedCubit extends BaseCubit<BaseState<List<Article>>>
    with RefreshableBlocMixin<BaseState<List<Article>>> {

  @override
  Future<void> onRefresh() async {
    final articles = await _repo.fetchLatest();
    emit(LoadedState(data: articles, lastUpdated: DateTime.now()));
  }

  // Optional: refresh every 5 minutes while widget is alive.
  @override bool get autoRefreshEnabled => true;
  @override Duration get autoRefreshInterval => const Duration(minutes: 5);

  @override
  Future<void> close() {
    disposeRefreshable(); // cancel timer
    return super.close();
  }
}

Wire to BlocManager:

BlocManager<FeedCubit, BaseState<List<Article>>>(
  bloc: cubit,
  enablePullToRefresh: true,
  onRefresh: cubit.performRefresh,
  child: ArticleList(),
)

BaseState<T> Reference #

BaseState<T>                ─ isInitial / isLoading / isLoaded /
│                             isSuccess / isError / hasData
├── InitialState<T>
├── LoadingState<T>           message?, progress?
├── LoadedState<T>            data, lastUpdated?, isFromCache
├── SuccessState<T>           successMessage, metadata?
├── ErrorState<T>             errorMessage, errorCode?, exception?
├── EmptyState<T>             message?
│
│   ── Async (stream / real-time) variants ──
├── AsyncLoadingState<T>      data? (stale), message?, isRefreshing
├── AsyncLoadedState<T>       data, lastUpdated, isFromCache
└── AsyncErrorState<T>        data? (stale), errorMessage, isRetryable

All states extend Equatable and have descriptive toString() for logging.


Example App #

A runnable counter example lives in example/:

cd example && flutter pub get && flutter run

Author #

Created and maintained by Abubakar Issa.

🐙 GitHub github.com/Teewhydot
💼 LinkedIn linkedin.com/in/issa-abubakar-a0a200189
🌐 Portfolio sirteefyapps.com.ng

Contributions, issues, and feature requests are welcome — open a ticket on GitHub Issues.


License #

MIT © 2026 Abubakar Issa

1
likes
0
points
335
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter BLoC management package that eliminates boilerplate state-management code. Provides a sealed BaseState hierarchy, BlocManager widget with auto loading/error/success handling, and CacheableBlocMixin, PaginationBlocMixin, RefreshableBlocMixin utilities for consistent state management across projects.

Homepage
Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

ansicolor, bloc, equatable, flutter, flutter_bloc, flutter_spinkit, loading_overlay, meta

More

Packages that depend on bloc_manager