How to create a StreamTransformer in Dart? - stream

Trying to build a custom StreamTransformer class, however a lot of the examples out there seem to be out of date, and the one found in the documentation isn't (what some typed languages might consider anyway) as a class (found here: https://api.dartlang.org/apidocs/channels/stable/dartdoc-viewer/dart:async.StreamTransformer). This doesn't seem like a very Dart-like way of approaching it and rather more of a Javascript-like way (which I'm using Dart to avoid).
Many online sources say this is how you create a StreamTransformer, however there errors when extending it.
class exampleStreamTransformer extends StreamTransformer
{
//... (This won't work)
}
'Implements' seems to be the way to go, along with implementing the bind function needed:
class exampleStreamTransformer implements StreamTransformer
{
Stream bind(Stream stream)
{
//... (Go on to return new stream, etc)
}
}
I can't seem to find any examples of this way, but have thrown something together myself (which is accepted in my IDE, but isn't accepted at runtime, I get a null object error when it tries to use pause getter):
class exampleStreamTransformer implements StreamTransformer
{
StreamController<String> _controller;
StreamSubscription<String> _subscription;
Stream bind(Stream stream)
{
_controller = new StreamController<String>(
onListen: ()
{
_subscription = stream.listen((data)
{
// Transform the data.
_controller.add(data);
},
onError: _controller.addError,
onDone: _controller.close,
cancelOnError: true); // Unsure how I'd pass this in?????
},
onPause: _subscription.pause,
onResume: _subscription.resume,
onCancel: _subscription.cancel,
sync: true
);
return _controller.stream;
}
}
Would like to achieve it this way, as in the 'typed' way of producing the class, any help is much appreciated, thank you.

Why don't you use StreamTransformer.fromHandler():
import 'dart:async';
void handleData(data, EventSink sink) {
sink.add(data*2);
}
void main() {
StreamTransformer doubleTransformer = new StreamTransformer.fromHandlers(handleData: handleData);
StreamController controller = new StreamController();
controller.stream.transform(doubleTransformer).listen((data) {
print('data: $data');
});
controller.add(1);
controller.add(2);
controller.add(3);
}
Output:
data: 2
data: 4
data: 6

Okay. Here's another working example:
import 'dart:async';
class DuplicateTransformer<S, T> implements StreamTransformer<S, T> {
StreamController _controller;
StreamSubscription _subscription;
bool cancelOnError;
// Original Stream
Stream<S> _stream;
DuplicateTransformer({bool sync: false, this.cancelOnError}) {
_controller = new StreamController<T>(onListen: _onListen, onCancel: _onCancel, onPause: () {
_subscription.pause();
}, onResume: () {
_subscription.resume();
}, sync: sync);
}
DuplicateTransformer.broadcast({bool sync: false, bool this.cancelOnError}) {
_controller = new StreamController<T>.broadcast(onListen: _onListen, onCancel: _onCancel, sync: sync);
}
void _onListen() {
_subscription = _stream.listen(onData,
onError: _controller.addError,
onDone: _controller.close,
cancelOnError: cancelOnError);
}
void _onCancel() {
_subscription.cancel();
_subscription = null;
}
/**
* Transformation
*/
void onData(S data) {
_controller.add(data);
_controller.add(data); /* DUPLICATE EXAMPLE!! REMOVE FOR YOUR OWN IMPLEMENTATION!! */
}
/**
* Bind
*/
Stream<T> bind(Stream<S> stream) {
this._stream = stream;
return _controller.stream;
}
}
void main() {
// Create StreamController
StreamController controller = new StreamController.broadcast();
// Transform
Stream s = controller.stream.transform(new DuplicateTransformer.broadcast());
s.listen((data) {
print('data: $data');
}).cancel();
s.listen((data) {
print('data2: $data');
}).cancel();
s.listen((data) {
print('data3: $data');
});
// Simulate data
controller.add(1);
controller.add(2);
controller.add(3);
}
Let me add some notes:
Using implements seems to be the right way here when looking at the source code of other dart internal transformers.
I implemented both versions for regular and a broadcast stream.
In case of a regular stream you can call cancel/pause/resumt directly on the new stream controller because we can only listen once.
If you use a broadcast stream I found out that listen() is only called if there is no one listening already to the stream. onCancel behaves the same. If the last subscriber cancels its subscription, then onCancel is called. That's why it is safe to use the same functions here.

