After closing streamSink, do I still need to cancel listeners?
Found this answer: Dart: Do I have to cancel Stream subscriptions and close StreamSinks?
In general, when you are done listening to that Stream, for any reason, you should close the subscription
So if I don't cancel subscription - they would be kept open forever? Garbage collector would not deal with them?
StreamController<int> controller = StreamController<int>();
Stream stream = controller.stream;
StreamSubscription subscription = stream.listen((value) {
print('Value from controller: $value');
})
..onDone(() => print('done'));
// prints : Value from controller: 1
controller.add(1);
// prints : done
controller.close();
// still listening to closed stream
// no error - cancels successfully
Future.delayed(Duration(seconds: 2), () => subscription.cancel());
Ive tested it today and it is not required to cancel() a StreamSubscription when the controller is closed because controller will emit done when closed and remove all listeners.
Example:
import "dart:async";
void main() {
final _controller = StreamController();
final _listener = _controller.stream.listen((e) => print("Listener $e"), onDone: () => print("onDone"));
_controller.add(1);
_controller.add(2);
_controller.add(3);
_controller.close().then((_) {
print("hasListener ${_controller.hasListener}");
});
}
Output:
Listener 1
Listener 2
Listener 3
onDone
hasListener false
According to the docs:
When the "done" event is fired, subscribers are unsubscribed before receiving the event. After the event has been sent, the stream has no subscribers. Adding new subscribers to a broadcast stream after this point is allowed, but they will just receive a new "done" event as soon as possible.
So closing steam means you will not be able to add further events,
And if you do this:
controller.close();
controller.add(1);
it will throw the error Bad state: Cannot add event after closing
And canceling a subscription means from now onward you don't want to listen to incoming events from the stream.
In your example, if you add an event to steam after 2 seconds. it will not listen.
Future.delayed(Duration(seconds: 7), () {
controller.add(1);
controller.close();
});
what docs say:
/// Closes the stream.
///
/// No further events can be added to a closed stream.
///
/// The returned future is the same future provided by [done].
/// It is completed when the stream listeners is done sending events,
/// This happens either when the done event has been sent,
/// or when the subscriber on a single-subscription stream is canceled.
Future close();
A straight forward answer:
If you close your stream, cancel subscriptions. Otherwise, listeners will keep listening to a stream that will never happen ;)
Related
I am developing an app by combining riverpod and stream. However, I receive the same event twice. To avoid duplicate reception, the stream is listened to in initState. However, a duplicate event occurred.
I checked it by taking breakpoints in debug mode, and I saw that two identical events were raised in streamController almost at the same time.
//This is the code that listens to the stream.
#override
void initState() {
super.initState();
ToiletListViewModel toiletListViewModel =
ref.read(toiletListViewModelProvider.notifier);
TextViewModel textViewModel = ref.read(textViewModelProvider.notifier);
textViewModel.setTexts();
toiletListViewModel.uiEventStream.listen((event) {
event.when(
onLoading: _onLoading,
onError: _onError,
onSuccess: _onSuccess,
);
});
toiletListViewModel.getToiletListLocal();
toiletListViewModel.getToiletListFromRemote();
}
//This is the code that sends an event to the stream
Future getToiletListFromRemote() async {
_uiEventController.add(const ToiletListUiEvent.onLoading());//This event occurs twice at a time.
try {
List<Toilet> results =
await getToiletListFromRemoteUseCase(toiletListPage);
state = [...state, ...results];
_uiEventController.add(const ToiletListUiEvent.onSuccess());
saveToiletList(state);
} catch (e) {
e as DioError;
_uiEventController.add(ToiletListUiEvent.onError(e.message));
}
return state;
}
If I make a mistake and fire the event twice, shouldn't success or fail occur twice as well as loading? In the code above, only the loading event is fired twice with no difference of 1ms.
What could be the cause? I don't know at all. Thank you for your help.
sorry. This was a stupid mistake. toiletListViewModel.getToiletListLocal();
I was calling the same event here.
I have a broadcast stream that I subscribe to it with the timeout:
final s = originBroadcast.timeout(timeout, onTimeout: (sink) => sink.close());
await for (final event in s) {
...
}
The problem is that timeout() creates another stream (s), and when I unsubscribe or timeout appeared, it doesn't cancel the original stream (originBroadcast). Is it possible to do this?
I found a workaround:
final s = originBroadcast.timeout(
timeout,
onTimeout: (sink) => sink.close(),
);
s.listen((event) {...});
Note: Please read edit first.
I want to create a broadcast stream for providing to Stream Builder. And I also have to attach websocket url to it. Using StreamController docs, this is what i've implemented. I have read similar questions but couldn't figure out the problem.
final WebSocketChannel channel = IOWebSocketChannel.connect("ws://192.168.225.220:6969/test");
Class _Sample extends state<Sample>{
#override
void initState() {
super.initState();
mystream = channel.stream;
controller = StreamController<dynamic>.broadcast();
controller.addStream(mystream);
subscription = controller.stream.listen((data) => data, onDone: () => print("Task done") , onError: (error) => error);
}
///widget tree
child: StreamBuilder(
stream: mystream,
builder: (context, snapshot) {
if(snapshot.hasData && !snapshot.hasError){
return Text(snapshot.hasData.toString());
}
return Text("No Data");
}
),
///widget tree
}
This is the error I'm getting
Bad state: Stream has already been listened to.
EDIT:
I realized after #andras pointed out I'm listening to the stream multiple times. I have read the documentation many times and viewed implementations for Stream and Streambuilder but still I am not able to comprehend Streams. For my implementation, I want to send data from a client to server. And multiple clients should be able to listen to this data.Hence, I want to create a broadcast stream so that multiple listeners can listen to a broadcasting client on a websocket channel.
So far this is what I have understood,
I create a websocket channel, this channel provides a single subscription stream where I have provided a url for server communication.
final WebSocketChannel channel = IOWebSocketChannel.connect("ws://192.168.225.220:6969/test");
Now to create a broadcast stream there is a StreamController.broadcast constructor and to attach a source there is addStream() method
void initState(){
controller = StreamController<dynamic>.broadcast();
controller.addStream(channel.stream);
subscription = controller.stream.listen((data) => data, onDone: () => print("Task done") , onError: (error) => error);
}
So I'm listening on controller.stream, now I want to attach this stream to Streambuilder so I can update builder whenever there is data from stream.
I'm echoing the data to same client using
controller.sink.add(data)
Error i get when i try to use the stream.
The following StateError was thrown while handling a gesture:
Bad state: Cannot add new events while doing an addStream
I'm not sure what i'm doing wrong.
The error description seems pretty detailed.
You listen to the stream once in the initState and then StreamBuilder does also listen to the stream you pass into it.
Think about your code a bit and see if broadcast methods fits for your use case if you want to stick to multiple listeners.
Documentation says 'Events must not be added directly to this controller using add, addError, close or addStream, until the returned future is complete.'
https://api.dart.dev/stable/2.12.4/dart-async/StreamController/addStream.html
I think you should use listen to web sockets stream and add events manually instead of using addStream()
I have one class in my application that is adding values to a BehaviorSubject stream:
class MyClassA{
BehaviorSubject<int> _signalSubject = BehaviorSubject();
Stream<int> get signalStream => _signalSubject.stream;
//at some other point in the same class i have:
_signalSubject.add(someIntValue)
}
Now, on another class in my application I have:
class MyClassWidget extends StatefulWidget {
MyClassWidget();
#override
_MyClassWidgetState createState() => _MyClassWidgetState();
}
class _MyClassWidgetState extends State<MyClassWidget>{
MyClassA classA = MyClassA();
... etc
#override
void initState() {
super.initState();
classA.signalStream
.listen((signal) => print("Signal received $signal"));
});
}
}
Is it possible to stop listening to signalStream under certain condition and re-start listening again under another condition?
The stream has the property .takeUntil which, as far as I understand, could be used to stop listening, but how to re-start listening again?
From https://api.flutter.dev/flutter/dart-async/StreamSubscription-class.html, you can see that you have pause and resume methods for StreamSubscription objects.
"
pause([Future resumeSignal]) → void
Request that the stream pauses events until further notice.
resume() → void
Resume after a pause.
"
Also from https://api.flutter.dev/flutter/dart-async/Stream-class.html:
" You listen on a stream to make it start generating events, and to set up listeners that receive the events. When you listen, you receive a StreamSubscription object which is the active object providing the events, and which can be used to stop listening again, or to temporarily pause events from the subscription."
So when you start listening to the stream, It returns a StreamSubscription object. By using that, You can pause and resume it when you want.
Is there any elegant way to map incoming streams from a private api directly inside mapEventToState() without having to create redundant private events in the bloc?
I came with this solution. It's ok with one single stream, but with multiple streams it starts to get a mess. Thanks in advance.
// (don't mind the imports, this is the bloc file)
class ExampleBloc extends Bloc<ExampleEvent, ExampleState> {
final MyPrivateApi api = MyPrivateApi.instance; // singleton
ExampleBloc() {
// api has a stream of booleans
api.myStream.listen((b) {
// if it's true fire this event
if (b) this.add(_MyPrivateEvent());
}
#override
ExampleState get initialState => InitialExampleState();
#override
Stream<ExampleState> mapEventToState(
ExampleEvent event,
) async* {
if (event is _MyPrivateEvent) {
yield SomeState;
}
}
// private Event
class _MyPrivateEvent extends ExampleEvent {
}
As I can see, you can subscribe on event updates in your screen, and push event from screen to Bloc if need some calculations. Code will be more clean.
Your way seems to be the only way works and seems to be used - see this bloc issue: https://github.com/felangel/bloc/issues/112 and this example project: https://github.com/algirdasmac/streams_and_blocs
Just make sure to dispose the subscription that gets returned by api.myStream.listen.
Previous answer
The following DOES NOT work for infinite streams because the generator function will await until the stream finishes. This can only be used for stream the complete fast, like maybe an upload/download progress.
See accepted answers here Dart yield stream events from another stream listener and here Dart/Flutter - "yield" inside a callback function
ExampleBloc() {
_MyInitEvent();
}
#override
Stream<ExampleState> mapEventToState(
ExampleEvent event,
) async* {
if (event is _MyInitEvent) {
await for (bool b in api.myStream) {
if (b) yield SomeState;
}
}
}
Build another block that encapsulate your stream of bytes.
You can make two events (ByteRead and ByteConsume) and two states (ByteWaiting and ByteAvailable).
Byteread and ByteAvailable should have a _byte field for storing data. Your bytebloc has a subscriber listening the stream and every time it reads a byte it fires a ByteRead event.
You should also add to the bloc a consume() method that gives the last byte readed and fires the ByteConsume event.
The two states and events are mutual:
you start in bytewaiting and every time you have a ByteRead you change to ByteAvailable and
every time you have a ByteConsume you change back to ByteWaiting.