DEV Community

Yash Fadadu
Yash Fadadu

Posted on

πŸš€ What Really Happens When You Call .add() Twice? Classic BLoC vs BlocSignal

We've all been there.

You're building a Flutter app with BLoC, and somewhere in your business logic you write:

myBloc.add(LoadUserData());
myBloc.add(FetchUserPreferences());
Enter fullscreen mode Exit fullscreen mode

Immediately, a thought pops into your head...

πŸ€” "Wait... did I just overwrite the first event before the UI even had a chance to rebuild?"

Or maybe:

  • Will EventB execute before EventA finishes?
  • Will the UI skip StateA?
  • If I read bloc.state immediately after .add(), what state do I get?
  • What happens if I call .add() 10,000 times?

Let's lift the hood and compare Classic BLoC and BlocSignal.


🀯 The Common Misconception

Many developers imagine something like this:

EventA
   ↓
StateA

EventB
   ↓
StateB

↓

UI only sees StateB 😱
Enter fullscreen mode Exit fullscreen mode

The fear is that rapid-fire .add() calls somehow overwrite each other before Flutter can render.

Fortunately...

That's not how either implementation works.

The difference lies in how state propagates, not whether events get lost.


🐒 Classic BLoC (Streams)

Traditional flutter_bloc is built on top of Dart Streams.

A simplified flow looks like this:

.add(Event)

        ↓

 StreamController

        ↓

 Event Handler

        ↓

 emit(State)

        ↓

 State Stream

        ↓

 BlocBuilder Listener

        ↓

 markNeedsBuild()

        ↓

 Flutter Frame
Enter fullscreen mode Exit fullscreen mode

Notice something important:

Flutter does not rebuild immediately.

It simply marks the widget as dirty and rebuilds during the next rendering frame.


So what happens?

Imagine:

bloc.add(EventA());
bloc.add(EventB());
Enter fullscreen mode Exit fullscreen mode

Internally it behaves more like:

EventA

↓

Handler runs

↓

emit(StateA)

↓

Widget marked dirty

↓

EventB

↓

Handler runs

↓

emit(StateB)

↓

Widget marked dirty again

↓

Flutter renders

↓

Latest state displayed
Enter fullscreen mode Exit fullscreen mode

No event is lost.

Both handlers execute.

The UI simply paints the latest settled state when Flutter decides to render the next frame.


⚑ BlocSignal (Signals)

BlocSignal keeps the familiar BLoC programming model...

bloc.add(...)
emit(...)
Enter fullscreen mode Exit fullscreen mode

...but replaces Streams with Signals.

Its propagation path is much shorter.

.add(Event)

      ↓

Handler

      ↓

emit(State)

      ↓

Signal

      ↓

BlocSignalBuilder

      ↓

markNeedsBuild()

      ↓

Flutter Frame
Enter fullscreen mode Exit fullscreen mode

The biggest difference is:

The handler executes synchronously.

Meaning this:

bloc.add(EventA());

print(bloc.state);
Enter fullscreen mode Exit fullscreen mode

already prints

StateA
Enter fullscreen mode Exit fullscreen mode

because the state has already been updated before .add() returns.

Then...

bloc.add(EventB());
Enter fullscreen mode Exit fullscreen mode

starts.

So EventB always sees the latest state.


πŸ€” Doesn't that rebuild the UI every time?

This is probably the most interesting question.

Suppose you do something completely unreasonable πŸ˜‚

for (int i = 0; i < 10000; i++) {
  bloc.add(IncrementEvent());
}
Enter fullscreen mode Exit fullscreen mode

Many people imagine:

markNeedsBuild()

↓

Build

↓

markNeedsBuild()

↓

Build

↓

markNeedsBuild()

↓

Build
Enter fullscreen mode Exit fullscreen mode

10,000 times.

Fortunately...

Flutter is much smarter than that.


🎨 What markNeedsBuild() Actually Does

Calling

markNeedsBuild();
Enter fullscreen mode Exit fullscreen mode

does not immediately rebuild the widget.

It simply tells Flutter:

"This widget is dirty."

Internally it's roughly equivalent to:

_dirty = true;
Enter fullscreen mode Exit fullscreen mode

If it's already dirty...

Setting

_dirty = true;
Enter fullscreen mode Exit fullscreen mode

again changes nothing.

So

markNeedsBuild()

markNeedsBuild()

markNeedsBuild()

markNeedsBuild()
Enter fullscreen mode Exit fullscreen mode

still results in

_dirty == true
Enter fullscreen mode Exit fullscreen mode

Once Flutter reaches the next frame...

it performs one build using the latest state.


πŸ“Š Classic BLoC vs BlocSignal

Feature Classic BLoC 🐒 BlocSignal ⚑
State Propagation Stream-based Signal-based
Handler Execution Stream/event pipeline Direct synchronous execution
bloc.state immediately after .add() Depends on propagation timing Updated immediately
Internal Dispatch Stream subscriptions Signal dependency graph
UI Rendering Flutter frame pipeline Flutter frame pipeline
Widget Rebuild Deferred to Flutter Deferred to Flutter
Developer Experience Mature & battle-tested Familiar API with Signals underneath

🎯 So... what does BlocSignal actually optimize?

The optimization isn't that Flutter suddenly rebuilds faster.

Flutter's rendering pipeline is exactly the same.

Instead, BlocSignal removes much of the reactive plumbing:

❌ StreamController

❌ Stream subscriptions

❌ Stream dispatch

❌ Async propagation through streams

Replacing it with:

βœ… Direct synchronous state updates

βœ… Signal dependency propagation

βœ… Immediate state availability

This makes the execution path shorter and more predictable.


πŸ’™ Final Thoughts

Calling:

bloc.add(EventA());
bloc.add(EventB());
Enter fullscreen mode Exit fullscreen mode

is perfectly safe.

Neither Classic BLoC nor BlocSignal loses events.

The key difference is how the new state travels from your business logic to the UI.

  • 🐒 Classic BLoC uses a Stream-based propagation pipeline.
  • ⚑ BlocSignal replaces that pipeline with synchronous Signal propagation.

Both still rely on Flutter's rendering engine, which intelligently batches widget rebuilds into the next frame.

So no matter how many times you call .add(), Flutter only paints what actually mattersβ€”the latest settled state.

Top comments (0)