Unlike map, transformers are more powerful and allows you to maintain an internal state, and emit a value whenever you want. It can achieve things map can't do, such as delaying, duplicating values, selectively omitting some values, and etc.
Essentially, the implementation requires a bind method that provides a new stream based on an old stream being passed in, and a cast method that helps with type-checking during run-time.
Here's an over-simplified example of implementing a "TallyTransformer" that transforms a stream of integer values into a stream of sums. For example, if the input stream so far had 1, 1, 1, -2, 0, ..., the output stream would've been 1, 2, 3, 1, 1, ..., i.e. summing all inputs up to this point.
Example usage: stream.transform(TallyTransformer())
class TallyTransformer implements StreamTransformer {
StreamController _controller = StreamController();
int _sum = 0; // sum of all values so far
#override
Stream bind(Stream stream) {
// start listening on input stream
stream.listen((value) {
_sum += value; // add the new value to sum
_controller.add(_sum); // emit current sum to our listener
});
// return an output stream for our listener
return _controller.stream;
}
#override
StreamTransformer<RS, RT> cast<RS, RT>() {
return StreamTransformer.castFrom(this);
}
}
This example is over-simplified (but still works) and does not cover cases such as stream pausing, resuming or canceling. If you run into "Stream has already been listened" error, make sure streams are broadcasting.

https://github.com/dart-lang/sdk/issues/27740#issuecomment-258073139
You can use StreamTransformer.fromHandlers to easily create
transformers that just convert input events to output events.
Example:
new StreamTransformer.fromHandlers(handleData: (String event, EventSink output) {
if (event.startsWith('data:')) {
output.add(JSON.decode(event.substring('data:'.length)));
} else if (event.isNotEmpty) {
output.addError('Unexpected data from CloudBit stream: "$event"');
}
});

If you want to simply transform values using a function like this
int handleData(int data) {
return data * 2;
}
use map method of Stream
stream
.map(handleData)
.listen((data) {
print('data: $data');
});
Full example:
import 'dart:async';
int handleData(int data) {
return data * 2;
}
void main() {
final controller = StreamController<int>();
controller.stream
.map(handleData)
.listen((data) {
print('data: $data');
});
controller.add(1);
controller.add(2);
controller.add(3);
}
See more examples on dart.dev

Related

Can I listen to a StreamController's stream multiple times (but not more than one subscription at a time)?

