Classic BLoC with Streams and Sinks
Classic BLoC separates UI from business logic by exposing inputs as Sinks and outputs as Streams. The widget pushes events into sinks, the BLoC processes them, and emits new state on streams that the UI rebuilds from, keeping logic testable and…
Why it exists
Classic BLoC emerged to solve a recurring Flutter problem: business logic tangled inside widgets is hard to test and reuse. Introduced by Google engineers, the pattern's goal was a clean boundary so the same logic could run across Flutter and AngularDart, with the UI reduced to a thin reactive shell.
The mental model
Think of a BLoC as a sealed box with two pipes. One pipe, the Sink, takes input events flowing in; the other, the Stream, carries output state flowing out. The widget never calls methods that mutate UI state directly. Instead it drops an event into the sink and later receives whatever new state the box chooses to emit. The box knows nothing about widgets, only data in and data out.
How it works
Internally the BLoC holds StreamControllers. Its sink is the controller's sink, exposing add for incoming events; its stream is the controller's stream, exposing emitted state. The BLoC listens to its input stream, runs business logic when an event arrives, and adds the resulting state to the output controller. In the widget tree a StreamBuilder subscribes to the output stream and rebuilds on each new state, while gesture handlers call add on the input sink. The BLoC must be disposed to close its controllers and avoid leaks.
When it matters
It matters when you want logic decoupled from the framework and easily unit tested without pumping widgets. It shines for cross-platform sharing and for teams that prefer explicit reactive streams. In modern Flutter most teams reach for the flutter_bloc package or other state solutions, but understanding the raw stream-and-sink mechanics clarifies what those abstractions automate.
A concrete example
A counter BLoC exposes an incrementSink and a countStream. A button's onPressed calls incrementSink.add(()). The BLoC listens, increments an internal value, and adds the new count to the count controller. A StreamBuilder wrapping the Text widget receives the new count and rebuilds, showing the updated number, all without the widget ever holding mutable counter state.
Interview question
In classic BLoC, how does a widget trigger a change in business logic, and how does it receive the result?
- a.It adds an event to a Sink and rebuilds from state emitted on a StreamCorrect
- b.It calls setState directly and reads a public field on the BLoC
- c.It mutates the BLoC's internal variables and forces a repaint
- d.It awaits a Future returned by the BLoC for each interaction
Why? this is the answer
Classic BLoC exposes inputs as Sinks and outputs as Streams, so the widget pushes events into a sink and rebuilds via a StreamBuilder on emitted state. Calling setState or mutating fields directly would defeat the pattern's separation of logic from UI.
Just read this? Test yourself on what you have been reading.
- #flutter
- #dart
- #bloc
- #streams
- #state-management
Put your scrolling time to good use
Learn one idea, try a quiz and save useful cards for revision. Tezvyn makes it easy to learn and stay current in your tech field, a few minutes at a time.
The iPhone app is on the way
We are building it. Until it lands, nothing here is held back from you: every interview card, your saved cards, streaks and the job board all work in Safari, plus hundreds of free practice quizzes of thirty questions each. Sign in and it all carries over to the app the day it arrives.
Want it as an icon? Tap Share at the bottom of Safari, then Add to Home Screen. It opens full screen and the cards you have read stay available offline.
We are hiring for this. Open roles that interview on flutter — each one lists the topics its interview covers.
See open roles