mono_bloc 1.0.1 copy "mono_bloc: ^1.0.1" to clipboard
mono_bloc: ^1.0.1 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

MonoBloc #

pub version pub version pub version pub version Tests

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 (@restartableEvent, @droppableEvent, etc.)
  • đŸŽ¯ Action Side-Effects - Handle navigation, dialogs, and snackbars with @action methods
  • â™ģī¸ 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 #

1. Install #

Add to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  mono_bloc: ^1.0.0

dev_dependencies:
  mono_bloc_generator: ^1.0.0
  build_runner: ^2.10.0

2. Create your bloc #

import 'package:bloc/bloc.dart';
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. Sequential mode - all events (button clicks, user input, API calls) processed in order, waiting for each to finish
@MonoBloc(sequential: true)
abstract class TodoBloc extends _$TodoBloc<TodoState> {
  TodoBloc._() : super(TodoState());
  
  factory TodoBloc() = _$TodoBlocImpl;  // Required for abstract class with actions
  
  // 2. Actions - side effects (navigation, dialogs, notifications)
  @action
  void showSuccess(String message);
  
  @action
  void navigateToDetail(String todoId);
  
  // 3. Events with different return types
  @event
  Future<TodoState> _onLoadTodos() async {
    // Load todos from repository
  }
  
  @restartableEvent  // 4. Concurrency control - cancels previous search
  Stream<TodoState> _onSearch(String query) async* {
    // Search with debounce, auto-cancels previous search
  }
  
  @droppableEvent  // 5. Prevents duplicate submissions
  Future<TodoState> _onAddTodo(String title) async {
    // Add todo
    showSuccess('Todo added!');  // Trigger action
  }
  
  @event
  Future<TodoState> _onToggleTodo(String id) async {
    // Toggle todo completion
  }
  
  @event
  Future<TodoState> _onDeleteTodo(String id) async {
    // Delete todo
    navigateToDetail('list');  // Trigger navigation action
  }
  
  // 6. Error handling - centralized for all events
  @onError
  TodoState _onError(Object error, StackTrace stackTrace) {
    // Handle all errors in one place
  }
  
  // 7. Initialization - runs automatically on creation
  @init
  void _onInit() {
    loadTodos();  // Dispatch event to load initial data
  }
}

// 8. Flutter integration - handle actions with MonoBlocActionListener
class TodoPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return TodoBlocActionListener(
      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
        },
      ),
    );
  }
}

// 9. Flutter Hooks support - cleaner action handling
class TodoPageWithHooks extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final bloc = context.read<TodoBloc>();
    
    useMonoBlocActionListener(
      bloc,
      TodoBlocActions.when(
        showSuccess: (context, message) { /* Show snackbar */ },
        navigateToDetail: (context, todoId) { /* Navigate */ },
      ),
    );
    
    return BlocBuilder<TodoBloc, TodoState>(
      builder: (context, state) {
        // Build UI
      },
    );
  }
}

What this example shows:

  1. Sequential Mode - @MonoBloc(sequential: true) processes all events (button clicks, user input, API calls) in order, waiting for each to finish
  2. Actions - @action for side effects (navigation, notifications)
  3. Multiple Event Types - Future<State>, Stream<State> return types
  4. Concurrency Control - @restartableEvent, @droppableEvent annotations
  5. Error Handling - @onError for centralized error management
  6. Initialization - @init runs automatically when bloc is created
  7. Flutter Integration - MonoBlocActionListener widget for handling actions
  8. Hooks Support - useMonoBlocActionListener for cleaner code with flutter_hooks

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.

Why actions? #

  • Separation of Concerns: Keep side effects separate from state
  • Type Safety: Full type checking for all action parameters
  • Pattern Matching: Use when() 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 using the @action annotation on abstract methods:

import 'package:bloc/bloc.dart';
import 'package:mono_bloc/mono_bloc.dart';

part 'checkout_bloc.g.dart';

