bloc_signals 1.4.0
bloc_signals: ^1.4.0 copied to clipboard
Core pure Dart reactive state container bridging BLoC semantics with signals v7 primitives.
|
β‘ 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 11 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 (
value/stateValue): Access rawStateTypevalues directly viavalue(the preferred modern getter) orstateValue(fully supported for backward compatibility), for exampleemit(value + 1).stateexposesReadonlySignal<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();
}
Tip: Because state transitions propagate synchronously in frame 0, keep transitions atomic ($S_n \to S_{n+1}$). Avoid multiple synchronous
emit()calls along the same linear path, and name private helper methods that emit state explicitly (for example_pruneAndEmit()rather than_prune()).
3. Composable Mixins (Overcoming Single Inheritance) #
Use CubitSignalMixin or BlocSignalMixin to turn any class with an existing superclass (for example, ChangeNotifier, TextEditingController, AnimationController, or BaseRepository) into a first-class BlocSignalBase state container without occupying its single extends slot:
class UserProfileRepository extends BaseRepository
with CubitSignalMixin<UserProfileState> {
UserProfileRepository() {
initCubitSignal(initialState: const UserProfileInitial());
}
Future<void> fetchProfile(String id) async {
emit(const UserProfileLoading());
final user = await api.getUser(id);
emit(UserProfileLoaded(user));
}
}
4. 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(),
);
}
}
5. Custom Equality Comparators #
class UserBloc extends CubitSignal<UserModel> {
UserBloc(UserModel initial)
: super(
initialState: initial,
equals: (a, b) => a.id == b.id, // Custom property equality
);
}
6. 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();
7. 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.