rxdart BehaviorSubject emits previous events and not only last value

Viewed 221

I have a strange issue with rxdart BehaviorSubject. Normally BehaviorSubject should only emit the last value to the stream:

A special StreamController that captures the latest item that has been added to the controller, and emits that as the first item to any new listener.

But in my case it is emitting all the previous values on the stream too and I dont know why.

I have the following test bloc:

class TestBloc {

  final _controller = BehaviorSubject.seeded([]);

  Stream get stream$ => _controller.stream;

  TestBloc(BehaviorSubject subject) {
    subject.listen((e) {
      print("listen $e");
      _controller.add([e]);
    });
  }

}

With test case:

    test("test bloc", () async {
      final subject = BehaviorSubject();

      final bloc = TestBloc(subject);

      subject.add(1);
      subject.add(2);

      // Uncomment next line to make test pass
      // await expectLater(subject.stream, emits(2)); 

      // Test will fail here
      await expectLater(bloc.stream$, emits([2]));
    });

The test fails because all the events from the subject where emitted and not only the expected last:

Expected: should emit an event that [2]
  Actual: <Instance of 'BehaviorSubject<List<dynamic>>'>
   Which: emitted * []
                  * [1]
                  * [2]
            which emitted an event that at location [0] is [] which shorter than expected

Is this an issue with rxdart or am I doing something wrong. To my understanding the .add() call in the listener should have the same effect as calling add consecutive like in the test case but it seems not to.

2 Answers

Code Spirit

The same behaviour could be faced when messing up with stream concurrency. To illustrate this you can add couple debug lines to your code:

class TestBloc {

  final _controller = BehaviorSubject.seeded([]);

  Stream get stream$ => _controller.stream;

  TestBloc(BehaviorSubject subject) {
    subject.listen((e) {
      print("listen $e");
      print('A: ${DateTime.now()}');
      _controller.add([e]);
      print('B: ${DateTime.now()}');
    });
  }

}

test case:

test("test bloc", () async {
      final subject = BehaviorSubject();

      final bloc = TestBloc(subject);

      subject.add(1);
      subject.add(2);

      // Uncomment next line to make test pass
      // await expectLater(subject.stream, emits(2));

      /// If you uncomment next comment it will fix the problem
      /// this await Future.delayed(Duration.zero) is added
      /// to avoid executing the expectLater() before
      /// the listener in TestBloc constructor.

      // await Future.delayed(Duration.zero);
      print('C: ${DateTime.now()}');
      await expectLater(bloc.stream$, emits([2]));
      print('D: ${DateTime.now()}');
  });

If you run this code you will see:

C: 2022-08-24 23:52:23.910217
listen 1
A: 2022-08-24 23:52:23.941463
B: 2022-08-24 23:52:23.942454
listen 2
A: 2022-08-24 23:52:23.945855
B: 2022-08-24 23:52:23.946078

thus

await expectLater(bloc.stream$, emits([2]));

is executed before

_controller.add([e]);

but when you uncomment

await expectLater(subject.stream, emits(2));

it plays the same role as

await Future.delayed(Duration.zero);

so if you expect the last item willbe [2], then you have to push [2] as the expected value.if you need to listen for multiple values you can also push multiple values

test("test bloc", () async { final subject = BehaviorSubject();

  final bloc = TestBloc(subject);

  subject.add(2);

  // Test will successful
  await expectLater(bloc.stream$, emits([2]));
});
Related