enum NotificationType { success, error, warning }

@MonoBloc()
abstract class CheckoutBloc extends _$CheckoutBloc<CheckoutState> {
  CheckoutBloc._() : super(CheckoutState());
  
  // Factory constructor required for abstract classes with actions
  factory CheckoutBloc() = _$CheckoutBlocImpl;
  
  // Actions - side effects that don't affect state
  @action
  void navigateToConfirmation(String orderId);
  
  @action
  void showNotification({
    required String message,
    required NotificationType type,
  });
  
  @action
  void trackAnalyticsEvent(String eventName, Map<String, dynamic> properties);
  
  // 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 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 CheckoutBlocActionListener(
      // 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);
            },
          );
        },
      ),
    );
  }
}

Manual subscription (alternative) #

You can also manually subscribe to the actions stream in initState:

class CheckoutPage extends StatefulWidget {
  @override
  State<CheckoutPage> createState() => _CheckoutPageState();
}

class _CheckoutPageState extends State<CheckoutPage> {
  late final StreamSubscription _actionsSubscription;
  
  @override
  void initState() {
    super.initState();
    
    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 = context.read<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);
      },
    );
  }
}

Requirements for abstract classes #

If your bloc is abstract and has actions, you must provide a factory constructor:

@MonoBloc()
abstract class MyBloc extends _$MyBloc<MyState> {
  MyBloc._() : super(initialState);
  
  // Required: Factory redirecting to generated implementation
  factory MyBloc() = _$MyBlocImpl;
  
  @action
  void myAction();
}

Why? Actions need to be implemented in a concrete class. The generator creates _$MyBlocImpl which implements the action methods by emitting to the action stream.

