Mastering Flutter: Using BLoC pattern part 2

Welcome back! Here’s the part 2 of our BLoC journey; in this chapter will see how to setup the Sign Up flow of our app. Instead of using a Cubit as we did in the part 1 of this series, we’ll use the pure BLoC.

Note: In this tutorial not much of the UI will be shown since it’s pretty much a copy of the LoginScaffold and LoginForm shown in the p1; the full example will be linked at the end of this article. Even the SignUpState it’s almost identical to our LoginState but for completeness we’ll insert it in this tutorial.

SignUp State

class SignUpState extends Equatable {
  const SignUpState({
    this.name = const Name.pure(),
    this.email = const Email.pure(),
    this.password = const Password.pure(),
    this.confirmPassword = const ConfirmPassword.pure(),
    this.image,
    this.status = FormzStatus.pure,
  });

  final Name name;
  final Email email;
  final Password password;
  final ConfirmPassword confirmPassword;
  final String image;
  final FormzStatus status;

  @override
  List<Object> get props => [
    image,
    name,
    email,
    password,
    confirmPassword,
    status
  ];

  SignUpState copyWith({
    String image,
    Name name,
    Email email,
    Password password,
    ConfirmPassword confirmPassword,
    FormzStatus status,
  }) {
    return SignUpState(
      name: name ?? this.name,
      email: email ?? this.email,
      password: password ?? this.password,
      confirmPassword: confirmPassword ?? this.confirmPassword,
      image: image ?? this.image,
      status: status ?? this.status,
    );
  }
}

There’s not pretty much more to add about our state so let’s go fast to the main course.

Now that we have our SignUpState defined we need to define the SignUpEvent which our bloc will be reacting to.

SignUp Event

First of all, let’s write our base SignUpEvent, and let’s analyze it:

  • our event is abstract, this means that this class cannot be instantiated and also, declaring it abstract, we ensure that all implementation subclasses define all the properties and methods that abstract class defines.
  • it extends Equatable so we can observe whenever the previous event will be different from the current one and react to this and modify what we need.
abstract class SignUpEvent extends Equatable {
  const SignUpEvent();

  @override
  List<Object> get props => [];
}

Done that we can create all the events that will inform BLoC that something in our sign up has changed, for example that the user inserted a password in our form.

class PasswordChanged extends SignUpEvent {
  const PasswordChanged({
    @required this.password
  });

  final String password;

  @override
  List<Object> get props => [password];
}

Using BLoC might seems overcomplicated sometimes (and it really is 😅) but you can now start to see how clear all the logic is; you will always know what is happening in your app and which state corresponds to our event! Knowing what happens, using just a single component, may be a real life saviour while developing a Flutter app.

Build the SignUp BLoC

This class should handle everything, in this case I’ve included the form field management, and a mock of an API call but this can include all do you need like CRUD operations with the database.

class SignUpBloc extends Bloc<SignUpEvent, SignUpState> {
  SignUpBloc() : super(SignUpState());

  @override
  Stream<SignUpState> mapEventToState(SignUpEvent event) async* {
    if (event is NameChanged) {
      final name = Name.dirty(event.name);
      yield state.copyWith(
        name: name.valid ? name : Name.pure(),
        status: Formz.validate([
          name,
          state.email,
          state.password,
          state.confirmPassword,
        ]),
      );
    } else if (event is EmailChanged) {
      final email = Email.dirty(event.email);
      yield state.copyWith(
        email: email.valid ? email : Email.pure(),
        status: Formz.validate([
          state.name,
          email,
          state.password,
          state.confirmPassword,
        ]),
      );
    } else if (event is PasswordChanged) {
      final password = Password.dirty(event.password);
      final confirm = ConfirmPassword.dirty(
          password: password.value,
          value: state.confirmPassword?.value,
      );
      yield state.copyWith(
        password: password.valid ? password : Password.pure(),
        status: Formz.validate([
          state.name,
          state.email,
          password,
          confirm,
        ]),
      );
    } else if (event is ConfirmPasswordChanged) {
      final password = ConfirmPassword.dirty(
          password: state.password.value,
          value: event.confirmPassword
      );
      yield state.copyWith(
        confirmPassword: password.valid ? password : ConfirmPassword.pure(),
        status: Formz.validate([
          state.name,
          state.email,
          state.password,
          password,
        ]),
      );
    } else if (event is ProfileImageChanged) {
      final String profileImage = event.image;
      yield state.copyWith(
        image: profileImage,
        status: Formz.validate([
          state.name,
          state.email,
          state.password,
          state.confirmPassword,
        ]),
      );
    } else if (event is FormSubmitted) {
      yield _signUp();
    }
  }

  _signUp() async* {
    if (!state.status.isValidated) return;
    yield state.copyWith(status: FormzStatus.submissionInProgress);
    try {
      await Future.delayed(Duration(seconds: 3));
      yield state.copyWith(status: FormzStatus.submissionSuccess);
    } on Exception {
      yield state.copyWith(status: FormzStatus.submissionFailure);
    }
  }
}

As you can see all the business is happening in the mapEventToState function,  where you take an event and map it to a state and deliver it to your UI.

If you want to dig more inside BLoC you can override the following methods:

  • onError you can observe if an error happens
@override
  void onError(Object error, StackTrace stackTrace) {
    // TODO: implement onError
    super.onError(error, stackTrace);
  }
  • onTransition contains the currentState, nextState and also the event which triggered the state change.
@override
  void onTransition(Transition<SignUpEvent, SignUpState> transition) {
    // TODO: implement onTransition
    super.onTransition(transition);
  }

Tip: even if you don’t care about this method try to implement it and print the transition, it will be really helpful while developing.

 

Connecting BLoC to UI

If you have read the previous part of this tutorial you know that we are coming to an end, we just need something that can provide this BLoC to our UI and, of course, you already know the answer… its’ the BlocProvider:

BlocProvider(
   create: (_) => SignUpBloc(),
   child: SignUpForm(),
)

Since we are building a Sign Up form we need to know when user has successfully complete all the process and to know that BlocListener comes in rescue:

class SignUpForm extends StatelessWidget {
  const SignUpForm({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocListener<SignUpBloc, SignUpState>(
      listener: (context, state) {
        if (state.status.isSubmissionFailure) {
          _showAlert();
        } else if (state.status.isSubmissionSuccess) {
          Navigator.of(context).pushNamed('/home');
        }
      },
      child: OurFormWidget()
    );
  }
}

listener is used here to show an error alert if the submission fails or show our Home Screen if everything went good.

class _EmailInputField extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<SignUpBloc, SignUpState>(
      buildWhen: (previous, current) => previous.email != current.email,
      builder: (context, state) {
        return AuthTextField();
      },
    );
  }
}

We wrapped each of our forms inside a BlocBuilder because buildWhen let us know exactly when our email is changing and so we can optimize rebuilding of the widget.

Great but… how is the BLoC aware of changes in my textfields?

onChanged: (email) => context.read<SignUpBloc>().add(EmailChanged(email: email))

In this particular case onChanged callback on typing is notifying our BLoC that, for instance, email is changed and to begin all the flow.

 

The End

This way to implement BLoC is just a drop in the bucket, continue to explore and you will see how many things you can do with this package: you can communicate between blocs, having one bloc that retain all the others… even this part of our journey comes to an end, see you the next time!

Add a Comment

Your email address will not be published. Required fields are marked *