TLDR;

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 type Stream<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();
}

Opinions are my own. Stay safe! Stay home!