bloc_small
A lightweight and simplified BLoC (Business Logic Component) library for Flutter, built on top of the flutter_bloc package. bloc_small
simplifies state management, making it more intuitive while maintaining the core benefits of the BLoC pattern.
- Features
- Installation
- Core Concepts
- Basic Usage
- Using Cubit
- Auto Route Integration
- Advanced Usage
- Best Practices
- API Reference
- Contributing
- License
Features
- Simplified BLoC pattern implementation using flutter_bloc
- Easy-to-use reactive programming with Dart streams
- Automatic resource management and disposal
- Integration with GetIt for dependency injection
- Support for loading states and error handling
- Streamlined state updates and event handling
- Built-in support for asynchronous operations
- Seamless integration with freezed for immutable state and event classes
- Enhanced
ReactiveSubject
with powerful stream transformation methods rxdart - Optional integration with auto_route for type-safe navigation, including:
- Platform-adaptive transitions
- Deep linking support
- Nested navigation
- Compile-time route verification
- Clean and consistent navigation API
Installation
Add bloc_small
to your pubspec.yaml
file:
dependencies:
bloc_small:
injectable:
dev_dependencies:
injectable_generator:
build_runner:
freezed:
Then run:
flutter pub run build_runner build --delete-conflicting-outputs
Note: Remember to run the build runner command every time you make changes to files that use Freezed or Injectable annotations. This generates the necessary code for your BLoCs, events, and states.
Core Concepts
Class | Description | Base Class | Purpose |
---|---|---|---|
MainBloc |
Foundation for BLoC pattern implementation | MainBlocDelegate |
Handles events and emits states |
MainCubit |
Simplified state management alternative | MainCubitDelegate |
Direct state mutations without events |
MainBlocEvent |
Base class for all events | - | Triggers state changes in BLoCs |
MainBlocState |
Base class for all states | - | Represents application state |
CommonBloc |
Global functionality manager | - | Manages loading states and common features |
BLoC Pattern:
@injectable
class CounterBloc extends MainBloc<CounterEvent, CounterState> {
CounterBloc() : super(const CounterState.initial()) {
on<Increment>(_onIncrement);
}
}
Cubit Pattern:
@injectable
class CounterCubit extends MainCubit<CounterState> {
CounterCubit() : super(const CounterState.initial());
void increment() => emit(state.copyWith(count: state.count + 1));
}
Basic Usage
1. Set up Dependency Injection
Use GetIt and Injectable for dependency injection:
@InjectableInit()
void configureInjectionApp() {
// Step 1: Register core dependencies from package bloc_small
getIt.registerCore();
// Step 2: Register your app dependencies
getIt.init();
}
void main() {
WidgetsFlutterBinding.ensureInitialized();
configureInjectionApp(); // Initialize both core and app dependencies
runApp(MyApp());
}
Important: The
RegisterModule
class with theCommonBloc
singleton is essential. If you don't include this Dependency Injection setup, your app will encounter errors. TheCommonBloc
is used internally bybloc_small
for managing common functionalities like loading states across your app.
Make sure to call configureInjectionApp()
before running your app
2. Define your BLoC
@injectable
class CountBloc extends MainBloc<CountEvent, CountState> {
CountBloc() : super(const CountState.initial()) {
on<Increment>(_onIncrementCounter);
on<Decrement>(_onDecrementCounter);
}
Future<void> _onIncrementCounter(Increment event, Emitter<CountState> emit) async {
await blocCatch(actions: () async {
await Future.delayed(Duration(seconds: 2));
emit(state.copyWith(count: state.count + 1));
});
}
void _onDecrementCounter(Decrement event, Emitter<CountState> emit) {
if (state.count > 0) emit(state.copyWith(count: state.count - 1));
}
}
3. Define Events and 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 Decrement extends CountEvent with _$Decrement {
const Decrement._() : super._();
const factory Decrement() = _Decrement;
}
@freezed
sealed class CountState extends MainBlocState with _$CountState {
const CountState._();
const factory CountState.initial({@Default(0) int count}) = _Initial;
}
4. Create a StatefulWidget with BaseBlocPageState
class MyHomePage extends StatefulWidget {
MyHomePage({required this.title});
final String title;
@override
MyHomePageState createState() => _MyHomePageState();
}
class MyHomePageState extends BaseBlocPageState<MyHomePage, CountBloc> {
@override
Widget buildPage(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: BlocBuilder<CountBloc, CountState>(
builder: (context, state) {
return Text(
'${state.count}',
);
},
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () => bloc.add(Increment()),
tooltip: 'Increment',
child: Icon(Icons.add),
),
FloatingActionButton(
onPressed: () => bloc.add(Decrement()),
tooltip: 'decrement',
child: Icon(Icons.remove),
),
],
),
);
}
}
Using Cubit (Alternative Approach)
If you prefer a simpler approach without events, you can use Cubit instead of BLoC:
1. Define your Cubit
@injectable
class CounterCubit extends MainCubit<CounterState> {
CounterCubit() : super(const CounterState.initial());
Future<void> increment() async {
await cubitCatch(
actions: () async {
await Future.delayed(Duration(seconds: 1));
emit(state.copyWith(count: state.count + 1));
},
keyLoading: 'increment',
);
}
void decrement() {
if (state.count > 0) {
emit(state.copyWith(count: state.count - 1));
}
}
}
2. Define Cubit State with Freezed
@freezed
sealed class CountState extends MainBlocState with _$CountState {
const CountState._();
const factory CountState.initial({@Default(0) int count}) = _Initial;
}
3. Create a StatefulWidget with BaseCubitPageState
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends BaseCubitPageState<CounterPage, CountCubit> {
@override
Widget buildPage(BuildContext context) {
return buildLoadingOverlay(
loadingKey: 'increment',
child: Scaffold(
appBar: AppBar(title: const Text('Counter Example')),
body: Center(
child: BlocBuilder<CountCubit, CountState>(
builder: (context, state) {
return Text(
'${state.count}',
style: const TextStyle(fontSize: 48),
);
},
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () => bloc.increment(),
child: const Icon(Icons.add),
),
const SizedBox(height: 16),
FloatingActionButton(
onPressed: () => bloc.decrement(),
child: const Icon(Icons.remove),
),
],
),
),
);
}
}
Feature | BLoC | Cubit |
---|---|---|
Event Handling | Uses events | Direct method calls |
Base Class | MainBloc |
MainCubit |
Widget State | BaseBlocPageState |
BaseCubitPageState |
Complexity | More boilerplate | Simpler implementation |
Use Case | Complex state logic | Simple state changes |
Using StatelessWidget
bloc_small also supports StatelessWidget with similar functionality to StatefulWidget implementations.
1. Using BLoC with StatelessWidget
class MyHomePage extends BaseBlocPage<CountBloc> {
const MyHomePage({super.key});
@override
Widget buildPage(BuildContext context) {
return buildLoadingOverlay(
context,
child: Scaffold(
appBar: AppBar(title: const Text('Counter Example')),
body: Center(
child: BlocBuilder<CountBloc, CountState>(
builder: (context, state) {
return Text(
'${state.count}',
style: const TextStyle(fontSize: 48),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => bloc.add(const Increment()),
child: const Icon(Icons.add),
),
),
);
}
}
2. Using Cubit with StatelessWidget
class CounterPage extends BaseCubitPage<CountCubit> {
const CounterPage({super.key});
@override
Widget buildPage(BuildContext context) {
return buildLoadingOverlay(
context,
child: Scaffold(
appBar: AppBar(title: const Text('Counter Example')),
body: Center(
child: BlocBuilder<CountCubit, CountState>(
builder: (context, state) {
return Text(
'${state.count}',
style: const TextStyle(fontSize: 48),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => cubit.increment(),
child: const Icon(Icons.add),
),
),
);
}
}
Key Features of StatelessWidget Implementation
Feature | Description |
---|---|
Base Classes | BaseBlocPage and BaseCubitPage |
DI Support | Automatic dependency injection |
Loading Management | Built-in loading overlay support |
Navigation | Integrated navigation capabilities |
State Management | Full BLoC/Cubit pattern support |
When to Use StatelessWidget vs StatefulWidget
Use Case | Widget Type |
---|---|
Simple UI without local state | StatelessWidget |
Complex UI with local state | StatefulWidget |
Performance-critical screens | StatelessWidget |
Screens with lifecycle needs | StatefulWidget |
If you want to use Auto Route Integration
- Add auto_route to your dependencies:
dev_dependencies:
auto_route_generator:
- Create your router:
@AutoRouterConfig()
@LazySingleton()
class AppRouter extends BaseAppRouter {
@override
List<AutoRoute> get routes => [
AutoRoute(page: HomeRoute.page, initial: true),
AutoRoute(page: SettingsRoute.page),
];
}
- Register Router in Dependency Injection
Register your router during app initialization:
void configureInjectionApp() {
// Register AppRouter (recommended)
getIt.registerAppRouter<AppRouter>(AppRouter(), enableNavigationLogs: true);
// Register other dependencies
getIt.registerCore();
getIt.init();
}
- Setup MaterialApp
Configure your MaterialApp to use auto_route:
class MyApp extends StatelessWidget {
final _router = getIt<AppRouter>();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _router.config(),
// ... other MaterialApp properties
);
}
}
- Navigation
Use the provided AppNavigator
for consistent navigation across your app:
class MyWidget extends StatelessWidget {
final navigator = getIt.getNavigator();
void _onNavigate() {
navigator?.push(const HomeRoute());
}
}
Use the bloc and cubit provided AppNavigator
:
class _MyWidgetState extends BaseBlocPageState<MyWidget, MyWidgetBloc> {
void _onNavigate() {
navigator?.push(const HomeRoute());
}
}
class _MyWidgetState extends BaseCubitPageState<MyWidget, MyWidgetCubit> {
void _onNavigate() {
navigator?.push(const HomeRoute());
}
}
// Basic navigation
navigator?.push(const HomeRoute());
// Navigation with parameters
navigator?.push(UserRoute(userId: 123));
Best Practices
- Always register AppRouter in your DI setup
- Use the type-safe methods provided by AppNavigator
- Handle potential initialization errors
- Consider creating a navigation service class for complex apps
Features
- Type-safe routing
- Automatic route generation
- Platform-adaptive transitions
- Deep linking support
- Nested navigation
- Integration with dependency injection
Benefits
- Compile-time route verification
- Clean and consistent navigation API
- Reduced boilerplate code
- Better development experience
- Easy integration with bloc_small package
For more complex navigation scenarios and detailed documentation, refer to the auto_route documentation.
Note: While you can use any navigation solution, this package is optimized to work with auto_route. The integration between auto_route and this package provides
If you choose a different navigation solution, you'll need to implement your own navigation registration strategy.
Advanced Usage
Handling Loading States
bloc_small
provides a convenient way to manage loading states and display loading indicators using the CommonBloc
and the buildLoadingOverlay
method.
Using buildLoadingOverlay
When using BaseBlocPageState
, you can easily add a loading overlay to your entire page:
class MyHomePageState extends BaseBlocPageState<MyHomePage, CountBloc> {
@override
Widget buildPage(BuildContext context) {
return buildLoadingOverlay(
child: Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pushed the button this many times:'),
BlocBuilder<CountBloc, CountState>(
builder: (context, state) {
return Text('${state.count}');
},
)
],
),
),
floatingActionButton: Wrap(
spacing: 5,
children: [
FloatingActionButton(
onPressed: () => bloc.add(Increment()),
tooltip: 'Increment',
child: Icon(Icons.add),
),
FloatingActionButton(
onPressed: () => bloc.add(Decrement()),
tooltip: 'decrement',
child: Icon(Icons.remove),
),
],
),
),
);
}
}
The buildLoadingOverlay
method wraps your page content and automatically displays a loading indicator when the loading state is active.
Customizing the Loading Overlay
You can customize the loading overlay by providing a loadingWidget
and specifying a loadingKey
:
buildLoadingOverlay(
child: YourPageContent(),
loadingWidget: YourCustomLoadingWidget(),
loadingKey:'customLoadingKey'
)
Activating the Loading State
To show or hide the loading overlay, use the showLoading
and hideLoading
methods in your BLoC:
class YourBloc extends MainBloc<YourEvent, YourState> {
Future<void> someAsyncOperation() async {
showLoading(); // or showLoading(key: 'customLoadingKey');
try {
// Perform async operation
} finally {
hideLoading(); // or hideLoading(key: 'customLoadingKey');
}
}
}
This approach provides a clean and consistent way to handle loading states across your application, with the flexibility to use global or component-specific loading indicators.
Error Handling
Use the blocCatch
method in your BLoC to handle errors:
await blocCatch(
actions: () async {
// Your async logic here
throw Exception('Something went wrong');
},
onError: (error) {
// Handle the error
print('Error occurred: $error');
}
);
Error Handling with BlocErrorHandlerMixin
bloc_small
provides a mixin for standardized error handling and logging:
@injectable
class CountBloc extends MainBloc<CountEvent, CountState> with BlocErrorHandlerMixin {
CountBloc() : super(const CountState.initial()) {
on<Increment>(_onIncrement);
}
Future<void> _onIncrement(Increment event, Emitter<CountState> emit) async {
await blocCatch(
actions: () async {
// Your async logic that might throw
if (state.count > 5) {
throw ValidationException('Count cannot exceed 5');
}
emit(state.copyWith(count: state.count + 1));
},
onError: handleError, // Uses the mixin's error handler
);
}
}
The mixin provides:
- Automatic error logging with stack traces
- Built-in support for common exceptions (NetworkException, ValidationException, TimeoutException)
- Automatic loading state cleanup
- Helper method for error messages
You can get error messages without state emission:
String message = getErrorMessage(error); // Returns user-friendly error message
For custom error handling, override the handleError method:
@override
Future<void> handleError(Object error, StackTrace stackTrace) async {
// Always call super to maintain logging
super.handleError(error, stackTrace);
// Add your custom error handling here
if (error is CustomException) {
// Handle custom exception
}
}
Lifecycle Management
bloc_small provides lifecycle hooks to manage state and resources based on widget lifecycle events.
Using Lifecycle Hooks in BLoC
@injectable
class CounterBloc extends MainBloc<CounterEvent, CounterState> {
Timer? _timer;
CounterBloc() : super(const CounterState.initial()) {
on<StartTimer>(_onStartTimer);
}
@override
void onDependenciesChanged() {
// Called when dependencies change (e.g., Theme, Locale)
add(const CounterEvent.checkDependencies());
}
@override
void onDeactivate() {
// Called when widget is temporarily removed
_timer?.cancel();
}
Future<void> _onStartTimer(StartTimer event, Emitter<CounterState> emit) async {
_timer = Timer.periodic(Duration(seconds: 1), (_) {
add(const CounterEvent.increment());
});
}
}
Implementation in Widget
class CounterPage extends StatefulWidget {
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends BaseBlocPageState<CounterPage, CounterBloc> {
@override
Widget buildPage(BuildContext context) {
return buildLoadingOverlay(
child: Scaffold(
body: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) => Text('Count: ${state.count}'),
),
floatingActionButton: FloatingActionButton(
onPressed: () => bloc.add(const StartTimer()),
child: Icon(Icons.play_arrow),
),
),
);
}
}
The lifecycle hooks are automatically managed by the base classes and provide:
- Automatic resource cleanup
- State synchronization with system changes
- Proper handling of widget lifecycle events
- Memory leak prevention
ReactiveSubject
ReactiveSubject<T>
is a wrapper around RxDart's BehaviorSubject/PublishSubject, providing a simplified API for reactive programming.
API Reference
Core API
Category | Method/Property | Description |
---|---|---|
Constructors | ReactiveSubject({T? initialValue}) |
Creates new with BehaviorSubject |
ReactiveSubject.broadcast() |
Creates new with PublishSubject | |
Properties | value |
Current value |
stream |
Underlying stream | |
isClosed |
Check if closed | |
isDisposed |
Check if disposed | |
Core Methods | add(T value) |
Add new value |
addError(Object error) |
Add error | |
dispose() |
Release resources | |
listen() |
Subscribe to stream |
Transformation Methods
Method | Description | Example |
---|---|---|
map<R>() |
Transform values | subject.map((i) => i * 2) |
where() |
Filter values | subject.where((i) => i > 0) |
switchMap() |
Switch streams | subject.switchMap((i) => api.fetch(i)) |
distinct() |
Remove duplicates | subject.distinct() |
scan() |
Accumulate values | subject.scan((sum, val) => sum + val) |
Time Control
Method | Description | Example |
---|---|---|
debounceTime() |
Delay emissions | subject.debounceTime(300.ms) |
throttleTime() |
Rate limit | subject.throttleTime(1.seconds) |
buffer() |
Collect over time | subject.buffer(timer) |
Error Handling
Method | Description | Example |
---|---|---|
retry() |
Retry on error | subject.retry(3) |
onErrorResumeNext() |
Recover from error | subject.onErrorResumeNext(backup) |
debug() |
Debug stream | subject.debug(tag: 'MyStream') |
State Management
Method | Description | Example |
---|---|---|
share() |
Share subscription | subject.share() |
shareReplay() |
Cache and replay | subject.shareReplay(maxSize: 2) |
groupBy() |
Group values | subject.groupBy((val) => val.type) |
Static Methods
Method | Description | Example |
---|---|---|
combineLatest() |
Combine multiple subjects | ReactiveSubject.combineLatest([s1, s2]) |
merge() |
Merge multiple subjects | ReactiveSubject.merge([s1, s2]) |
fromFutureWithError() |
Create from Future | ReactiveSubject.fromFutureWithError(future) |
Basic Usage Example
// Create and initialize
final subject = ReactiveSubject<int>(initialValue: 0);
// Transform and handle errors
final stream = subject
.map((i) => i * 2)
.debounceTime(Duration(milliseconds: 300))
.retry(3)
.debug(tag: 'MyStream');
// Subscribe
final subscription = stream.listen(
print,
onError: handleError,
);
// Cleanup
await subject.dispose();
This organization
- Groups related methods together
- Provides clear, concise descriptions
- Includes practical examples
- Maintains all essential information while being more readable
- Ends with a practical usage example
License
This project is licensed under the MIT License - see the LICENSE file for details.
Libraries
- base/base_app_router
- base/base_bloc_page
- base/base_bloc_page_state
- base/base_cubit_page
- base/base_cubit_state
- base/base_page_delegate
- base/base_page_stateless_delegate
- bloc/common/common_bloc
- bloc/core/base_delegate
- bloc/core/bloc/main_bloc
- bloc/core/cubit/main_cubit
- bloc/core/di/di
- bloc/core/error/bloc_error_handler
- bloc/core/error/cubit_error_handler
- bloc/core/error/error_state
- bloc/core/error/exceptions
- bloc/core/main_bloc_event
- bloc/core/main_bloc_state
- bloc_small
- constant/default_loading
- extensions/bloc_context_extension
- utils/reactive_subject
- widgets/loading_indicator