The Hidden Trap of Dart Streams, Isolates, and ReceivePorts: Why Your Listeners Stop Working (and How to Fix It)
TL;DR:
If you use Dart isolates with ReceivePort and expose its stream to your Flutter UI, your listeners will silently break whenever you restart the isolate. The fix? Use a persistent StreamController.broadcast() as your public stream and pipe all events into it.
The Problem: Listeners That Stop Listening
Recently, I hit a frustrating bug in my Flutter app. I was using isolates to fetch real-time data, exposing a dataStream getter in my service like this:
Stream<dynamic> get dataStream {
  if (_receiver == null) return Stream.empty();
  return _receiver!;
}
In my UI, I’d listen to dataStream in initState():
_realtimeInstance.dataStream.listen((data) {
  // update state
});
It worked—until I restarted the service (by calling start() again). Suddenly, my UI stopped receiving updates. No errors, no warnings, just… silence.
The Subtle Cause: ReceivePort Replacement
Every time you call start(), you create a new ReceivePort:
_receiver = ReceivePort();
But your UI is still listening to the old stream! The new ReceivePort is a new stream object, and your listeners are now disconnected.
Result:
Your UI never receives data after the first restart.
Why Isn’t This in the Docs?
- The Dart/Flutter docs focus on simple, static streams.
- Most tutorials don’t cover isolates, or dynamic replacement of streams.
- The ReceivePortdocs don’t warn you about this “hot swap” problem.
This is one of those bugs you only learn about the hard way.
The Robust Solution: Persistent StreamController
Here’s the pattern you want:
- Create a private, persistent StreamController.broadcast().
- Expose its stream as your public API.
- Pipe all data from your ReceivePortinto this controller.
Example:
final StreamController<dynamic> _controller = StreamController.broadcast();
Stream<dynamic> get dataStream => _controller.stream;
Future<void> start() async {
  _receiver = ReceivePort();
  _receiver!.listen((event) {
    _controller.add(event);
  });
  // ... start isolate, etc.
}
Now, your UI can listen to dataStream once, and always receive updates—no matter how many times you start/stop the service or replace the port.
The Takeaway
- Never expose a stream that may be replaced under the hood.
- Always use a persistent StreamController.broadcast()for your public API.
- Pipe all sources (ReceivePort, sockets, etc.) into it.
Bonus: Why Is This a “Broadcast” Stream?
- A broadcast stream allows multiple listeners (e.g., UI, logs, analytics).
- It prevents “single subscription” errors if you have more than one consumer.
Final Thoughts
If you’re building anything with Dart isolates and real-time data, this pattern will save you hours of silent bugs and confusion. I wish I’d seen this in the docs—so I’m writing it for you!
Did this save you? Let me know in the comments, or share your own Dart gotchas!
Author:
Dr. Pshtiwan Mahmood
Flutter/Dart Enthusiast | Real-time Systems Debugger
 

 
    
Top comments (0)