I am using a StreamController in dart. I would like to be able to stop listening to the stream of the controller and then start listening again. I do not require to have multiple listeners at once. I just require that I listen to the stream, then stop listening to the stream, and then establish a new listener afterwards.
I have created a minimal example where I try to cancel the initial subscription, and then listen to the stream again, but I still get a bad state error.
import 'dart:async';
void main(List<String> arguments) async {
var controller = StreamController<String>();
var sub = controller.stream.listen((event) { });
await sub.cancel();
controller.stream.listen((event) { }); // throws bad state
}
The StreamController class creates either a single-subscription stream (which you can only listen to once) or a broadcast stream (if created using StreamController.broadcast) which can be listened to multiple times.
For your described use, you'd want the broadcast variant.
You probably want to avoid sending events while that controller has no listeners (broadcast streams can emit events even when there are no listeners, they are broadcast into the void). The onCancel and onListen callbacks of a broadcast stream controller are called when the last listener is cancelled and the first (new) listener is added.
The StreamController.broadcast controller doesn't prevent you from having multiple simultaneous listeners, but that's something you should be able to avoid just by being careful.
Example:
import 'dart:async';
void main(List<String> arguments) async {
var controller = StreamController<String>.broadcast();
controller.onListen = () {
print("Active");
};
controller.onCancel = () {
print("Inactive");
};
var sub = controller.stream.listen((event) { }); // "Active"
await sub.cancel(); // "Inactive"
controller.stream.listen((event) { }); // "Active"
}
If you really want to insist on "one listener at a time", you can wrap the stream in something like:
import "dart:async";
/// Allows a stream to be listened to multiple times.
///
/// Returns a new stream which has the same events as [source],
/// but which can be listened to more than once.
/// Only allows one listener at a time, but when a listener
/// cancels, another can start listening and take over the stream.
///
/// If the [source] is a broadcast stream, the listener on
/// the source is cancelled while there is no listener on the
/// returned stream.
/// If the [source] is not a broadcast stream, the subscription
/// on the source stream is maintained, but paused, while there
/// is no listener on the returned stream.
///
/// Only listens on the [source] stream when the returned stream
/// is listened to.
Stream<T> resubscribeStream<T>(Stream<T> source) {
MultiStreamController<T>? current;
StreamSubscription<T>? sourceSubscription;
bool isDone = false;
void add(T value) {
current!.addSync(value);
}
void addError(Object error, StackTrace stack) {
current!.addErrorSync(error, stack);
}
void close() {
isDone = true;
current!.close();
current = null;
sourceSubscription = null;
}
return Stream<T>.multi((controller) {
if (isDone) {
controller.close(); // Or throw StateError("Stream has ended");
return;
}
if (current != null) throw StateError("Has listener");
current = controller;
var subscription = sourceSubscription ??=
source.listen(add, onError: addError, onDone: close);
subscription.resume();
controller
..onPause = subscription.pause
..onResume = subscription.resume
..onCancel = () {
current = null;
if (source.isBroadcast) {
sourceSubscription = null;
return subscription.cancel();
}
subscription.pause();
return null;
};
});
}

Is it possible to add an item to a List contained in a Stream in Dart?

Problem
I have a Stream<List> which is being listened to in several classes and I need all the classes to receive the updated stream once a value is added to the list in the Stream.
What I have tried
void main() {
StreamedList<String> dataStream = StreamedList();
dataStream.data.listen((list) => print(list));
dataStream.updateList(['Apple', 'Orange']);
dataStream.addToList('Mango'); // This is what I want to do
}
This is the code for StreamList class
class StreamedList<T> {
StreamController<List<T>> _controller = StreamController.broadcast();
Stream<List<T>> get data => _controller.stream;
void updateList(List<T> list) {
_controller.sink.add(list);
}
void addToList(T value) {
// Is it possible to do this?
// List<T> dataList = await _controller.data;
// dataList.add(value);
// updateList(dataList);
}
void dispose() {
_controller.close();
}
}
I have tried different APIs from the dart:async library including Stream.first, etc. which return a Future<List<T>>. But the problem is that this Future resolves only after something is added to the Stream later (eg. by calling the StreamedList.updateList function).
Question
How do I add a single value to the List inside the Stream?
You are misunderstanding what a Stream does. It doesn't "contain" data. It merely accepts data on one end (the sink) and propagates it out the other end (the stream), and any listeners gain access to the streamed object. Trying to "insert an item in a list within the stream" doesn't make any conceptual sense.
If you want to push a list with an additional item, take the old list, append the item to it, then re-add the list to the stream.
class StreamedList<T> {
StreamController<List<T>> _controller = StreamController.broadcast();
Stream<List<T>> get data => _controller.stream;
List<T> _list = [];
void updateList(List<T> list) {
_list = list;
_dispatch();
}
void addToList(T value) {
_list.add(value);
_dispatch();
}
void _dispatch() {
controller.sink.add(_list);
}
void dispose() {
_list = null;
_controller.close();
}
}
If you wanted to be doubly safe, you could recreate the list after every addToList, since if a listener captured the list elsewhere and modified its contents, that would affect _list as well.
void addToList(T value) {
_list = [..._list, value];
_dispatch();
}