Validation: The generator validates that:

  1. Factory constructor exists
  2. Factory redirects to _$[BlocName]Impl
  3. Bloc class is abstract (can't implement abstract action methods)

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:

  1. @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
  2. @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 time
  • MonoConcurrency.restartable - Cancel ongoing event when new one arrives
  • MonoConcurrency.droppable - Ignore new events while one is processing

Transformer Types #

Sequential

Process events one at a time, waiting for each to complete:

@sequentialEvent
Stream<State> _onSearch(String query) async* {
  // Each search waits for the previous search to complete
  // But other events (like onLoadData) can run in parallel
  final results = await api.search(query);
  yield SearchResults(results);
}

Concurrent

Process all events simultaneously:

@concurrentEvent
Future<State> _onRefresh() async {
  // Multiple refreshes run in parallel
  final data = await api.fetch();
  return LoadedState(data);
}

Restartable

Cancel ongoing event when new one arrives:

@restartableEvent
Stream<State> _onSearch(String query) async* {
  // Previous search is cancelled when user types again
  yield SearchingState();
  final results = await api.search(query);
  yield SearchResults(results);
}

Droppable

Ignore new events while one is processing:

@droppableEvent
Future<void> _onSubmit(_Emitter emit) async {
  // Rapid clicks are ignored until submit completes
  emit(SubmittingState());
  await api.submit(data);
  emit(SubmittedState());
}

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 (@sequentialEvent for one event, @restartableEvent for 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 must include these imports:

import 'package:mono_bloc/mono_bloc.dart'; // or package:mono_bloc_flutter/mono_bloc_flutter.dart

part 'my_bloc.g.dart';

@MonoBloc()
class MyBloc extends _$MyBloc<MyState> {
  MyBloc() : super(initialState);
  
  @event
  MyState _onEvent() => newState;
}

Note: The mono_bloc and mono_bloc_flutter packages export package:bloc/bloc.dart and @protected from package:meta/meta.dart, so you don't need to import them separately.

Common Errors:

  • Missing package:mono_bloc/mono_bloc.dart (or mono_bloc_flutter) - Required for annotations like @MonoBloc() and @event
  • Missing part directive - Required to include the generated code

Build errors #

If you see errors about missing generated files:

  1. Ensure you have the part directive: part 'my_bloc.g.dart';
  2. Check that all required imports are present (see above)
  3. Run the generator: dart run build_runner build -d
  4. 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 Code Generation Guide

## Core Annotations

- @MonoBloc() - Marks a class for code generation. Creates base class _$YourBloc with event handlers
- @MonoBloc(sequential: true) - All events process sequentially, waiting for each to complete before starting next
- @MonoBloc(concurrency: MonoConcurrency.restartable) - Set default concurrency mode for all events
- @event - Marks a private method as an event handler. Generates public method without underscore/prefix
- @action - Defines side-effect methods (navigation, dialogs). Requires abstract class with factory constructor
- @onError - Global error handler for all events. Specific handlers: _onError{EventName}(error, stackTrace)
- @init - Runs automatically when bloc is created. Use for initial data loading

## 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:

```
@event
int _onIncrement() => state + 1;

@event
Future<TodoState> _onLoadTodos() async {
  final todos = await repository.fetchTodos();
  return TodoState(todos: todos);
}

Calling events (generated methods remove underscore and prefix):

final bloc = CounterBloc();
bloc.increment();  // Calls _onIncrement
bloc.loadTodos();  // Calls _onLoadTodos
```

## Actions Pattern

For side effects (navigation, dialogs, notifications):

```
@MonoBloc()
abstract class MyBloc extends _$MyBloc<MyState> {
  MyBloc._() : super(MyState());
  factory MyBloc() = _$MyBlocImpl;  // Required for actions
  
  @action
  void navigateToCheckout();
  
  @action
  void showNotification(String message);
}

// In Flutter widget - use generated MyBlocActionListener typedef
MyBlocActionListener(
  actions: MyBlocActions.when(
    navigateToCheckout: (context) => Navigator.pushNamed(context, '/checkout'),
    showNotification: (context, msg) => ScaffoldMessenger.of(context).showSnackBar(...),
  ),
  child: ...,
)
```

## Async State Management

Use @AsyncMonoBloc() for automatic loading/error states:

```
@AsyncMonoBloc()
class MyBloc extends _$MyBloc<List<Item>> {
  MyBloc() : super(const MonoAsyncValue.withData([]));
  
  @event
  Future<List<Item>> _onLoad() async {
    // Automatically wraps in loading state, then success/error
    return await repository.fetchItems();
  }
}
```

// State accessors: state.isLoading, state.hasError, state.dataOrNull

## @init - Initialization

Run code automatically when bloc is created. Init methods should return `void` and dispatch events:

```
@MonoBloc()
class MyBloc extends _$MyBloc<MyState> {
  MyBloc() : super(MyState.initial());

  @init
  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`:

```
import 'package:mono_bloc_hooks/mono_bloc_hooks.dart';

final bloc = useBloc<MyBloc>();

useMonoBlocActionListener(
  bloc,
  MyBlocActions.when(
    myAction: (context, param) { /* Handle action */ },
  ),
);
``` 

## Required Imports

Every MonoBloc file needs:

```
import 'package:mono_bloc/mono_bloc.dart';
part 'my_bloc.g.dart';
```

For Flutter projects with actions, use mono_bloc_flutter instead:

```
import 'package:mono_bloc_flutter/mono_bloc_flutter.dart';
part 'my_bloc.g.dart';
```

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:

  1. Check existing issues and PRs
  2. Follow the existing code style
  3. Add tests for new features
  4. 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.

Resources #

1
likes
0
points
81
downloads

Publisher

verified publisherwestito.dev

Weekly Downloads

Simplify Flutter Bloc with code generation. Define events as methods, get automatic concurrency control, actions for side effects, and async state management.

Repository (GitHub)
View/report issues

Topics

#bloc #code-generation #state-management #dart

License

unknown (license)

Dependencies

bloc, bloc_concurrency, meta, stack_trace

More

Packages that depend on mono_bloc