Skip to content
This repository was archived by the owner on Oct 17, 2024. It is now read-only.

Add StreamExtensions.firstOrNull #195

Merged
merged 3 commits into from
Aug 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.9.0

* Add `StreamExtensions.firstOrNull`.

## 2.8.2

* Deprecate `EventSinkBase`, `StreamSinkBase`, `IOSinkBase`.
Expand Down
25 changes: 25 additions & 0 deletions lib/src/stream_extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,29 @@ extension StreamExtensions<T> on Stream<T> {
sink.close();
}));
}

/// A future which completes with the first event of this stream, or with
/// `null`.
///
/// This stream is listened to, and if it emits any event, whether a data
/// event or an error event, the future completes with the same data value or
/// error. If the stream ends without emitting any events, the future is
/// completed with `null`.
Future<T?> get firstOrNull {
var completer = Completer<T?>.sync();
final subscription = listen(null);
Copy link
Contributor

@lrhn lrhn Aug 31, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can use cancelOnError: true. Then you can just tear off completer.completeErrorinstead of creating a new closure. It can even be:

  var subscription = listen(null, 
    onError: completer.completeError,
    onDone: completer.complete, // The argument is optional if the type is nullable!
    cancelOnError: true);
  subscription.onData((event) {
    subscription.cancel();
    completer.complete(event);
  });

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered doing it that way, but I think the symmetry between the onData and onError events is easier to follow than having one of them manually cancel the subscription and the other cancel it through cancelOnError.

subscription
..onData((event) {
subscription.cancel();
completer.complete(event);
})
..onError((Object error, StackTrace stackTrace) {
subscription.cancel();
completer.completeError(error, stackTrace);
})
..onDone(() {
completer.complete(null);
});
return completer.future;
}
}
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: async
version: 2.8.3
version: 2.9.0-dev

description: Utility functions and classes related to the 'dart:async' library.
repository: https://github.com/dart-lang/async
Expand Down
37 changes: 37 additions & 0 deletions test/stream_extensions_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,41 @@ void main() {
expect(() => Stream.fromIterable([1]).slices(0), throwsRangeError);
});
});

group('.firstOrNull', () {
test('returns the first data event', () {
expect(
Stream.fromIterable([1, 2, 3, 4]).firstOrNull, completion(equals(1)));
});

test('returns the first error event', () {
expect(Stream.error('oh no').firstOrNull, throwsA('oh no'));
});

test('returns null for an empty stream', () {
expect(Stream.empty().firstOrNull, completion(isNull));
});

test('cancels the subscription after an event', () async {
var isCancelled = false;
var controller = StreamController<int>(onCancel: () {
isCancelled = true;
});
controller.add(1);

await expectLater(controller.stream.firstOrNull, completion(equals(1)));
expect(isCancelled, isTrue);
});

test('cancels the subscription after an error', () async {
var isCancelled = false;
var controller = StreamController<int>(onCancel: () {
isCancelled = true;
});
controller.addError('oh no');

await expectLater(controller.stream.firstOrNull, throwsA('oh no'));
expect(isCancelled, isTrue);
});
});
}