What is the difference between StreamSink and Sink?

I was coding a simple dart code and I couldn't see any difference between the implementation of StreamSink and Sink. By the way both have the same behaviour in this case.
int _counter = 0;
final _counterStreamController = StreamController<int>();
final _counterEventController = StreamController<CounterEvent>();
CounterBloc() {
_counterEventController.stream.listen(mapEventToState);
}
StreamSink<int> get _sinkCounter => _counterStreamController.sink;
Stream<int> get counter => _counterStreamController.stream;
Sink<CounterEvent> get counterEventSink => _counterEventController.sink;
void mapEventToState(CounterEvent event) {
if (event is IncrementEvent) {
_counter++;
}
_sinkCounter.add(_counter);
}
The StreamSink class implements a StreamConsumer and EventSink on top of a Sink.
StreamConsumer allows to add multiple Streams to a Sink, so your StreamSink can "output" several streams.
The EventSink provides the method to addErrors besides data to the stream.

Avoid repetition in BLoCs and RxDart

hopefully I can make myself clear.
After video and tutorials, I found this way to have some widgets to input data to the bloc (valueSetting) and some others to get this data (value).
What I am asking is if there is a better way (there has to be..). I want to avoid the need to have 4 variables for just 1 real value shared between widgets.
import 'dart:async';
import 'package:rxdart/subjects.dart';
class BlocExample {
final _valueSettingController = StreamController<bool>();
// object use by widget to push data
Sink<bool> get valueSetting => _valueSettingController.sink;
final _value = BehaviorSubject<bool>(seedValue: false);
// object used by widget to get data
Stream<bool> get value => _value.stream;
BlocExample() {
_valueSettingController.stream.listen(_value.add);
}
void dispose() {
_value.close();
_valueSettingController.close();
}
}
First of, let me say that you can remove the private variables by using a custom factory constructor. Here's an example:
class MyBloc {
final Sink<bool> input;
final Stream<bool> output;
final VoidCallback _dispose;
MyBloc._({this.input, this.output, VoidCallback dispose}) : _dispose = dispose;
factory MyBloc() {
final mainController = BehaviorSubject(seedValue: false);
return MyBloc._(
input: mainController.sink,
output: mainController.stream,
dispose: () {
mainController.close();
},
);
}
void dispose() {
_dispose();
}
}
Secondly, the problem you're trying to solve is actually not a problem. While it seems at first that there's a lot of duplicates; in reality they serve different purposes.
In many situations, your Stream will be more than just _controller.stream. For example, for whatever reason you may want to transform the value before exposing it:
final mainController = BehaviorSubject(seedValue: false);
final Stream<bool> output = mainController.map((foo) => !foo);
This code makes that the output stream reverses the value of everything passed to mainController.sink
But in my situation this is not the case. So why 3 variables that point to the same thing?
The fact that in your situation, your controller is both the sink and stream without transformation is an implementation detail and may be subject to changes.
By exposing Sink/Stream as done before, you actually abstract this implementation detail. So that in the future if your stream needs custom operations; no change will be required by your UI.
This is not necessary. But recommended.
You can do something like this :)
enum STREAM_GROUP {
TYPE1,TYPE2,TYPE3
}
class BlocExample {
Map<STREAM_GROUP, StreamController<bool>> groups = new Map();
Stream<bool> getValue(STREAM_GROUP type){
return groups[type].stream;
}
Sink<bool> getValueSetting(STREAM_GROUP type){
return groups[type].sink;
}
BlocExample() {
groups[STREAM_GROUP.TYPE1] = StreamController<bool>();
groups[STREAM_GROUP.TYPE2] = StreamController<bool>();
groups[STREAM_GROUP.TYPE3] = StreamController<bool>();
groups.forEach((groupType, streamController){
final currentValue = BehaviorSubject<bool>(seedValue: false);
streamController.stream.listen(currentValue.add);
});
}
void dispose() {
groups.forEach((groupType, streamController){
streamController.close();
});
}
}

