centralized_bloc
A lightweight and reusable Flutter BLoC architecture for centralized state handling, loading, error handling, and UI building.
centralized_bloc provides a simple way to manage common BLoC states without repeatedly writing BlocBuilder, BlocListener, loading widgets, and error widgets throughout your application.
Features
- ð Simple reusable
BaseBloc - ð Centralized loading state handling
- â Centralized success state handling
- â Centralized error state handling
- ðĻ Global UI configuration
- âïļ Configurable initial, loading, and error widgets
- ðŊ Application-wide default UI
- ð§ Built-in
BlocListenersupport - ð§Đ Combines
BlocBuilderandBlocListener - ð ïļ
maybeMap()for handling only required states - ð Customizable error handling
- ðŠķ Lightweight with minimal dependencies
- ð Works with any repository/API implementation
Installation
Add centralized_bloc to your pubspec.yaml:
dependencies:
centralized_bloc: ^1.0.0
Then run:
flutter pub get
Dependencies
This package intentionally keeps dependencies minimal:
flutter_bloc
It does not depend on Dio, Freezed, Retrofit, or any specific networking package.
You can use it with:
- HTTP
- Dio
- Firebase
- GraphQL
- Local databases
- REST APIs
- Any custom repository
Basic Architecture
The package provides four main states:
Initial
â
Loading
â
Loaded
â
Error
The main classes are:
BaseState<T>
âââ Initial<T>
âââ Loading<T>
âââ Loaded<T>
âââ Error<T>
BaseBloc<E, M>
CentralizedBloc<B, T>
CentralizedBlocConfig
Where:
E= Event typeM= Model/data typeB= BLoC typeT= Loaded data type
1. Create Your Model
For example:
class User {
final int id;
final String name;
final String email;
const User({
required this.id,
required this.name,
required this.email,
});
}
2. Create Your Repository
The package does not require a specific networking library.
Your repository can contain any data source.
import 'package:example/model/user.dart';
class UserRepository {
Future<User> getUser() async {
await Future.delayed(
const Duration(seconds: 2),
);
return const User(
id: 1,
name: 'Centralized Bloc',
email: 'bloc@example.com',
);
}
}
For a real application, this could instead call an API, database, Firebase, etc.
3. Create Your Events
Define your BLoC events normally:
sealed class UserEvent {
const UserEvent();
}
final class LoadUser extends UserEvent {
const LoadUser();
}
4. Create Your BLoC
Extend BaseBloc:
import 'package:centralized_bloc/centralized_bloc.dart';
import 'package:example/model/user.dart';
import 'package:example/repository/user_repository.dart';
import 'user_event.dart';
class UserBloc extends BaseBloc<UserEvent, User> {
final UserRepository repository;
UserBloc({
required this.repository,
}) {
on<LoadUser>(
defaultHandler(
(event, emit) {
return repository.getUser();
},
),
);
}
}
The defaultHandler() automatically handles:
emit Loading
â
execute repository operation
â
emit Loaded(data)
If an exception occurs:
Exception
â
Error handler
â
emit Error(message)
5. Provide the BLoC
Use the normal BlocProvider from flutter_bloc:
import 'package:centralized_bloc/centralized_bloc.dart';
import 'package:example/bloc/user_bloc.dart';
import 'package:example/bloc/user_event.dart';
import 'package:example/repository/user_repository.dart';
import 'package:example/user_screen.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: BlocProvider(
create: (_) => UserBloc(
repository: UserRepository(),
)..add(const LoadUser()),
child: const UserScreen(),
),
);
}
}
6. Build the UI with CentralizedBloc
Instead of writing:
BlocBuilder
and
BlocListener
separately, use CentralizedBloc.
CentralizedBloc automatically displays:
SizedBox.shrink()forInitialCircularProgressIndicatorforLoading- Your widget for
Loaded - Error message for
Error
7. Add a listener and onDataLoaded
You can also listen to state changes using the optional listener parameter.
CentralizedBloc<UserBloc, User>(
listener: (context, state) {
state.maybeMap(
loaded: (user) {
// Handle successful loading.
},
error: (message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
),
);
},
);
},
onDataLoaded: (context, user) {
return _UserView(user: user);
},
)
This allows you to keep UI rendering and side effects in one place.
For example:
CentralizedBloc
â
âââ UI building
â âââ onDataLoaded
â
âââ State listening
âââ listener
8. Complete User Screen
import 'package:centralized_bloc/centralized_bloc.dart';
import 'package:example/bloc/user_bloc.dart';
import 'package:example/model/user.dart';
import 'package:flutter/material.dart';
class UserScreen extends StatelessWidget {
const UserScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Centralized Bloc'),
),
body: CentralizedBloc<UserBloc, User>(
listener: (context, state) {
state.maybeMap(
loaded: (user) {
// User loaded successfully.
},
error: (message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
),
);
},
);
},
onDataLoaded: (context, user) {
return _UserView(user: user);
},
),
);
}
}
class _UserView extends StatelessWidget {
final User user;
const _UserView({
required this.user,
});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
user.name,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(user.email),
],
),
);
}
}
9. Global UI Configuration
Configure the default UI once for the entire application.
void main() {
CentralizedBlocConfig.configure(
loadingBuilder: (context) {
return const Center(
child: CircularProgressIndicator.adaptive(),
);
},
errorBuilder: (context, message) {
return Center(
child: Text(message),
);
},
);
runApp(const MyApp());
}
You can configure:
initialBuilder loadingBuilder errorBuilder
If you do not configure them, the package uses its default widgets.
To restore the defaults:
CentralizedBlocConfig.reset();
Error Handling
BaseBloc automatically catches exceptions thrown by your operation.
For example:
defaultHandler(
(event, emit) async {
return repository.getUser();
},
);
If repository.getUser() throws an exception, the BLoC automatically changes to:
BaseState.error(message)
The default error message is generated from:
error.toString()
Custom Error Handling
You can provide your own error handler:
class UserBloc extends BaseBloc<UserEvent, User> {
final UserRepository repository;
UserBloc({
required this.repository,
}) : super(
errorHandler: (error) {
return 'Unable to load user';
},
) {
on<LoadUser>(
defaultHandler(
(event, emit) {
return repository.getUser();
},
),
);
}
}
This is useful when your application uses Dio, HTTP, Firebase, or another API layer.
For example, your application can convert different exceptions into user-friendly messages:
errorHandler: (error) {
if (error is SomeApiException) {
return 'Something went wrong while contacting the server.';
}
return 'Something went wrong.';
},
The package itself does not need to know which networking library your application uses.
Why Use defaultHandler()?
Without BaseBloc, you may repeatedly write:
on<LoadUser>((event, emit) async {
try {
emit(const BaseState.loading());
final user = await repository.getUser();
emit(BaseState.loaded(user));
} catch (error) {
emit(
BaseState.error(
error.toString(),
),
);
}
});
With BaseBloc:
on<LoadUser>(
defaultHandler(
(event, emit) {
return repository.getUser();
},
),
);
The common state-management logic is centralized.
Complete Flow
A typical request follows this flow:
User opens screen
â
âž
LoadUser event
â
âž
UserBloc
â
âž
defaultHandler()
â
âââââââââââââââââ
âž â
Loading â
â â
âž â
Repository â
â â
âââââ Success âââĪ
â â
âž âž
Loaded Error
â â
âž âž
onDataLoaded Error UI
â
âž
User View
Philosophy
centralized_bloc is designed to keep repetitive BLoC state-management code in one place while allowing application developers to keep their repositories and models completely independent.
The package focuses on:
- Simple APIs
- Minimal dependencies
- Reusable architecture
- Generic error handling
- Less UI boilerplate
- Compatibility with different data sources
Contact
For questions, suggestions, or support:
Email: itsmaddeveloper@gmail.com