By Randal L. Schwartz, and a few million TPU cycles
Motto: "With the rigor of Bloc and the flex and speed of Signal"
Why You Don't Have to Tear Up Your Codebase to Enjoy the Speed of Synchronous Signals
If you have followed my talks, articles, or comments in the Flutter community over the years, you know I have been a strong advocate for Riverpod. Riverpod solved many of the fundamental global-state scoping issues inherent in classic InheritedWidget patterns, providing compile-safe dependency injection and clean state isolation.
However, as the Flutter ecosystem evolved toward Riverpod 3, I grew increasingly wary of the direction being pushed: a heavy reliance on mandatory code generation (@riverpod annotations, build_runner, macros). Code generation introduces build-step friction, bloats compile times, and makes debugging generated syntax opaque.
On the other side of the tracks sat BLoC. While I appreciated BLoC's structured, predictable event-to-state machine pattern (on<Event>), I was never a big fan of classic BLoC's reliance on underlying Dart Streams. Streams operate asynchronously via microtask queuesβintroducing subtle frame-rendering latencyβand require extensive stream-transformer ceremony for simple state updates.
Then came Signals (specifically Rody Davis's signals package). Signals brought raw speed, zero microtask overhead, fine-grained composable reactivity, and pure Dart portability.
That realization birthed BlocSignal: combining BLoC's disciplined, enterprise event-state architecture with Signals' synchronous reactivity.
And more importantly, it solved the single biggest pain point in Flutter development: the migration trap.
π The Core Metaphor: "Switching Tracks in BlocSignal"
In Flutter development, choosing a state management tool often feels like choosing a railroad company. If your team built an application on package:provider or flutter_bloc and wants to adopt Riverpod or Signals, traditional wisdom dictates a nightmare: tearing up all your existing tracks and rebuilding your rail lines from scratch.
BlocSignal rejects this all-or-nothing trap. Named after the classic railway block signalβthe system that manages train traffic safely to prevent collisionsβBlocSignal acts as a central railway switchyard.
Classic BLoC Line (Streams) ββββββββββ
β
Riverpod Express (Providers) βββββββββΌβββΊ [ BlocSignal Central Switchyard ] ββββΊ Pure Reactive Signals
β
Provider Local (Listenables) βββββββββ
You do not need to abandon your existing rail network or halt traffic. Whether your feature runs on the Riverpod Express, BLoC Stream Line, or Flutter's native ChangeNotifier local track, you can switch tracks synchronously at the BlocSignal switchyard and keep your train moving smoothly!
β‘ The Synchronous Signal Core: Green Lights All the Way
In classic flutter_bloc, state updates flow through StreamController microtask queues:
// Classic BLoC: Asynchronous microtask queue scheduling
emit(newState); // State arrives on the next microtask tick
In BlocSignal, bloc.state is natively a ReadonlySignal<State>. State propagation is 100% synchronous:
// BlocSignal: Synchronous signal graph propagation
emit(newState); // Downstream computeds & UI elements update IN THE EXACT SAME FRAME
Because ReadonlySignal is a lightweight, primitive reactive node, external state objects (Stream, ProviderListenable, ValueListenable) can be adapted into a BlocSignal (or vice-versa) with zero-cost bridge wrappers.
Furthermore, all BlocSignal interop bridges support custom equality comparators (equals: (prev, next) => ...), preserving strict state de-duplication rules across track switches.
Let's walk through the three main rail lines.
π Track 1: BLoC, Redux & Stream Interop (package:bloc_signals)
If you have legacy BLoCs, Redux stores, or RxDart observables, you can bridge them onto the BlocSignal switchyard without breaking existing stream subscribers.
Ingesting Streams & Redux
Use StreamBlocSignal to turn any Dart Stream into a BlocSignalBase container:
import 'package:bloc_signals/bloc_signals.dart';
// 1. Adapt a standard Dart Stream / RxDart Observable
final streamCubit = StreamBlocSignal<int>(
stream: myLegacyStream,
initialState: 0,
);
// 2. Adapt a Redux Store
final reduxCubit = StreamBlocSignal<AppState>(
stream: reduxStore.onChange,
initialState: reduxStore.state,
);
Exporting BlocSignal to Stream
Need to feed a legacy StreamBuilder or BlocBuilder? Expose any BlocSignal as a standard Dart stream:
final Stream<MyState> stream = myBlocSignal.toStream();
π Track 2: Riverpod Interop (package:bloc_signals_riverpod)
For developers using Riverpod for dependency injection who want to avoid code generation and microtask delays, package:bloc_signals_riverpod provides zero-boilerplate bidirectional adapters.
Ingesting Riverpod Providers (With Automatic Teardown!)
Convert any Riverpod ProviderListenable into a BlocSignal:
import 'package:bloc_signals_riverpod/bloc_signals_riverpod.dart';
// Ingest a Riverpod provider into BlocSignal
// Automatically binds ref.onDispose(bloc.close) under the hood!
final userBloc = userProvider.toBlocSignal(ref);
π‘ No Retain Count Leaks: Passing
ref(orWidgetRef/ProviderContainer) automatically registersref.onDisposeteardown hooks. When the parent Riverpod container or widget unmounts, the underlyingBlocSignalis closed automatically.
Exporting BlocSignal to Riverpod
Turn any BlocSignal into a standard Riverpod NotifierProvider:
final NotifierProvider<Notifier<CounterState>, CounterState> riverpodProvider =
myCounterBloc.toProvider();
Riverpod 3 AsyncValue β Signals AsyncState Bridge
Riverpod 3 introduced sealed AsyncValue classes. bloc_signals_riverpod includes seamless conversions:
final AsyncState<UserData> signalsState = riverpodAsyncValue.toAsyncState();
final AsyncValue<UserData> riverpodValue = signalsAsyncState.toAsyncValue();
π Track 3: Flutter Listenable & package:provider (package:bloc_signals_flutter)
Flutter's native ChangeNotifier, ValueNotifier, and package:provider remain heavily used across thousands of codebases.
Ingesting Listenable and ValueNotifier
Convert any Flutter Listenable directly into a BlocSignalBase:
import 'package:bloc_signals_flutter/bloc_signals_flutter.dart';
// ValueNotifier -> BlocSignal
final countCubit = myValueNotifier.toBlocSignal();
// ChangeNotifier -> BlocSignal
final authCubit = myChangeNotifier.toBlocSignal(
readState: () => myChangeNotifier.currentUser,
);
Exporting BlocSignal to ValueListenable
Expose any BlocSignal as a standard Flutter ValueListenable<T>:
final ValueListenable<int> listenable = myBlocSignal.toValueListenable();
// Consume via package:provider's ValueListenableProvider
ValueListenableProvider<int>.value(
value: listenable,
child: Consumer<int>(
builder: (context, count, _) => Text('Count: $count'),
),
);
π Multi-Track Routing: Synchronizing Across Ecosystems
Because every adapter maps cleanly to BlocSignalBase, you can route state across multiple track lines in a single, synchronous pipeline!
Example: Provider β BlocSignal β Riverpod
Imagine a legacy ChangeNotifier form field that needs to be consumed by a new Riverpod feature:
// 1. Ingest Flutter ChangeNotifier into a BlocSignal Cubit
final formCubit = myChangeNotifier.toBlocSignal(
readState: () => myChangeNotifier.formValue,
);
// 2. Export the Cubit directly as a Riverpod NotifierProvider
final riverpodFormContainer = formCubit.toProvider();
State changes in myChangeNotifier propagate synchronously through formCubit into Riverpod in the exact same frame, with zero frame lag or microtask queuing!
π€ AI Agent Skills: Your Automated Signal Operator
One of the most exciting aspects of modern Dart and Flutter development is AI-assisted coding. However, generic AI assistants often generate outdated, deprecated, or incorrect state management patterns.
To solve this, every package in the BlocSignal ecosystem ships with dedicated AI Agent Skills (plugins/bloc-signals/skills/bloc-signals/).
plugins/bloc-signals/skills/bloc-signals/
βββ SKILL.md # Master agent skill entrypoint
βββ architecture.md # Core synchronous signal graph invariants
βββ event_handlers.md # Concurrency transformers (droppable, sequential)
βββ testing.md # Declarative testing with bloc_signals_test
βββ interoperability.md # Ecosystem bridge matrix & migration recipes
When you use AI coding assistants equipped with these skills (such as Gemini, Antigravity, Claude, Cursor, or Context7), your AI agent acts like an expert signal operator:
- Resolves accurate, up-to-date
BlocSignalAPI contracts without hallucination. - Applies correct teardown rules (
ref.onDispose,unawaited(super.onEvent(event))). - Generates declarative
blocSignalTestsuites with 100% line coverage. - Provides instant, step-by-step migration recipes from legacy BLoC or Riverpod codebases.
π― Summary & The Interoperability Matrix
State management should serve your projectβnot lock you into a rigid framework. With BlocSignal, you retain the architectural discipline of BLoC state machines, gain the raw speed of synchronous Signals, and bridge your existing Riverpod or Provider codebases effortlessly.
π Cheat-Sheet Interoperability Matrix
| Source Paradigm | From Source β BlocSignal
|
From BlocSignal β Target |
Target Package |
|---|---|---|---|
| BLoC / Stream | StreamBlocSignal(stream) |
blocSignal.toStream() |
bloc_signals |
| Redux | StreamBlocSignal(store.onChange) |
blocSignal.toStream() |
bloc_signals |
| Riverpod | provider.toBlocSignal(ref) |
blocSignal.toProvider() |
bloc_signals_riverpod |
| Provider (Listenable) | listenable.toBlocSignal() |
blocSignal.toValueListenable() |
bloc_signals_flutter |
| Riverpod Async | asyncValue.toAsyncState() |
asyncState.toAsyncValue() |
bloc_signals_riverpod |
π Get Started Today
Check out the official open-source packages and AI skills on GitHub:
- π¦ GitHub Repository: github.com/RandalSchwartz/BlocSignal
- π οΈ Core Package:
pub.dev/packages/bloc_signals - π± Flutter UI Package:
pub.dev/packages/bloc_signals_flutter - π Riverpod Interop:
pub.dev/packages/bloc_signals_riverpod - π§ͺ Testing Package:
pub.dev/packages/bloc_signals_test - π€ AI Agent Skill Bundle:
plugins/bloc-signals/skills/bloc-signals
Stop tearing up your tracks. Step up to the switchyard, switch tracks with BlocSignal, and keep your Flutter trains running on time! π
Top comments (0)