dartstreamsubscriptionlisten

failure on multiple calls to "first" property in a dart stream


although dart source code cancels the stream subscription, i get a Bad state: Stream has already been listened to error since the second call to stream.first.

sample code:

import 'dart:async';

void main() async {
  final ctrl = StreamController<int>();
  // fails also with:
  // final ctrl = StreamController<int>(sync: true);
  // final ctrl = StreamController<int>.broadcast();
  // final ctrl = StreamController<int>.broadcast(sync: true);

  ctrl.add(1);
  final i1 = await ctrl.stream.first; // ok
  print(i1.toString());

  ctrl.add(2);
  final i2 = await ctrl.stream.first; // error!
  print(i2.toString());
}

any insight will be helpful.


Solution

  • It is not the same error you are getting in all four examples. In general, there are two different types of streams to consider here:

    Stream

    A normal Stream can only have one subscriber and can hereafter not be used again even if this one subscriber makes an un-subscription. If you read the documentation of the .first property you get:

    The first element of this stream.

    Stops listening to this stream after the first element has been received.

    Internally the method cancels its subscription after the first element. This means that single-subscription (non-broadcast) streams are closed and cannot be reused after a call to this getter.

    https://api.dart.dev/stable/3.4.4/dart-async/Stream/first.html

    So your program works as expected with its first subscription where we can see your program do print out the one element that got added to the Stream. The problem first comes when you try reuse the Stream after it have been used.

    BroadcastStream

    In those cases your program does actually not crash but it seems to do nothing. The reason for this are because of this detail:

    Broadcast streams do not buffer events when there is no listener.

    https://api.dart.dev/stable/3.4.4/dart-async/StreamController/StreamController.broadcast.html

    So any events you add to a broadcast stream, will be thrown away if there are no listeners for the events. If we instead change the program a bit so we makes sure there are a subscription before data gets added:

    import 'dart:async';
    
    void main() async {
      final ctrl = StreamController<int>.broadcast();
    
      ctrl.stream.first.then(print);
      ctrl.add(1);
    
      ctrl.stream.first.then(print);
      ctrl.add(2);
    }
    

    We now get a program that prints 1 and then 2.

    You might now think:

    "Hey, that is great but why does my program not just halt forever since no event ever comes when doing await ctrl.stream.first?"

    Reason for this is that Dart will stop your program if there are no more events on the event queue and there are nothing in the program that can affect your program like e.g. Timer, ReceivePort, network calls and so on. So in your case, your program are "dead" and can just be stopped instead of halting forever.