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.
Related
I was going though GSkinner's flutter_vignattes codebase, in one of the functions there was an empty await for a Future
Future<void> _reset() async {
// Wait until next event loop to advance animation and call setState or flutter will yell at you
await Future<void>.value();
_controller.forward(from: 1.0 - _percentage * 0.83);
if (_isLoading) {
setState(() {
_model = BasketballGameModel.randomize();
});
}
_isLoading = false;
}
I understand how promises are sent to micro-task queue in JS (assuming same happens in Dart), but not quite able to understand the reason provided in the comment here i.e.,
// Wait until next event loop to advance animation and call setState or flutter will yell at you
Really appreciate if someone can provide a deeper insight into this. This is the particular line in codebase i am referring to.
https://github.com/gskinnerTeam/flutter_vignettes/blob/0ccc72c5b87b5ab6ba2dee9eff76f48ce2fadec8/vignettes/basketball_ptr/lib/demo.dart#L149
Future<void> function() {}
Defines an asynchronous function that ultimately returns nothing but can notify callers when it eventually completes. Also see: What's the difference between returning void vs returning Future?
Or You can learn from this https://github.com/dart-lang/sdk/issues/33415
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 ;)
My app structure is a little bit mess, but I have to add this patch first and then I'll restructure the entire logic. The thing is I first check if there's a firebase user, then if there is one I use StreamBuilder to get the current user profile from Firestore, then I have the _firebaseMessaging.configure method because onLaunch and onResume I use this callback:
void _navigateToGestorResevas(Map<String, dynamic> message, User currentUser) {
Navigator.push(context,
MaterialPageRoute(builder: (context) =>
GestorScreen(user: currentUser)));
}
Because I need to send the User to this screen where he fetch the message from firebase.
onResume this works fine, but onLaunch it goes to the screen and fetch the data but there are like 20 seconds where there are some kind of glitch. It switch like 20-30 times between two states where I have and no have snapshot data in this _initState func:
final snapshot = await _dbRef.child('mensajes').child(widget.user.id).once();
if (snapshot.value != null) {
setState(() {
hayMensajes = true;
});
final data = snapshot.value;
for (var entry in data.entries) {
Message message = Message.fromJson(entry.value);
setState(() {
message.add(message);
});
}
} else {
setState(() {
hayMensajes = false;
});
}
Anyone have an idea what am I doing wrong?
If I am not mistaken, there are some active issues about FCM onLaunch callback with flutter. Some of them are still not fixed. One of the problems most people had to face was that onLaunch callback being called multiple times. I don't know why it happened, but as in your case, you can possibly get rid of the issue by some temporary fixes.
If the same screen is getting pushed over and over again, and glitching, you can pop the stack until it reaches the one you meant to open and set a condition to push navigator only if the new route is different from the old one. Using the named routes,
Navigator.popUntil(context, ModalRoute.withName(routeName));
if (ModalRoute.of(context).settings.name != routeName) {
Navigator.pushNamed(context, routeName);
}
I am not sure if that was the problem you asked, but I hope at least my answer helps somehow.
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.
I'm developing a Flutter app which has some tabs inside, each of them depend on the database that is loaded on the first run. State is stored in a ScopedModel.
On every tab I have this code:
#override
void initState() {
super.initState();
loadData();
}
void loadData() async {
await MyModel.of(context).scopedLoadData();
_onCall = MyModel.of(context).onCall;
setState(() {
});
}
And this is the code snippet that matters for the ScopedModel:
Future<Null> scopedLoadData() async {
if (_isLoading) return;
_isLoading = true;
(...)
_isLoading = false;
}
If the user waits on the first tab for a few seconds everything is fine, since Database is loaded. However, if the user switches tabs right after app launch, the method scopedLoadData is still being executed so that I get a runtime error ("Unhandled Exception: NoSuchMethodError: The method 'ancestorWidgetOfExactType' was called on null.").
This exception happens because the scopedLoadData has not yet been completed. So I'm looking for a way to wait for a method that is still being executed.
Thanks!
Not sure without seeing your build method but I would start your build method with a guard clause.
if (_oncall == null) return Container(); // or Text("loading") or similar
use should be using a FutureBuilder on each tab to make sure the data is loaded before you try to build the widget... more code would be helpful
I solved the exception by getting rid of every:
setState(() { });
and implementing ScopedModelDescendant on every relevant tab on top of using notifyListeners() at the end of the database loading method.
This pulls the responsibility from the tabs for updating views and gives it to the scopedLoadData().