mono_bloc 1.0.7
mono_bloc: ^1.0.7 copied to clipboard
Simplify Flutter Bloc with code generation. Define events as methods, get automatic concurrency control, actions for side effects, and async state management.
MonoBloc #
Simplify your Flutter Bloc code with annotations and automatic code generation
Write cleaner, more maintainable Bloc classes by defining events as simple methods. MonoBloc eliminates boilerplate event and state classes, reduces naming conflicts, and provides built-in concurrency control with action side-effects.
Features #
- β¨ Less Boilerplate - Define events as methods instead of separate classes
- ποΈ Built-in Concurrency - Use transformers with simple annotations
- π― Action Side-Effects - Handle navigation, dialogs, and snackbars with @MonoActions() mixin
- β»οΈ Automatic Async State - @AsyncMonoBloc handles loading/error states automatically
- β©οΈ Flexible Returns - Support for State, Future
- π¨ Error Handling - Centralized error handling with @onError
- ποΈ Event Queues - Sequential processing for related operations
- πͺ Flutter Hooks Support - mono_bloc_hooks package for cleaner action handling in HookWidget
Why MonoBloc? #
Traditional Bloc architecture requires separate event classes, leading to verbose code and boilerplate. MonoBloc simplifies this by letting you define events as simple methods, automatically generating all the boilerplate while providing powerful features like built-in concurrency control, action side-effects, and automatic async state management.
Table of Contents #
- Quick Start
- Feature Example
- Event Patterns
- Actions - Side Effects Pattern
- Concurrency Transformers
- Event Queues
- Event Filtering with @onEvent
- Error Handling
- Troubleshooting
- Coding Agents Instructions
- Contributing
Quick Start #
1. Install #
For pure Dart projects:
dependencies:
mono_bloc: ^1.0.0
dev_dependencies:
mono_bloc_generator: ^1.0.0
build_runner: ^2.10.0
For Flutter projects:
dependencies:
flutter:
sdk: flutter
mono_bloc_flutter: ^1.0.0 # Exports flutter_bloc + mono_bloc
dev_dependencies:
mono_bloc_generator: ^1.0.0
build_runner: ^2.10.0
For Flutter + Hooks projects:
dependencies:
flutter:
sdk: flutter
mono_bloc_flutter: ^1.0.0 # Exports flutter_bloc + mono_bloc
mono_bloc_hooks: ^1.0.0
flutter_hooks: ^0.21.0
dev_dependencies:
mono_bloc_generator: ^1.0.0
build_runner: ^2.10.0
2. Create your bloc #
import 'package:mono_bloc/mono_bloc.dart';
part 'counter_bloc.g.dart';
@MonoBloc()
class CounterBloc extends _$CounterBloc<int> {
CounterBloc() : super(0);
@event
int _onIncrement() => state + 1;
@event
int _onDecrement() => state - 1;
@event
int _onReset() => 0;
}
3. Generate #
Run the code generator:
dart run build_runner build --delete-conflicting-outputs
4. Use #
void main() {
final bloc = CounterBloc();
// Generated methods - clean and type-safe
bloc.increment();
bloc.decrement();
bloc.reset();
print(bloc.state); // 0
}
Feature Example #
Here's a comprehensive example showcasing all MonoBloc features:
import 'package:mono_bloc_flutter/mono_bloc_flutter.dart';
part 'todo_bloc.g.dart';
// 1. Define actions in a private mixin with @MonoActions()
@MonoActions()
mixin _TodoBlocActions {
void showSuccess(String message);
void navigateToDetail(String todoId);
}
// 2. Sequential mode - all events processed in order, waiting for each to finish
@MonoBloc(sequential: true)
class TodoBloc extends _$TodoBloc<TodoState> {
TodoBloc() : super(TodoState());
// 3. Events with different return types
@event
Future<TodoState> _onLoadTodos() async {
final todos = await repository.fetchTodos();
return state.copyWith(todos: todos);
}
@restartableEvent // 4. Concurrency control - cancels previous search
Stream<TodoState> _onSearch(String query) async* {
yield state.copyWith(isSearching: true);
final results = await repository.search(query);
yield state.copyWith(isSearching: false, todos: results);
}
@event // 5. Stream with loading/data yields for progressive updates
Stream<TodoState> _onLoadFromMultipleSources() async* {
yield state.copyWith(isLoading: true);
final allTodos = <Todo>[];
for (final source in TodoSource.values) {
final todos = await repository.fetchFrom(source);
allTodos.addAll(todos);
yield state.copyWith(isLoading: false, todos: allTodos);
}
}
@droppableEvent // 6. Prevents duplicate submissions
Future<TodoState> _onAddTodo(String title) async {
final todo = await repository.add(title);
showSuccess('Todo added!'); // Trigger action
return state.copyWith(todos: [...state.todos, todo]);
}
@event
Future<TodoState> _onToggleTodo(String id) async {
final todos = state.todos.map((t) => t.id == id ? t.copyWith(completed: !t.completed) : t).toList();
return state.copyWith(todos: todos);
}
@event
Future<TodoState> _onDeleteTodo(String id) async {
await repository.delete(id);
navigateToDetail('list'); // Trigger navigation action
return state.copyWith(todos: state.todos.where((t) => t.id != id).toList());
}
// 7. Error handling - centralized for all events
@onError
TodoState _onError(Object error, StackTrace stackTrace) {
return state.copyWith(error: error.toString());
}
// 8. Initialization - runs automatically on creation
@onInit
void _onInit() {
loadTodos(); // Dispatch event to load initial data
}
}
// 9. Flutter integration - handle actions with MonoBlocActionListener
class TodoPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MonoBlocActionListener<TodoBloc>(
actions: TodoBlocActions.when(
showSuccess: (context, message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
},
navigateToDetail: (context, todoId) {
Navigator.pushNamed(context, '/detail', arguments: todoId);
},
),
child: BlocBuilder<TodoBloc, TodoState>(
builder: (context, state) {
// Build UI based on state
},
),
);
}
}
// 10. Flutter Hooks support - cleaner action handling
class TodoPageWithHooks extends HookWidget {
@override
Widget build(BuildContext context) {
final bloc = useBloc<TodoBloc>();
useMonoBlocActionListener(
bloc,
TodoBlocActions.when(
showSuccess: (context, message) { /* Show snackbar */ },
navigateToDetail: (context, todoId) { /* Navigate */ },
),
);
return BlocBuilder<TodoBloc, TodoState>(
builder: (context, state) {
// Build UI
},
);
}
}
For more complete examples, see the example directory.
Event Patterns #
MonoBloc supports multiple return patterns for maximum flexibility:
Direct state return #
The simplest pattern - just return the new state:
@event
int _onIncrement() => state + 1;
@event
CounterState _onSetValue(int value) => CounterValue(value);
Async state return #
For asynchronous operations:
@event
Future<TodoState> _onLoadTodos() async {
final todos = await repository.fetchTodos();
return TodoState(todos: todos);
}
Stream state return #
For progressive updates:
@event
Stream<CounterState> _onLoadAsync() async* {
yield CounterLoading();
await Future.delayed(Duration(seconds: 2));
yield CounterValue(42);
}
Important: When using stream-returning events with transformers like @restartableEvent, dispatching a new event will cancel the previous stream. This means any ongoing async operations (like repository fetches or network calls) within the stream will be interrupted. This is useful for scenarios like search-as-you-type, where you want to cancel the previous search when the user types a new query.
Emitter pattern #
For multiple emissions with full control, use the generated _Emitter typedef:
@event
Future<void> _onComplexOperation(_Emitter emit) async {
emit(LoadingState());
try {
final result = await doWork();
emit(SuccessState(result));
} catch (e) {
emit(ErrorState(e));
}
}
Actions - Side Effects Pattern #
Actions provide a clean pattern for handling side effects that don't modify bloc state, such as navigation, showing dialogs, triggering analytics, or displaying notifications. Actions are emitted to a separate stream and can be handled in the UI layer without affecting your state management.
π± Flutter Project Detection: In Flutter projects (with
flutterSDK inpubspec.yaml), all action handlers automatically receiveBuildContextas the first parameter. In pure Dart projects, actions don't includeBuildContext. This is determined by project type, not by file imports.
Why actions? #
- Separation of Concerns: Keep side effects separate from state
- Type Safety: Full type checking for all action parameters
- Pattern Matching: Use
when()orof()for exhaustive action handling - No State Pollution: Navigation/dialogs don't belong in state
- Clean UI Code: Handle actions with simple stream listeners
Basic usage #
Define actions in a private mixin annotated with @MonoActions(). All abstract void methods in the mixin automatically become actions:
import 'package:mono_bloc/mono_bloc.dart';
part 'checkout_bloc.g.dart';
enum NotificationType { success, error, warning }
// 1. Define actions in a private mixin with @MonoActions()
@MonoActions()
mixin _CheckoutBlocActions {
void navigateToConfirmation(String orderId);
void showNotification({
required String message,
required NotificationType type,
});
void trackAnalyticsEvent(String eventName, Map<String, dynamic> properties);
}
// 2. Bloc class - the generated base class includes the actions mixin
@MonoBloc()
class CheckoutBloc extends _$CheckoutBloc<CheckoutState> {
CheckoutBloc() : super(CheckoutState());
// Events - modify state as usual
@event
Future<CheckoutState> _onSubmitOrder(Order order) async {
try {
final orderId = await repository.submitOrder(order);
// Trigger actions during event processing
trackAnalyticsEvent('order_submitted', {'orderId': orderId});
navigateToConfirmation(orderId);
showNotification(
message: 'Order submitted successfully!',
type: NotificationType.success,
);
return state.copyWith(isProcessing: false);
} catch (e) {
showNotification(
message: 'Failed to submit order',
type: NotificationType.error,
);
rethrow;
}
}
}
Handling actions in UI - MonoBlocActionListener #
Use MonoBlocActionListener<YourBloc> widget to handle actions declaratively (recommended):
import 'package:mono_bloc_flutter/mono_bloc_flutter.dart';
class CheckoutPage extends StatelessWidget {
const CheckoutPage({super.key});
@override
Widget build(BuildContext context) {
return MonoBlocActionListener<CheckoutBloc>(
// Generated CheckoutBlocActions with named callbacks
actions: CheckoutBlocActions.when(
navigateToConfirmation: (context, orderId) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ConfirmationPage(orderId: orderId),
),
);
},
showNotification: (context, message, type) {
Color color;
switch (type) {
case NotificationType.success:
color = Colors.green;
case NotificationType.error:
color = Colors.red;
case NotificationType.warning:
color = Colors.orange;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: color,
),
);
},
trackAnalyticsEvent: (context, eventName, properties) {
analytics.track(eventName, properties);
},
),
child: BlocBuilder<CheckoutBloc, CheckoutState>(
builder: (context, state) {
return CheckoutForm(
isProcessing: state.isProcessing,
onSubmit: (order) {
context.read<CheckoutBloc>().submitOrder(order);
},
);
},
),
);
}
}
Using of() with interface implementation #
For cleaner action handling, implement the generated actions interface and use of():
import 'package:mono_bloc_flutter/mono_bloc_flutter.dart';
/// Implements CheckoutBlocActions interface - all action methods in one place
class CheckoutPage extends StatelessWidget implements CheckoutBlocActions {
const CheckoutPage({super.key});
@override
void navigateToConfirmation(BuildContext context, String orderId) {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => ConfirmationPage(orderId: orderId)),
);
}
@override
void showNotification(BuildContext context, String message, NotificationType type) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}
@override
void trackAnalyticsEvent(BuildContext context, String eventName, Map<String, dynamic> properties) {
analytics.track(eventName, properties);
}
@override
Widget build(BuildContext context) {
return MonoBlocActionListener<CheckoutBloc>(
// Use of() to wire this instance as the action handler
actions: CheckoutBlocActions.of(this),
child: BlocBuilder<CheckoutBloc, CheckoutState>(
builder: (context, state) {
return CheckoutForm(onSubmit: context.read<CheckoutBloc>().submitOrder);
},
),
);
}
}
When to use which pattern:
when()- Inline callbacks, good for simple pages with few actionsof()- Interface implementation, better for complex pages or reusable action handlers
Manual subscription (alternative) #
You can also manually subscribe to the actions stream using didChangeDependencies:
class CheckoutPage extends StatefulWidget {
@override
State<CheckoutPage> createState() => _CheckoutPageState();
}
class _CheckoutPageState extends State<CheckoutPage> {
StreamSubscription? _actionsSubscription;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_initSubscription();
}
void _initSubscription() {
if (_actionsSubscription != null) return; // Already initialized
final bloc = context.read<CheckoutBloc>();
// Create actions handler
final actionHandler = CheckoutBlocActions.when(
navigateToConfirmation: (context, orderId) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ConfirmationPage(orderId: orderId),
),
);
},
showNotification: (context, message, type) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
},
trackAnalyticsEvent: (context, eventName, properties) {
analytics.track(eventName, properties);
},
);
// Subscribe to actions stream
_actionsSubscription = bloc.actions.listen(
(action) => actionHandler.actions(context, action),
);
}
@override
void dispose() {
_actionsSubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<CheckoutBloc, CheckoutState>(
builder: (context, state) {
return CheckoutForm(
isProcessing: state.isProcessing,
onSubmit: (order) {
context.read<CheckoutBloc>().submitOrder(order);
},
);
},
);
}
}
With Flutter hooks #
Use the mono_bloc_hooks package for a cleaner approach with HookWidget:
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:mono_bloc_hooks/mono_bloc_hooks.dart';
class CheckoutPage extends HookWidget {
const CheckoutPage({super.key});
@override
Widget build(BuildContext context) {
final bloc = useBloc<CheckoutBloc>();
// Hook automatically manages subscription
useMonoBlocActionListener(
bloc,
CheckoutBlocActions.when(
navigateToConfirmation: (context, orderId) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ConfirmationPage(orderId: orderId),
),
);
},
showNotification: (context, message, type) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
},
),
);
return BlocBuilder<CheckoutBloc, CheckoutState>(
builder: (context, state) {
return CheckoutForm(onSubmit: bloc.submitOrder);
},
);
}
}
Mixin requirements #
When using actions, follow these rules:
- @MonoActions() mixin: Actions must be defined in a mixin annotated with
@MonoActions()starting with_(e.g.,_CheckoutBlocActions) - Abstract methods only: Action methods must be abstract (no body)
- Return void: All action methods must return
void - Automatic mixin: The generated
_$Blocbase class automatically includes the actions mixin - you don't need to addwith _BlocActions
// Correct: Actions in a private mixin with @MonoActions()
@MonoActions()
mixin _MyBlocActions {
void myAction();
}
// The generated _$MyBloc already includes 'with _MyBlocActions'
@MonoBloc()
class MyBloc extends _$MyBloc<MyState> {
MyBloc() : super(initialState);
}
Inheritance #
Share common actions across multiple blocs by having private action mixins implement a public base mixin. This allows you to define reusable action interfaces that can be shared across your application, enabling consistent error handling, navigation patterns, and other cross-cutting concerns. The generated _$Bloc base class automatically includes the actions mixin.
Concurrency Transformers #
Control how concurrent events are handled with built-in transformers.
Important: Bloc-Level vs Event-Level Sequential Mode #
@MonoBloc(sequential: true) β @sequentialEvent
These are different concurrency controls:
-
@MonoBloc(sequential: true)- Global bloc-level mode- ALL events in the entire bloc wait for each other
- Button clicks, API calls, user input - everything queued sequentially
- Simplest option to prevent any race conditions
-
@sequentialEvent- Per-event annotation- Only that specific event type waits for previous instances of itself
- Other event types can run in parallel
- Fine-grained control per event
Example showing the difference:
// Bloc-level sequential: ALL events wait for each other
@MonoBloc(sequential: true)
class BlocLevelSequential extends _$BlocLevelSequential<State> {
@event
Future<State> _onSearch(String query) async { /* ... */ } // Waits for loadData
@event
Future<State> _onLoadData() async { /* ... */ } // Waits for search
// If loadData is running, search must wait (and vice versa)
}
// Event-level sequential: Only same event types wait for each other
@MonoBloc()
class EventLevelSequential extends _$EventLevelSequential<State> {
@sequentialEvent
Future<State> _onSearch(String query) async { /* ... */ } // Only waits for previous search
@event
Future<State> _onLoadData() async { /* ... */ } // Runs independently
// If loadData is running, search can still execute (they're different events)
}
Default Concurrency Mode #
Set a default concurrency mode for all events using @MonoBloc(concurrency:):
@MonoBloc(concurrency: MonoConcurrency.restartable)
class SearchBloc extends _$SearchBloc<SearchState> {
SearchBloc() : super(const SearchState());
@event // Uses restartable (from bloc default)
Future<SearchState> _onSearch(String query) async {
final results = await api.search(query);
return state.copyWith(results: results);
}
@event // Uses restartable (from bloc default)
Future<SearchState> _onFilter(String filter) async {
final results = await api.filter(filter);
return state.copyWith(results: results);
}
@droppableEvent // Explicit override: uses droppable instead
Future<SearchState> _onLoadMore() async {
final more = await api.loadMore(state.page + 1);
return state.copyWith(results: [...state.results, ...more]);
}
}
Available concurrency modes:
MonoConcurrency.concurrent- Process all events simultaneously (default)MonoConcurrency.sequential- Process events one at a timeMonoConcurrency.restartable- Cancel ongoing event when new one arrivesMonoConcurrency.droppable- Ignore new events while one is processing
Event Queues #
For advanced use cases, group specific events into named queues with custom transformers:
@MonoBloc()
class TodoBloc extends _$TodoBloc<TodoState> {
static const modifyQueue = 'modify';
static const syncQueue = 'sync';
TodoBloc() : super(
TodoState(),
queues: {
modifyQueue: MonoEventTransformer.sequential, // Modify queue: sequential
syncQueue: MonoEventTransformer.droppable, // Sync queue: droppable
},
);
// Modify operations in 'modify' queue (sequential)
@MonoEvent.queue(modifyQueue)
Future<TodoState> _onAddTodo(String title) async {
await repository.add(title);
return await _loadTodos();
}
@MonoEvent.queue(modifyQueue)
Future<TodoState> _onDeleteTodo(String id) async {
await repository.delete(id);
return await _loadTodos();
}
// Sync operations in 'sync' queue (droppable)
@MonoEvent.queue(syncQueue)
Future<TodoState> _onSync() async {
await api.sync();
return await _loadTodos();
}
// Read operations run independently
@restartableEvent
Stream<TodoState> _onSearch(String query) async* {
yield await _performSearch(query);
}
}
When to use what:
@MonoBloc(sequential: true)- Simplest option. ALL events in the entire bloc wait for each other (global sequential mode).- Individual transformers - Per-event control. Only same event types affect each other (
@sequentialEventfor one event,@restartableEventfor another). - Event Queues - Advanced control. Group specific events with custom transformers.
Event Filtering with @onEvent #
Control which events are processed using @onEvent handlers. This is perfect for preventing race conditions, implementing loading state guards, or conditional event processing.
Basic usage - all events #
Filter all events with a single handler:
@MonoBloc()
class TodoBloc extends _$TodoBloc<TodoState> {
TodoBloc() : super(TodoState());
@event
Future<TodoState> _onLoadTodos() async {
final todos = await repository.fetchTodos();
return TodoState(todos: todos);
}
@event
Future<TodoState> _onSaveTodo(Todo todo) async {
await repository.save(todo);
return await _loadCurrentState();
}
/// Prevent any events while loading
@onEvent
bool _onEvents(_Event event) {
// Skip all events if currently loading
if (state.isLoading) {
return false; // Event will be dropped
}
return true; // Event will be processed
}
}
Specific event filtering #
Filter individual event types:
@AsyncMonoBloc()
class DataBloc extends _$DataBloc<Data> {
DataBloc() : super(const MonoAsyncValue.withData(initialData));
@event
Future<Data> _onLoadData() async {
return await repository.loadData();
}
@event
Future<Data> _onRefreshData() async {
return await repository.refreshData();
}
/// Only filter the loadData event
@onEvent
bool _onLoadDataFilter(_LoadDataEvent event) {
// Skip loadData if already loading
if (state.isLoading) {
print('Skipping loadData - already loading');
return false;
}
return true;
}
// refreshData is not filtered, can run anytime
}
Event group filtering #
Filter groups of events like sequential events or queue events:
@MonoBloc()
class TaskBloc extends _$TaskBloc<TaskState> {
TaskBloc() : super(TaskState());
@sequentialEvent
Future<TaskState> _onProcessTask(Task task) async {
await processor.process(task);
return state.addCompleted(task);
}
@sequentialEvent
Future<TaskState> _onExecuteTask(Task task) async {
await executor.execute(task);
return state.addExecuted(task);
}
@event
TaskState _onGetStatus() => state;
/// Filter all sequential events as a group
@onEvent
bool _onSequential(_$SequentialEvent event) {
// Block sequential events if queue is full
if (state.queueSize >= maxQueueSize) {
print('Queue full, dropping sequential event');
return false;
}
return true;
}
// getStatus() is not affected by the filter
}
Queue event filtering #
Filter events in specific queues:
@MonoBloc()
class UploadBloc extends _$UploadBloc<UploadState> {
static const uploadQueue = 'upload';
static const syncQueue = 'sync';
UploadBloc() : super(
UploadState(),
queues: {
uploadQueue: MonoEventTransformer.sequential, // Upload queue
syncQueue: MonoEventTransformer.droppable, // Sync queue
},
);
@MonoEvent.queue(uploadQueue)
Future<UploadState> _onUploadFile(File file) async {
await api.upload(file);
return state.addUploaded(file);
}
@MonoEvent.queue(syncQueue)
Future<UploadState> _onSync() async {
await api.sync();
return await _getCurrentState();
}
/// Filter only upload queue events
@onEvent
bool _onUploadQueue(_$UploadQueueEvent event) {
// Limit concurrent uploads
if (state.activeUploads >= 3) {
return false;
}
return true;
}
// Sync events (sync queue) are not affected
}
Error Handling #
Centralized error handling for your bloc using @onError:
@MonoBloc()
class TodoBloc extends _$TodoBloc<TodoState> {
TodoBloc() : super(TodoState());
@event
Future<TodoState> _onLoadTodos() async {
// Any error is caught and passed to error handler
final todos = await repository.fetchTodos();
return TodoState(todos: todos);
}
@onError
TodoState _onError(Object error, StackTrace stackTrace) {
return state.copyWith(
errorMessage: 'Failed to load: ${error.toString()}',
);
}
}
MonoBloc automatically provides enhanced stack traces for debugging async errors. Every event captures its dispatch location, and when errors occur, you get a combined stack trace showing both where the event was dispatched and where the error occurred, with framework noise automatically filtered out.
Specific error handlers #
Handle errors for specific events:
@MonoBloc()
class MyBloc extends _$MyBloc<MyState> {
@event
Future<MyState> _onAddItem(String item) async {
await repository.add(item);
return SuccessState();
}
// Specific handler for addItem errors
@onError
MyState _onErrorAddItem(Object error, StackTrace stackTrace) {
return ErrorState('Failed to add item: $error');
}
// General error handler for other events
@onError
MyState _onError(Object error, StackTrace stackTrace) {
return ErrorState('An error occurred: $error');
}
}
Troubleshooting #
Required imports #
Every MonoBloc file needs just one import and a part directive:
Pure Dart:
import 'package:mono_bloc/mono_bloc.dart';
part 'my_bloc.g.dart';
@MonoBloc()
class MyBloc extends _$MyBloc<MyState> {
MyBloc() : super(initialState);
@event
MyState _onEvent() => newState;
}
Flutter (with actions):
import 'package:mono_bloc_flutter/mono_bloc_flutter.dart';
part 'my_bloc.g.dart';
@MonoActions()
mixin _MyBlocActions {
void showMessage(String message);
}
// No need to add 'with _MyBlocActions' - it's included in _$MyBloc
@MonoBloc()
class MyBloc extends _$MyBloc<MyState> {
MyBloc() : super(initialState);
@event
MyState _onEvent() => newState;
}
What's exported:
mono_blocexports:package:bloc/bloc.dart+@protected/@immutablefrommetamono_bloc_flutterexports:package:flutter_bloc/flutter_bloc.dart+ all ofmono_blocmono_bloc_hooksexports:useMonoBlocActionListener(useshooked_blocinternally foruseBloc<T>())
Common Errors:
- Missing the MonoBloc import - Required for annotations like
@MonoBloc()and@event - Missing
partdirective - Required to include the generated code - Importing
blocorflutter_blocdirectly - Not needed, already exported
Build errors #
If you see errors about missing generated files:
- Ensure you have the
partdirective:part 'my_bloc.g.dart'; - Check that all required imports are present (see above)
- Run the generator:
dart run build_runner build -d - Check that method names are valid and don't conflict
Coding agents instructions #
Use this guide when working with MonoBloc code generation. Add this to your AI coding agent instructions or context files:
# MonoBloc State Management
## Core Annotations
- @MonoBloc() - Marks a class for code generation. Creates base class _$YourBloc with event handlers
- @onInit - Marks a private method to run automatically when bloc is created (e.g., `void _onInit()`)
- @event - Marks a private method as an event handler. Generates public method without underscore/prefix
- @MonoActions() - Marks a mixin containing action side-effect methods (navigation, dialogs)
- @onError - Global error handler for all events. Specific handlers: _onError{EventName}(error, stackTrace)
## Event Return Types
Methods annotated with @event can return:
- State - Direct synchronous state update
- Future<State> - Async operation returning new state
- Stream<State> - Multiple state emissions over time (use async*)
- void with _Emitter emit - Manual control with emit(state) calls
- Future<void> with _Emitter emit - Async manual control
Example:
```dart
class MyBloc extends _$MyBloc<int> {
@event
int _onIncrement() => state + 1;
@event
Future<TodoState> _onLoadTodos() async {
final todos = await repository.fetchTodos();
return TodoState(todos: todos);
}
@event
Stream<SearchState> _onSearch(String query) async* {
yield state.copyWith(isSearching: true);
final results = await repository.search(query);
yield state.copyWith(isSearching: false, results: results);
}
// Stream with progressive loading from multiple sources
@event
Stream<DataState> _onLoadFromSources() async* {
yield state.copyWith(isLoading: true);
final allItems = <Item>[];
for (final source in sources) {
final items = await source.fetch();
allItems.addAll(items);
yield state.copyWith(isLoading: false, items: allItems);
}
}
}
```
Calling events (generated methods remove underscore and prefix):
```dart
void main() {
final bloc = CounterBloc();
bloc.increment(); // Calls _onIncrement
bloc.loadTodos(); // Calls _onLoadTodos
}
```
## Actions Pattern
For side effects (navigation, dialogs, notifications), define actions in a private mixin with @MonoActions():
```dart
// 1. Define actions in a private mixin with @MonoActions()
@MonoActions()
mixin _MyBlocActions {
void navigateToCheckout();
void showNotification(String message);
}
// 2. Bloc extends generated base class (actions mixin included automatically)
@MonoBloc()
class MyBloc extends _$MyBloc<MyState> {
MyBloc() : super(MyState());
@event
Future<MyState> _onSubmit() async {
// Call actions during event processing
navigateToCheckout();
showNotification('Success!');
return state.copyWith(submitted: true);
}
}
// In Flutter widget - use MonoBlocActionListener<YourBloc>
void build(BuildContext context) {
return MonoBlocActionListener<MyBloc>(
actions: MyBlocActions.when(
navigateToCheckout: (context) => Navigator.pushNamed(context, '/checkout'),
showNotification: (context, msg) => ScaffoldMessenger.of(context).showSnackBar(...),
),
child: Widget(),
)
}
```
## Shared Action Mixins
Share common actions across multiple blocs by having private action mixins `implement` a public base mixin (e.g., `mixin _OrderBlocActions implements ErrorHandlerActions`). The generated base class automatically includes the actions mixin.
## Async State Management
Use @AsyncMonoBloc() for automatic loading/error states:
```dart
@AsyncMonoBloc()
class MyBloc extends _$MyBloc<List<Item>> {
MyBloc() : super(const MonoAsyncValue.withData([]));
// Future<T> - Automatically emits loading, then data or error
@event
Future<List<Item>> _onLoad() async {
return await repository.fetchItems();
}
// Stream<_State> - Full control with loading/data/error yields
@restartableEvent
Stream<_State> _onSearch(String query) async* {
yield loading(); // Keeps current data, sets isLoading=true
try {
final items = await repository.search(query);
yield withData(items);
} catch (e, stack) {
yield withError(e, stack, state.dataOrNull);
}
}
// _Emitter pattern - Fine-grained control
@event
Future<void> _onRefresh(_Emitter emit) async {
emit.loadingClearData(); // Clears data, shows spinner
try {
final items = await repository.fetchItems();
emit(items);
} catch (e, stack) {
emit.error(e, stack);
}
}
}
```
// Helpers: loading(), loadingClearData(), withData(T), withError(e, stack, [data])
// State accessors: state.isLoading, state.hasError, state.dataOrNull, state.data
## @onInit - Initialization
Run code automatically when bloc is created. Init methods should return `void` and dispatch events:
```dart
@MonoBloc()
class MyBloc extends _$MyBloc<MyState> {
MyBloc() : super(MyState.initial());
@onInit
void _onInit() {
loadItems(); // Dispatch event to load data
}
@event
Future<MyState> _onLoadItems() async {
final items = await repository.fetchAll();
return MyState(items: items);
}
}
```
## Flutter Hooks
Use the `mono_bloc_hooks` package for cleaner action handling in `HookWidget`:
```dart
import 'package:mono_bloc_hooks/mono_bloc_hooks.dart';
Widget build(BuildContext context) {
final bloc = useBloc<MyBloc>();
useMonoBlocActionListener(
bloc,
MyBlocActions.when(
myAction: (context, param) { /* Handle action */ },
),
);
return BlocBuilder<MyBloc, MyState>(...);
}
```
Contributing #
Contributions are welcome! Here's how you can help:
- Report bugs: Open an issue with reproduction steps
- Request features: Describe your use case and proposed solution
- Submit PRs: Add features, fix bugs, or improve documentation
- Write tests: Add test scenarios in the
mono_bloc_generator/test/directory
Before contributing, please:
- Check existing issues and PRs
- Follow the existing code style
- Add tests for new features
- Update documentation as needed
License #
MIT License - see LICENSE file for details.
Acknowledgments #
MonoBloc is built on top of the excellent bloc library by Felix Angelov. We extend its functionality with code generation to reduce boilerplate while keeping the powerful state management patterns that made bloc great.