img

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.


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 a TodoCreatedEvent
  • 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 as JozzBus interface (recommended for most usage).
  • JozzEvents.service: Access full JozzBusService 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 using freezed, dartz, and injectable

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 into subscription (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