I was wondering why mapEventToState
returns a Stream
and not a Future
.
Isn't the expectation that every mapEventToState
returns just one new State
?
I think if people were to emit multiple states here they would interfere with the next invocation etc.
Or is there a special advantage of "Streams of 1" over Future that I am not aware of?
@tp great question! While it first it may seem strange that mapEventToState
returns a Stream
rather than a Future
it's very common for a single event to cause multiple state changes.
Imagine a scenario where a user taps a button (Login Button for example) and we need to make an asynchronous request in order to get our new state. Since mapEventToState
returns a Stream
, we can yield
an initial LoadingState
to show the user that something is happening while we make the async request and then we can update the state with a success/failure once the async request completes.
if (event is FetchData) {
yield LoadingState();
try {
final data = await repository.getData();
yield SuccessState(data);
} catch (error) {
yield ErrorState(error);
}
}
Emitting multiple states per event does not interfere with the next invocation because every event is processes in the order in which it was dispatched. Let me know if that helps and again very good question! 馃憤
@felangel Thanks for the quick & detailed response.
Emitting multiple states per event does not interfere with the next invocation because every event is processes in the order in which it was dispatched.
Is the previous stream disconnected when a new event comes it? Or could it be that a previous invocation of mapEventToState
still writes and updates the stream while a more recent event is processing as well?
@tp no problem! Streams are updated in the order in which the events are dispatched.
I think if you take a look at this example it will be more clear:
enum CounterEvent { increment, decrement }
class CounterBloc extends Bloc<CounterEvent, int> {
@override
int get initialState => 0;
@override
void onTransition(Transition<CounterEvent, int> transition) {
print(transition);
}
@override
Stream<int> mapEventToState(int currentState, CounterEvent event) async* {
switch (event) {
case CounterEvent.decrement:
// Simulating Network Latency
await Future<void>.delayed(Duration(seconds: 1));
yield currentState - 1;
break;
case CounterEvent.increment:
// Simulating Network Latency
await Future<void>.delayed(Duration(milliseconds: 500));
yield currentState + 1;
break;
}
}
}
void main() {
final counterBloc = CounterBloc();
counterBloc.dispatch(CounterEvent.increment);
counterBloc.dispatch(CounterEvent.increment);
counterBloc.dispatch(CounterEvent.increment);
counterBloc.dispatch(CounterEvent.decrement);
counterBloc.dispatch(CounterEvent.decrement);
counterBloc.dispatch(CounterEvent.decrement);
}
The order or state changes is:
Transition { currentState: 0, event: CounterEvent.increment, nextState: 1 }
Transition { currentState: 1, event: CounterEvent.increment, nextState: 2 }
Transition { currentState: 2, event: CounterEvent.increment, nextState: 3 }
Transition { currentState: 3, event: CounterEvent.decrement, nextState: 2 }
Transition { currentState: 2, event: CounterEvent.decrement, nextState: 1 }
Transition { currentState: 1, event: CounterEvent.decrement, nextState: 0 }
Does that clarify things? Thanks!
Thanks for the example.
I was wondering whether there is any interference, if the decrements (with longer delays) where dispatched first, and whether the shorter increments would overrun them. But that does not seem to be the case :)
flutter: Transition { currentState: 0, event: CounterEvent.decrement, nextState: -1 }
flutter: Transition { currentState: -1, event: CounterEvent.decrement, nextState: -2 }
flutter: Transition { currentState: -2, event: CounterEvent.decrement, nextState: -3 }
flutter: Transition { currentState: -3, event: CounterEvent.increment, nextState: -2 }
flutter: Transition { currentState: -2, event: CounterEvent.increment, nextState: -1 }
flutter: Transition { currentState: -1, event: CounterEvent.increment, nextState: 0 }
So just that I get my mental model correct: For each new event
passed to mapEventToState
it runs the stream / function to completion, and only then processes the next event, right?
If that is true, there is no way to receive a new event while one is still processing, right?
So for example with your above example of blocking the UI until a network call succeeded, there is no way for the BLoC
to handle other events fired in the system, right?
Added some more logging to verify that no multiple mapEventToState
are run at the same time.
Seems to work as I now understood from your explanation into my model above.
flutter: CounterEvent.decrement
flutter: Transition { currentState: 0, event: CounterEvent.decrement, nextState: -1 }
flutter: CounterEvent.decrement
flutter: Transition { currentState: -1, event: CounterEvent.decrement, nextState: -2 }
flutter: CounterEvent.decrement
flutter: Transition { currentState: -2, event: CounterEvent.decrement, nextState: -3 }
flutter: CounterEvent.increment
flutter: Transition { currentState: -3, event: CounterEvent.increment, nextState: -2 }
flutter: CounterEvent.increment
flutter: Transition { currentState: -2, event: CounterEvent.increment, nextState: -1 }
flutter: CounterEvent.increment
flutter: Transition { currentState: -1, event: CounterEvent.increment, nextState: 0 }
I think this all stems from asyncExpand
here https://github.com/felangel/bloc/blob/5fa7c06d6390d53d874b14aee0df74a543406889/packages/bloc/lib/src/bloc.dart#L67 right?
@tp yup that's correct 馃憤
Do you have any outstanding questions/concerns?
Closing for now but feel free to comment with additional questions or concerns and I'll reopen this 馃槃
@felangel Nope, this clarified all my open questions. Thanks for taking the time :)
I'm glad I ran into this thread as it is very helpful. It would be awesome to include information about how one event can return multiple states in the documentation.
In the official Firestore todo example, the loadTodosEvent is handled by this function
Stream<TodosState> _mapLoadTodosToState() async* {
_todosSubscription?.cancel();
_todosSubscription = _todosRepository.todos().listen(
(todos) {
dispatch(
TodosUpdated(todos),
);
},
);
}
I was trying to understand if yield* could be used here instead but if my understanding is correct, this is a bad idea as this would disallow the bloc from handling other events while the repository is still streaming data.
Hi @alanwguo 馃憢
I鈥檒l be sure to include that in the core concepts doc later today 馃憤
Regarding your second question, you are correct. Using yield* in mapEventToState would block the processing of all incoming events.
Thanks for confirming that!
Most helpful comment
@tp great question! While it first it may seem strange that
mapEventToState
returns aStream
rather than aFuture
it's very common for a single event to cause multiple state changes.Imagine a scenario where a user taps a button (Login Button for example) and we need to make an asynchronous request in order to get our new state. Since
mapEventToState
returns aStream
, we canyield
an initialLoadingState
to show the user that something is happening while we make the async request and then we can update the state with a success/failure once the async request completes.Emitting multiple states per event does not interfere with the next invocation because every event is processes in the order in which it was dispatched. Let me know if that helps and again very good question! 馃憤