# Switching Tracks in BlocSignal: The Universal State Switchyard for BLoC, Riverpod, and Provider

> Source: <https://dev.to/gde/switching-tracks-in-blocsignal-the-universal-state-switchyard-for-bloc-riverpod-and-provider-929>
> Published: 2026-07-27 15:37:53+00:00

*By Randal L. Schwartz, and a few million TPU cycles*

*Motto: "With the rigor of Bloc and the flex and speed of Signal"*

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**.

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!

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

```
// 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.

`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.

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,
);
```

`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();
```

`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.

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`

(or`WidgetRef`

/`ProviderContainer`

) automatically registers`ref.onDispose`

teardown hooks. When the parent Riverpod container or widget unmounts, the underlying`BlocSignal`

is closed automatically.

`BlocSignal`

to Riverpod
Turn any `BlocSignal`

into a standard Riverpod `NotifierProvider`

:

```
final NotifierProvider<Notifier<CounterState>, CounterState> riverpodProvider = 
    myCounterBloc.toProvider();
```

`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();
```

`Listenable`

& `package:provider`

(`package:bloc_signals_flutter`

)
Flutter's native `ChangeNotifier`

, `ValueNotifier`

, and `package:provider`

remain heavily used across thousands of codebases.

`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,
);
```

`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'),
  ),
);
```

Because every adapter maps cleanly to `BlocSignalBase`

, you can route state across multiple track lines in a single, synchronous pipeline!

`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!

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**:

`BlocSignal`

API contracts without hallucination.`ref.onDispose`

, `unawaited(super.onEvent(event))`

).`blocSignalTest`

suites with 100% line coverage.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.

| 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` |

Check out the official open-source packages and AI skills on GitHub:

`pub.dev/packages/bloc_signals`

`pub.dev/packages/bloc_signals_flutter`

`pub.dev/packages/bloc_signals_riverpod`

`pub.dev/packages/bloc_signals_test`

`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! 🚂