How do you create a Stream in Dart?

I basically know how to use them; for instance listening to the onClick Stream of an Element.
But, how do you set up your own Streams?
Simple example
Here's a complete working example:
import 'dart:async';
import 'dart:io';
class Application {
Stream onExit;
Application() {
// Create a stream controller and assign its stream to "onExit".
var controller = new StreamController();
onExit = controller.stream;
// Create some class that uses our stream.
new UserOfStream(this);
// Whenever we exit the application, notify everyone about it first.
controller.add('we are shutting down!');
exit(0);
}
}
class UserOfStream {
UserOfStream(app) {
app.onExit.listen((String message) => print(message));
}
}
main() => new Application();
You can also do cool things like check if there are subscribers with controller.hasListener or you can signal an error. Be sure to check the API documentation on StreamController.
You can use new StreamController.broadcast() for allowing multiple listeners.
For copy-pasters
Here's a simple way to create a stream (great snippet for copy-pasters):
class Something {
StreamController _onExitController = new StreamController.broadcast();
Stream get onExit => _onExitController.stream;
}
Then the class can just access _onExitController to control the stream (to for example .add()).
In addition to StreamController you can instantiate a Stream directly with one of its named constructors:
Stream.fromFuture() Returns a stream that fires one event (whatever the Future completes to.)
Stream.fromIterable() Returns a stream that converts the Iterable elements to a sequence of events.
Stream.periodic() Returns a stream that fires a computed event periodically.
This is very handy as you can write code that expects to consume a stream, but you have multiple choices as to how to feed events to that class. For example: Stream.fromIterable() could be used in a unit test to fire a known sequence of events to a class that otherwise normally would be fed data events read from a file.
I just created a new Dart library called event_stream to make creating custom events on your classes easier. Here is an example:
class ClassWithEvents implements NotifyPropertyChanged {
String _someProperty;
final EventStream<PropertyChangedEventArgs> _onPropertyChangedEvent = new EventStream<PropertyChangedEventArgs>();
Stream<PropertyChangedEventArgs> get onPropertyChanged => _onPropertyChangedEvent.stream;
final EventStream _onClosedEvent = new EventStream();
Stream get onClosed => _onClosedEvent.stream;
String get someProperty => _someProperty;
set someProperty(String value) {
_onPropertyChangedEvent.signal(new PropertyChangedEventArgs('someProperty', value));
_someProperty = value;
}
close() {
_onClosedEvent.signal();
}
}
main() {
var c = new ClassWithEvents();
c.onPropertyChanged.listen((PropertyChangedEventArgs<String> args) => print('changed: name=${args.propertyName} value=${args.value}'));
c.onClosed.listen((_) => print('closed'));
c.someProperty = "test";
c.close();
}
There is sample with from flutter bloc
Add dependency
rxdart: ^0.27.2
Create stream controller
final _todoStreamController = BehaviorSubject<List>.seeded(const []);
Update when there is a change
Future saveTodo(Todo todo) {
final todos = [..._todoStreamController.value];
final todoIndex = todos.indexWhere((t) => t.id == todo.id);
if (todoIndex >= 0) {
todos[todoIndex] = todo;
} else {
todos.add(todo);
}
_todoStreamController.add(todos);
}
Broadcast
Stream<List> getTodos() => _todoStreamController.asBroadcastStream();
Subscribe
Future _onSubscriptionRequested(
TodosOverviewSubscriptionRequested event,
Emitter emit,
) async {
emit(state.copyWith(status: () => TodosOverviewStatus.loading));
await emit.forEach<List<Todo>>(
_todosRepository.getTodos(),
onData: (todos) => state.copyWith(
status: () => TodosOverviewStatus.success,
todos: () => todos,
),
onError: (_, __) => state.copyWith(
status: () => TodosOverviewStatus.failure,
),
);
}
REF LINK

Resources