๐ 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()); 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 beforeEventA 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 ๐ฑ 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 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()); 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 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(...) ...but replaces Streams with Signals. Its propagation path is much shorter. .add(Event) โ Handler โ emit(State) โ Signal โ BlocSignalBuilder โ markNeedsBuild() โ Flutter Frame The biggest difference is: The handler executes synchronously. Meaning this: bloc.add(EventA()); print(bloc.state); already prints StateA because the state has already been updated before .add() returns. Then... bloc.add(EventB()); 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()); } Many people imagine: markNeedsBuild() โ Build โ markNeedsBuild() โ Build โ markNeedsBuild() โ Build 10,000 times. Fortunately... Flutter is much smarter than that. ๐จ What markNeedsBuild() Actually Does Calling markNeedsBuild(); does not immediately rebuild the widget. It simply tells Flutter: "This widget is dirty." Internally it's roughly equivalent to: _dirty = true; If it's already dirty... Setting _dirty = true; again changes nothing. So markNeedsBuild() markNeedsBuild() markNeedsBuild() markNeedsBuild() still results in _dirty == true 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()); 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)
Comments
No comments yet. Start the discussion.