cd /news/developer-tools/switching-tracks-in-blocsignal-the-u… · home topics developer-tools article
[ARTICLE · art-75600] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=↑ positive

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

Randal L. Schwartz introduces BlocSignal, a state management library for Flutter that combines BLoC's event-state architecture with Signals' synchronous reactivity. BlocSignal acts as a central switchyard allowing seamless interop between BLoC, Riverpod, Provider, and Signals, eliminating the need to rewrite existing state management code.

read6 min views1 publishedJul 27, 2026

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

(orWidgetRef

/ProviderContainer

) automatically registersref.onDispose

teardown hooks. When the parent Riverpod container or widget unmounts, the underlyingBlocSignal

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

── more in #developer-tools 4 stories · sorted by recency
── more on @randal l. schwartz 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/switching-tracks-in-…] indexed:0 read:6min 2026-07-27 ·