DEV Community

loading...
Cover image for Mastering Flutter: BLoC pattern for Login: Part 2

Mastering Flutter: BLoC pattern for Login: Part 2

theotherdevs profile image TheOtherDev/s Updated on ・5 min read

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,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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 => [];
}
Enter fullscreen mode Exit fullscreen mode

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];
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
Enter fullscreen mode Exit fullscreen mode
  • onTransition contains the currentState, nextStateand also the eventwhich triggered the state change.
@override
  void onTransition(Transition<SignUpEvent, SignUpState> transition) {
    // TODO: implement onTransition
    super.onTransition(transition);
  }
Enter fullscreen mode Exit fullscreen mode

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(),
)
Enter fullscreen mode Exit fullscreen mode

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()
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
      },
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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! Check out the full code here.

Discussion (2)

Collapse
skhendle profile image
Sandile Khendle

Hi, how did you create a confirmPassword formz class? How did you implement the logic of checking the two passwords against each other?

Collapse
theotherdevs profile image
TheOtherDev/s Author

Hi, you can check out the code here: github.com/Alessandro-v/bloc_login
You can find the implementation under "auth_models".
Sorry we didn't add the Github code on this tutorial.

Forem Open with the Forem app