|
⚡ 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 |
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:
📖 Background & Architecture References
bloc_signals bridges two foundational state management technologies:
- BLoC Architecture (bloclibrary.dev): Business Logic Component event-driven state machines, state decoupling, and global lifecycle observation (
BlocSignalObserver). - Signals Primitives (signals.dart): Rody Davis's signals v7 reactive primitives providing fine-grained dependency tracking and zero-latency value holding.
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 parameterinitialState:(for example,: super(initialState: 0)), unlike Felix BLoC's positional: super(0). - 📊 Explicit State Value Access (
stateValue): UsestateValue(orstate.value) to read rawStateTypevalues in methods or event handlers (for example,emit(stateValue + 1)), whilestateexposesReadonlySignal<StateType>for reactive signal bindings. - 🔒 Streamless Concurrency: Support for
Mutex,droppable(),sequential(), andrestartable()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(), andrestartable()event transformers without stream overhead. - 🛠️ DevTools & Telemetry: Built-in VM Service RPC extensions (
DevToolsService) and standarddart:developerevent 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
options.equality(highest priority if specified inSignalOptions)equalsconstructor parameter or@override bool equals(...)method- 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_bloctoBlocSignal. - 🌁 Universal Interoperability Guide: Bridging state containers across BLoC, Riverpod, Provider, and Listenable primitives.
- 📦 AI Skill Bundle: Load the pre-packaged
bloc-signalsskill 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.