Delegate to Another Generator Function - Simplify Event Handlers in BLoc 💻
TLDR;
-
Be mindful about the return type when writing
Generator
function in Dart. It’s better to declare the explicit return type for aGenerator
function to avoid dynamically interring types. -
The usage of
yield*
in Dart is the same as JavaScript although Dart Language Tour doesn’t mention howyield*
enables a generator function to delegate to another generator function. It only tells usyield*
can tune the performance of recursive calls of aGenerator
which is incomplete to demonstrate the usage ofyield*
. (Recap:yield*
in JavaScript) -
yield
in Dart can be used insideasync*
functions (async generators) in order to add the result to the resultingStream
that is returned by the function. -
yield*
(yield-each) inserts all the elements of the subsequence into the sequence currently being constructed as if we had an individualyield
for each element.
My Story - BLoc and a misleading Generator docs in Dart Language Tour
👨💻 I came to Flutter and Dart from the world of React, React Native, Redux, JavaScript and TypeScript. The mental model of writing Dart within Flutter is rather easy for me.
After playing around with Flutter and Bloc for a while, I found that most of the tutorials and code snippets available online write lengthy event handlers in a Bloc
. Code readability significantly drops after adding more and more events into mapEventToState
function.
For example,
@override
Stream<LoginState> mapEventToState(LoginEvent event) async* {
if (event is LoginButtonPressed) {
// Start of LoginButtonPressed event handler
yield LoginLoading();
try {
final token = await userRepository.authenticate(
username: event.username,
password: event.password,
);
await userRepository.persistToken(token);
yield LoginSuccess();
} catch (error) {
yield LoginFailure(error: error);
}
// End of LoginButtonPressed event handler
}
if (event is LogoutButtonPressed) {
yield LogoutLoading();
await userRepository.invalidateToken();
authenticateBloc.add(Logout());
yield LogoutInitial();
}
}
In order to simplify it to practise SoC, I thought we only have to extract the piece of event handler logic into another generator function. so this code snippet came to my mind.
@override
Stream<LoginState> mapEventToState(LoginEvent event) async* {
if (event is LoginButtonPressed) {
yield _handleLoginButtonPressed(event);
}
if (event is LogoutButtonPressed) {
yield _handleLogoutButtonPressed(event);
}
}
_handleLoginButtonPressed(LoginButtonPressed event) async* {
yield LoginLoading();
// ...many lines of code
yield LoginInitial();
}
_handleLogoutButtonPressed(LoginButtonPressed event) async* {
yield LogoutLoading();
// ...many lines of code
yield LogoutInitial();
}
As we can tell, the code in mapEventToState
looks neat now. Let’s click the Login Button!
Oops, LoginButtonPressed
and LogoutButtonPressed
event does NOT trigger each handlers anymore.
After thinking awhile and finishing 30 push ups 🏃, I recalled that yield*
was used in JavaScript
to delegate to another generator or iterable object. Therefore, I Googled Dart language tour - our favorite cheat sheet for Dart.
After reading it, I found the good news is that Dart has a yield*
syntax. However, the bad news is that the document only explains yield*
as a performance tuning for recursive generators.
In the entire section about Generator, it doesn’t talk about how to delegate to another generator
function.
If your generator is recursive, you can improve its performance by using yield*:
Iterable<int> naturalsDownFrom(int n) sync* { if (n > 0) { yield n; yield* naturalsDownFrom(n - 1); } }
It doesn’t harm anything if I give it a try although the offical documents doesn’t cover it. So:
@override
Stream<LoginState> mapEventToState(LoginEvent event) async* {
if (event is LoginButtonPressed) {
yield* _handleLoginButtonPressed(event);
}
if (event is LogoutButtonPressed) {
yield* _handleLogoutButtonPressed(event);
}
}
Oops, it throws an error message 🤬
flutter: type
_ControllerStream<dynamic>
is not a subtype of typeStream<LoginState>
Hmmm, I didn’t manage to find any information about it from Google or StackOverflow 🧐. After being frozen for a few minutes and reading the error messages for a few times, the ahuh moment came.
Source code of Dart libraries is easily accessible so we should take advantage of it. The source code of mapStateToEvent
points to some usage of StreamController
.
Additional readings about creating streams from Google.
The source code of StreamController
shows that class member stream
returns _ControllerStream<T>
:
Stream<T> get stream => new _ControllerStream<T>(this);
So, it’s actually a noob mistake caused by my laziness! I didn’t declare the explicit return type of _handleLoginButtonPressed
and _handleLogoutButtonPressed
so Dart is inferring the type dynamically. That’s where _ControllerStream<dynamic>
comes from.
So the fix is quick. We should add Stream<LoginState>
as the return type to avoid letting Dart dynamically infers it during run time.
🚀 Dada! The final working solution:
@override
Stream<LoginState> mapEventToState(LoginEvent event) async* {
if (event is LoginButtonPressed) {
yield* _handleLoginButtonPressed(event);
}
if (event is LogoutButtonPressed) {
yield* _handleLogoutButtonPressed(event);
}
}
Stream<LoginState> _handleLoginButtonPressed(LoginButtonPressed event) async* {
yield LoginLoading();
// ...many lines of code
yield LoginInitial();
}
Stream<LoginState> _handleLogoutButtonPressed(LoginButtonPressed event) async* {
yield LogoutLoading();
// ...many lines of code
yield LogoutInitial();
}
Comments