bloc_signals

⚡ bloc_signals

"With the rigor of BLoC and the flex and speed of Signals"

A synchronous state management library bridging the Business Logic Component (BLoC) pattern with a reactive signals foundation (using Rody Davis's signals package v7).

This package provides core pure-Dart reactive state containers (BlocSignalBase, CubitSignal, BlocSignal), event concurrency transformers (Mutex, droppable, sequential, restartable), VM Service telemetry (DevToolsBlocSignalObserver, DevToolsService), and stream interop extensions.


🌐 Ecosystem Packages

The BlocSignal monorepo consists of 10 modular packages:

Package Version Description
bloc_signals pub Core pure Dart reactive state primitives bridging BLoC & Signals
bloc_signals_flutter pub Flutter UI bindings, providers, builders, listeners & selectors
bloc_signals_jaspr pub Jaspr web component integration and state binding for BlocSignal
bloc_signals_riverpod pub Bidirectional Riverpod 2/3 interop adapters & provider extensions
bloc_signals_hydrate pub Automated synchronous local state persistence & hydration
bloc_signals_replay pub Undo & redo state history tracking for CubitSignal and BlocSignal
bloc_signals_otel pub OpenTelemetry tracing and span generation for state transitions
bloc_signals_devtools pub Universal DevTools telemetry observer using dart:developer
bloc_signals_test pub Declarative unit testing utilities (blocSignalTest)
bloc_signals_lint pub Custom analyzer lint rules & automated IDE quick-fixes

📖 Background & Architecture References

bloc_signals bridges two foundational state management technologies:

Key Architectural Differences & Design Choices:

  • Synchronous State Propagation: State changes run synchronously when calling emit(newState) rather than asynchronously on microtask-queue Streams.
  • 🔑 Named Constructor Initial State (initialState:): Constructors require the named parameter initialState: (for example, : super(initialState: 0)), unlike Felix BLoC's positional : super(0).
  • 📊 Explicit State Value Access (stateValue): Use stateValue (or state.value) to read raw StateType values in methods or event handlers (for example, emit(stateValue + 1)), while state exposes ReadonlySignal<StateType> for reactive signal bindings.
  • 🔒 Streamless Concurrency: Support for Mutex, droppable(), sequential(), and restartable() event transformers using pure Dart higher-order functions with zero stream memory allocations.

⚡ Key Features

  • 🚀 Synchronous Propagation: emit() updates state immediately in the exact same frame without microtask delay.
  • 🎯 Automatic De-duplication: Identical states (== or custom equality) are automatically de-duplicated to prevent unnecessary downstream recalculations.
  • 🔒 Streamless Concurrency: Support for Mutex, droppable(), sequential(), and restartable() event transformers without stream overhead.
  • 🛠️ DevTools & Telemetry: Built-in VM Service RPC extensions (DevToolsService) and standard dart:developer event posting (DevToolsBlocSignalObserver).

🚀 Getting Started

Add bloc_signals to your pubspec.yaml:

dependencies:
  bloc_signals: ^1.0.0

💡 Quick Examples

1. CubitSignal (Simple State Management)

import 'package:bloc_signals/bloc_signals.dart';

class CounterCubit extends CubitSignal<int> {
  CounterCubit() : super(initialState: 0);

  void increment() => emit(stateValue + 1);
  void decrement() => emit(stateValue - 1);
}

void main() {
  final counter = CounterCubit();
  print(counter.stateValue); // 0
  counter.increment();
  print(counter.stateValue); // 1
  counter.close();
}

2. BlocSignal (Event-Driven State Management)

import 'package:bloc_signals/bloc_signals.dart';

sealed class CounterEvent {}
final class IncrementEvent extends CounterEvent {}
final class DecrementEvent extends CounterEvent {}

class CounterBloc extends BlocSignal<CounterEvent, int> {
  CounterBloc() : super(initialState: 0) {
    on<IncrementEvent>((event, emit) => emit(stateValue + 1));
    on<DecrementEvent>((event, emit) => emit(stateValue - 1));
  }
}

void main() {
  final bloc = CounterBloc();
  bloc.add(IncrementEvent()); // Synchronously transitions state to 1
  print(bloc.stateValue); // 1
  bloc.close();
}

3. Event Concurrency Transformers (droppable, sequential, restartable)

class AsyncDataBloc extends BlocSignal<DataEvent, DataState> {
  AsyncDataBloc(Repository repo) : super(initialState: DataInitial()) {
    // Drop incoming FetchData events while current handler is active
    on<FetchData>(
      (event, emit) async {
        final data = await repo.load();
        emit(DataLoaded(data));
      },
      transformer: droppable(),
    );
  }
}

4. Custom Equality Comparators

class UserBloc extends CubitSignal<UserModel> {
  UserBloc(UserModel initial)
      : super(
          initialState: initial,
          equals: (a, b) => a.id == b.id, // Custom property equality
        );
}

5. Stream Interop Extensions

// Convert any BlocSignal into a Dart Stream
final Stream<int> stream = counterBloc.toStream();

// Convert a Stream into a BlocSignalBase<T> holding raw domain values
final streamBloc = stream.toBlocSignal(initialState: 0);

// Convert a Stream into a BlocSignalBase<AsyncState<T>> tracking loading/data/error
final asyncStreamBloc = stream.toAsyncBlocSignal();

6. Signal & Future Interop Extensions

// Convert any ReadonlySignal (Signal, Computed, AsyncSignal) to BlocSignalBase<T>
final countSignal = signal(0);
final countBloc = countSignal.toBlocSignal();

// Convert a Future<T> into a BlocSignalBase<T> with a required initialState
final userBloc = api.fetchUser(id).toBlocSignal(initialState: User.anonymous());

// Convert a Future<T> into a BlocSignalBase<AsyncState<T>> tracking loading/data/error
final asyncUserBloc = api.fetchUser(id).toAsyncBlocSignal();

🏷️ Debug Names, Signal Options & Custom Equality

All BlocSignalBase containers (CubitSignal, BlocSignal), side-effect handlers (createEffect), and Flutter selectors (BlocSignalSelector) accept explicit options configuration (SignalOptions, EffectOptions, ComputedOptions) and generate descriptive automatic debug names for DevTools inspection.

1. Automatic & Custom Debug Names

By default, state signals and internal effects are assigned rich diagnostic names in VM Service / DevTools telemetry:

  • State Signal: '$runtimeType.state' (for example 'CounterCubit.state')
  • Lifecycle Effect: '$runtimeType.lifecycleEffect'
  • Custom Effects: '$runtimeType.effect#1', '$runtimeType.effect#2'

You can customize debug names using the options: parameter:

final cubit = CounterCubit(
  options: SignalOptions<int>(name: 'CustomCounterCubit.state'),
);

2. Custom Equality & Identity Comparison (identical)

By default, state updates use standard value equality (previous == current). You can customize state de-duplication strategy using equals: or options:.

💡 FAQ: How do I force Reference Identity Equality (identical)?

To ensure every emit() call triggers a state update regardless of == value equality, pass Dart's built-in identical top-level function tear-off:

// Option A: Passing `identical` tear-off to the constructor
class ForceRebuildCubit extends CubitSignal<StateModel> {
  ForceRebuildCubit(StateModel initial)
      : super(initialState: initial, equals: identical);
}

// Option B: Using SignalOptions.identity()
class IdentityBloc extends CubitSignal<StateModel> {
  IdentityBloc(StateModel initial)
      : super(
          initialState: initial,
          options: SignalOptions(equality: SignalEquality.identity()),
        );
}

⚖️ Equality Evaluation Precedence Order

  1. options.equality (highest priority if specified in SignalOptions)
  2. equals constructor parameter or @override bool equals(...) method
  3. Default value equality (previous == current)

🔍 DevTools & Telemetry Setup

Enable global DevTools telemetry in main.dart:

void main() {
  // Enables VM Service RPC extensions & developer.postEvent telemetry
  BlocSignalObserver.observer = DevToolsBlocSignalObserver();

  runApp(const MyApp());
}

🤖 AI Coding Assistant Skill & Guides

This package is supported by official pre-packaged AI Coding Skills and architectural documentation guides representing best practices, lifecycle contracts, and usage patterns for BlocSignal:

  • 🔄 Migration Guide: Transitioning from classic package:bloc / package:flutter_bloc to BlocSignal.
  • 🌁 Universal Interoperability Guide: Bridging state containers across BLoC, Riverpod, Provider, and Listenable primitives.
  • 📦 AI Skill Bundle: Load the pre-packaged bloc-signals skill bundle for AI coding assistants (such as Claude Code, Antigravity, Gemini, Cursor, or Codex) to guide code generation and analysis.

📜 Credits & Acknowledgements

Inspired by bloc by Felix Angelov and signals by Rody Davis.

Libraries

bloc_signals
A reactive state management library bridging the BLoC pattern with Rody Davis's signals (v7) primitives.