bloc_manager
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,EmptyStateand async variants).BaseCubit<S>/BaseBloc<E,S>– base classes withemitLoading(),emitSuccess(),emitError(), andexecuteAsync()helpers.BlocManager<B,S>– aBlocConsumerwrapper that automatically shows loading overlays, error snackbars, and success snackbars.- Mixins –
CacheableBlocMixin,PaginationBlocMixin,RefreshableBlocMixin.
Installation
dependencies:
bloc_manager: ^1.1.0
flutter pub get
Or via a local path during development:
dependencies:
bloc_manager:
path: ../bloc_manager
How to Use
1 · BlocManagerTheme — app-wide branding
Instead of repeating loadingColor, onError, and onSuccess on every BlocManager instance,
set them once at the app root and have every instance inherit automatically.
// In MyApp.build() — wrap your MaterialApp / GetMaterialApp
BlocManagerTheme(
data: BlocManagerThemeData(
loadingColor: AppColors.primary.withValues(alpha: 0.5),
onError: (context, message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: AppColors.error,
),
);
},
onSuccess: (context, message) {
if (message != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: AppColors.success,
),
);
}
},
),
child: MaterialApp(…),
)
Now every BlocManager in the tree will use these handlers without any extra configuration.
BlocManagerThemeData fields
| Field | Type | Default | Description |
|---|---|---|---|
loadingWidget |
Widget? |
null |
Global spinner (default: SpinKitCircle) |
loadingColor |
Color? |
null |
Global overlay tint colour |
onError |
void Function(BuildContext, String)? |
null |
Global error handler — replaces the built-in red snackbar |
onSuccess |
void Function(BuildContext, String?)? |
null |
Global success handler — replaces the built-in green snackbar |
showResultErrorNotifications |
bool |
true |
Show error notifications when no onError is set |
showResultSuccessNotifications |
bool |
false |
Show success notifications when no onSuccess is set |
Resolution priority
For each setting, BlocManager resolves in this order:
- Instance param — explicit value passed directly to the
BlocManagerwidget BlocManagerTheme— value from the nearestBlocManagerThemeancestor- Built-in default — package default (e.g. red snackbar for errors)
Per-instance overrides still work
// Silence error notifications for this one silent / root observer
BlocManager<AuthCubit, BaseState<AuthData>>(
bloc: sl<AuthCubit>(),
showResultErrorNotifications: false,
showLoadingIndicator: false,
child: child,
)
// Active screen — theme handles the error UI automatically
BlocManager<AuthCubit, BaseState<AuthData>>(
bloc: sl<AuthCubit>(),
onSuccess: (ctx, _) => Navigator.pushReplacementNamed(ctx, '/home'),
child: LoginForm(),
)
2 · 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'));
3 · 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,
})
4 · 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 withshowResultErrorNotifications: false) - Green snackbar on
SuccessState(opt-in withshowResultSuccessNotifications: true) onSuccesscalled for bothSuccessStateandLoadedState
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? |
null |
Auto red snackbar on error; null inherits from BlocManagerTheme |
showResultSuccessNotifications |
bool? |
null |
Auto green snackbar on success; null inherits from BlocManagerTheme |
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 |
5 · 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(…),
)
6 · 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;
},
);
}
}
7 · 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 comprehensive example app demonstrating all bloc_manager features lives in example/.
The example app uses real public APIs to showcase production-ready implementations:
| Tab | Feature | API | Demonstrates |
|---|---|---|---|
| Posts | PaginationBlocMixin | JSONPlaceholder Posts | Infinite scroll pagination with page indicators |
| Pokemon | CacheableBlocMixin | PokeAPI | In-memory caching with 10-minute TTL and visual "From Cache" badge |
| Products | RefreshableBlocMixin | Fake Store API | Pull-to-refresh + auto-refresh every 30 seconds |
| Todos | All BaseState types | JSONPlaceholder Todos | Complete state flow: Initial → Loading → Loaded → Success/Error/Empty |
Running the Example
cd example && flutter pub get && flutter run
What You'll Learn
Each tab is self-contained and demonstrates real-world patterns:
-
Posts Tab - Infinite scroll pagination with:
loadFirstPage()/loadNextPage()methodsPaginationInfowith page numbers and total count- Loading indicators for additional pages
-
Pokemon Tab - Smart caching with:
- Cache checking before API calls (search same Pokemon twice)
- Orange "From Cache" badge indicator
- Cache TTL (10 minutes)
- Manual cache controls (restore/clear)
-
Products Tab - Refresh capabilities with:
- Pull-to-refresh gesture support
- Auto-refresh timer (30 seconds for demo)
- Refresh cooldown protection
- "Last updated" timestamp
-
Todos Tab - Complete state management with:
- All 6 BaseState types demonstrated
- State banner showing current state with color coding
- SuccessState snackbars on actions
- Force error button for testing ErrorState
Architecture
example/
├── lib/
│ ├── main.dart # BlocManagerTheme setup
│ ├── models/ # Post, Pokemon, Product, Todo
│ ├── repositories/ # Dio-based API client + repos
│ ├── cubits/ # All feature cubits
│ ├── screens/ # Tab screens
│ └── widgets/ # PokemonCard, LoadingCard, ErrorCard
└── README.md # Example-specific docs
See example/README.md for detailed implementation notes.
Contributing
Contributions are welcome! If you'd like to contribute to bloc_manager, please follow these steps:
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Please feel free to:
- Report bugs via GitHub Issues
- Suggest new features or enhancements
- Submit pull requests for bug fixes or new features
- Improve documentation
All contributions are appreciated, and help make this package better for everyone!
Author
Created and maintained by Abubakar Issa.
| 🐙 GitHub | github.com/Teewhydot/bloc_manager |
| linkedin.com/in/issa-abubakar-a0a200189 | |
| 🌐 Portfolio | sirteefyapps.com.ng |
License
MIT © 2026 Abubakar Issa
Libraries
- bloc_manager
- A Flutter package providing base BLoC/Cubit classes, a BlocManager widget, and utility mixins for common state-management patterns.