Domain-first, framework-agnostic event system for Clean Architecture
jozz_events
is a lightweight, strongly-typed, and modular event bus designed for Clean Architecture. It enables scalable, maintainable, and decoupled communication across your applicationβs features and layers.
Ideal for Dart projects (including Flutter), this package brings clarity and safety to event-driven design with first-class support for modularity, testability, and lifecycle awareness β without any external dependencies.
π Why jozz_events
?
Feature | jozz_events |
event_bus |
Bloc-to-Bloc | Signals |
---|---|---|---|---|
Clean Arch Friendly | β | β | β (Tight) | β (UI-tied) |
Strong Typing | β | β | β | β |
Lifecycle Support | β | β | β | β οΈ via hooks |
Global Singleton | β Optional | β Always | β | β |
Dependency Injection | β | β | β | β οΈ |
Built for Clean Architecture
- Events are decoupled from emitters and listeners.
- Cross-layer support: UI, Application, Domain, and Infrastructure.
- Perfect for feature-based modular systems.
Strongly Typed & Predictable
- No string-based identifiers or dynamic types.
- Built entirely with Dart's type-safe system.
- Clear, explicit contracts via
JozzEvent
.
Framework-Agnostic
- No Flutter dependency.
- Works in Dart CLIs, server apps, and Flutter (mobile/web/desktop).
Lifecycle-Aware
- Optional lifecycle mixins for Bloc, Cubit, State, etc.
- Subscriptions are cleaned up automatically when components are disposed.
- Use Case Example
- Using the Global Singleton
- Why not just
event_bus
? - Features
- Clean Architecture Integration Tutorial
Installation & Getting Started
dependencies:
jozz_events: ^latest
Then import it:
import 'package:jozz_events/jozz_events.dart';
π§± Use Case Example
Two features need to communicate without knowing about each other:
TodoService
emits aTodoCreatedEvent
NotificationModule
listens and shows a message
Define the Event
class TodoCreatedEvent extends JozzEvent {
final String title;
const TodoCreatedEvent(this.title);
}
Emit the Event
eventBus.emit(TodoCreatedEvent('Buy milk'));
Listen to the Event
eventBus.on<TodoCreatedEvent>().listen((event) {
print('New todo: ${event.title}');
});
That's it β no tight coupling, no service locators, just clean, type-safe communication.
π For a full Clean Architecture integration, see the π¦ Clean Architecture Integration Tutorial.
π For a quick singleton usage approach, see the π Using the Global Singleton section.
π Clean Architecture Example Structure
features/
βββ todo/
β βββ domain/events/todo_created_event.dart
β βββ application/todo_service.dart
βββ notifications/
βββ presentation/notification_listener.dart
Both features are fully decoupled. Communication happens through JozzBus
.
Created by developers who love Clean Architecture and hate spaghetti.
π Using the Global Singleton
For small apps or rapid prototyping, use the global singleton:
import 'package:jozz_events/jozz_events.dart';
void main() {
// Emit
JozzEvents.bus.emit(TodoCreatedEvent(todoId: '123', title: 'Do dishes'));
// Listen
JozzEvents.bus.on<TodoCreatedEvent>().listen((event) {
print('Global handler: ${event.title}');
});
}
Singleton Access
JozzEvents.bus
: Access asJozzBus
interface (recommended for most usage).JozzEvents.service
: Access fullJozzBusService
for advanced control (e.g.dispose()
).
β οΈ Note: Use the singleton only if you're not using dependency injection. In large, scalable apps, prefer constructor injection and
JozzBusService
instances per module.
Why not just event_bus
?
While event_bus
is convenient, it comes with architectural compromises. Here's how jozz_events
stands apart:
Feature | jozz_events |
event_bus |
---|---|---|
Strong Typing | β Yes | β No |
Lifecycle Support | β Auto-dispose | β None |
Clean Arch Friendly | β Layered | β Tight Coupling |
Dependency Injection | β DI-first | β Singleton-only |
Global Singleton | β Optional | β Always |
Testability | β Mockable | β Difficult |
Even in non-Clean Architecture projects, strong typing, lifecycle handling, and testability make jozz_events
a safer, more robust foundation for event-driven code.
Features
- β Strongly typed events
- β No tight coupling between features
- β Clean architecture ready
- β Pure Dart β no Flutter dependency
- β Lifecycle support for widgets, blocs, cubits, etc.
- β Optional global singleton
- β Fluent event emission
- β Easy testing & mocking
π’ Coming Soon
- β Flutter integration helpers
- π§ͺ Testing utilities
- π§© Middleware & event interceptors (logging, side effects)
- π‘ Namespaced topics or channels for filtering
π¦ Clean Architecture Integration Tutorial
A step-by-step guide for integrating
jozz_events
into a modular, Clean Architecture Flutter project usingfreezed
,dartz
, andinjectable
What Is jozz_events
and Why Use It in Clean Architecture?
π§© Problem:
In a large Clean Architecture app, features should not know about each other. But sometimes, one feature needs to trigger behavior in another. For example:
- The
subscription
feature completes a purchase. - The
auth
feature needs to refresh custom claims.
β οΈ Naive Solutions:
- Inject
AuthBloc
intosubscription
(tight coupling β) - Use global state or service locators manually (messy β)
β
jozz_events
Solves This By:
- Allowing feature-level events to be emitted and listened to with no direct dependency
- Supporting strong typing, lifecycle-safe subscriptions, and Clean Architecture separation
1. Install the Package
dependencies:
jozz_events: ^<latest>
2. Setup Event Bus for Dependency Injection
In your core
folder:
// core/events/jozz_bus_di.dart
import 'package:injectable/injectable.dart';
import 'package:jozz_events/jozz_events.dart';
@module
abstract class JozzBusModule {
@lazySingleton
JozzBus get eventBus => JozzBusService();
}
Inject it where needed:
final JozzBus jozzBus;
@injectable
MyBloc(this.jozzBus);
3. Create a Domain Event
// features/subscription/domain/events/subscription_purchased.dart
import 'package:jozz_events/jozz_events.dart';
class SubscriptionPurchased extends JozzEvent {
const SubscriptionPurchased();
}
4. Emit the Event from a Use Case
class PurchasePremium {
final InAppPurchaseService _iap;
final JozzBus _jozzBus;
PurchasePremium(this._iap, this._jozzBus);
Future<Either<Failure, Unit>> call() async {
final result = await _iap.purchase();
if (result.isRight()) {
_jozzBus.emitEvent(const SubscriptionPurchased());
}
return result;
}
}
5. Listen to the Event in Another Feature
In your AuthCubit
, use the JozzLifecycleMixin
to auto-dispose:
class AuthCubit extends Cubit<AuthState> with JozzLifecycleMixin {
final JozzBus _jozzBus;
final RefreshUserClaims _refreshClaims;
final GetSignedInUser _getUser;
AuthCubit(this._jozzBus, this._refreshClaims, this._getUser) : super(...)
{
_jozzBus.autoListen<SubscriptionPurchased>(this, (_) async {
await _refreshClaims();
final user = await _getUser();
emit(AuthState.authenticated(user.getOrElse(() => throw UnexpectedError())));
});
}
@override
Future<void> close() {
disposeJozzSubscriptions();
return super.close();
}
}
π― Summary
- Use
jozz_events
to allow features to communicate via domain events without tight coupling - Events are emitted from use cases and listened to in Blocs, Cubits, or services
- Integration is clean, scalable, and testable
- Especially useful for cross-feature flows like:
purchase β claims refresh
,login β analytics
,delete β undo
π License MIT Β© Jozz
Libraries
- bus/jozz_bus
- bus/jozz_bus_service
- bus/jozz_bus_subscription
- events/jozz_event
- extensions/jozz_bus_extensions
- jozz_events
- The jozz_events library provides a domain-driven, event-based communication system.
- lifecycle/jozz_lifecycle_handler
- lifecycle/jozz_lifecycle_mixin
- singleton/jozz_events