The Hidden Trap of Dart Streams, Isolates, and ReceivePorts: Why Your Listeners Stop Working (and How to Fix It)



This content originally appeared on DEV Community and was authored by Pshtiwan Mahmood

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 ReceivePort docs 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:

  1. Create a private, persistent StreamController.broadcast().
  2. Expose its stream as your public API.
  3. Pipe all data from your ReceivePort into 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


This content originally appeared on DEV Community and was authored by Pshtiwan Mahmood