Improve readability of your BlocListeners

Improve readability of your BlocListeners

Utilize encapsulation to make your widget tree cleaner

·

3 min read

From the bloclibrary

BlocListener is a Flutter widget which takes a BlocWidgetListener and an optional Bloc and invokes the listener in response to state changes in the bloc. It should be used for functionality that needs to occur once per state change such as navigation, showing a SnackBar, showing a Dialog, etc...

listener *is only called once for each state change (*NOT including the initial state) unlike builder in BlocBuilder and is a void function.

If the bloc parameter is omitted, BlocListener will automatically perform a lookup using BlocProvider and the current BuildContext.

BlocListener and MultiBlocListner are two very useful widgets we almost can't do without when using flutter_bloc but they come with some verbosity and the flutter widget tree is unforgiving when it comes to that.

Once you start nesting more widgets, the UI code becomes unreadable and messy. Ideally, you should aim for 150 lines of code per file or widget and extract the rest. I know I usually don't stick to it because I'm in the zone or too lazy to do a refactor but surely you don't, right?

Anyways, I'm here to show you how to reduce these lines of code through encapsulation, you know, one of the four OOP principles? Consider this login_page.dart:

class LoginPage extends StatelessWidget {
  const LoginPage({super.key});

  @override
  Widget build(BuildContext context) {
    return MultiBlocListener(
      listeners: [
        BlocListener<LoginBloc, LoginState>(
          listenWhen: (previous, current) {
            return previous.email != current.email;
          },
          listener: (context, state) {
            // TODO(Henry): Do something when email changes
          },
        ),
        BlocListener<LoginBloc, LoginState>(
          listenWhen: (previous, current) {
            return previous.password != current.password;
          },
          listener: (context, state) {
            // TODO(Henry): Do something when password changes
          },
        ),
      ],
      child: const LoginPageBody(),
    );
  }
}

There are two BlocListener widgets. The first one listens to changes in the email field of the LoginState, and the second one listens to changes in the password field of the LoginState. The listenWhen function is used to determine when to call the listener function. In this case, it's called when the previous state's email or password is not equal to the current state's email or password.

The entire file is just 27 lines of code but you can see how it can quickly increase if you were to do something when either the email or password changes.

We can reduce the LOC to less than half with simple encapsulation like this:

class EmailChangeListener extends BlocListener<LoginBloc, LoginState> {
  EmailChangeListener({super.child})
      : super(
          listenWhen: (previous, current) {
            return previous.email != current.email;
          },
          listener: (context, loginState) {
            // TODO(Henry): Do something when email changes
          },
        );
}
class PasswordChangeListener extends BlocListener<LoginBloc, LoginState> {
  PasswordChangeListener({super.child})
      : super(
          listenWhen: (previous, current) {
            return previous.password != current.password;
          },
          listener: (context, loginState) {
            // TODO(Henry): Do something when password changes
          },
        );
}

By passing the listenWhen and listener to the superclass and inheriting all the other properties and methods of the BlocListener you can extract all relevant code to its class and increase its reusability at the same time. By extending BlocListener, we have our EmailChangeListener and PasswordChangeListener with an optional child parameter.

The child parameter is nullable so that means we can now do this:

class LoginPage extends StatelessWidget {
  const LoginPage({super.key});

  @override
  Widget build(BuildContext context) {
    return MultiBlocListener(
      listeners: [
        EmailChangeListener(),
        PasswordChangeListener(),
      ],
      child: const LoginPageBody(),
    );
  }
}

or this:

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return EmailChangeListener(child: const HomePageBody());
  }
}

Although short, that's all I have for today. You can check out other articles of mine or connect with me on linkedin.

Did you find this article valuable?

Support Henry's blog by becoming a sponsor. Any amount is appreciated!