DEV Community

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

Mastering Flutter: BLoC pattern for Login: Part 1

TheOtherDev/s
Hey you! Yes, you! We know you’ve stumbled upon our blog while searching for more kittens videos. Well, you’re entering our world dude, so listen up! Trust us! It’ll be worth it.
・3 min read

Handle a login / signup flow can cause some serious headaches with Flutter, using BLoC can make your life much easier and clarify the whole process, so let's dive in this tuto... journey!

First things first, since this isn't a basic tutorial we will take for granted the knowledge of the routes and we've also included a little bit of "validation" with formz package to create reusable models; it's not the purpose of this tutorial to show how this will work, you will see this in the next tutorial. For the login part we've also used, for tutorial purposes, a subset of BLoC (Cubit) so you will see the difference between those two.

Before we dive in let's add to our pubspec.yaml the necessary packages:

equatable: ^2.0.0
flutter_bloc: ^7.0.0
formz: ^0.3.2
Enter fullscreen mode Exit fullscreen mode

Adding the equatable package will only make your life easier but if you want to compare instances of classes "manually" you just need to override "==" and the hashCode.

Login State

Let's start with a class that will contain the status of the form and all the fields states:

class LoginState extends Equatable {
  const LoginState({
    this.email = const Email.pure(),
    this.password = const Password.pure(),
    this.status = FormzStatus.pure,
    this.exceptionError,
  });

  final Email email;
  final Password password;
  final FormzStatus status;
  final String exceptionError;

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

  LoginState copyWith({
    Email email,
    Password password,
    FormzStatus status,
    String error,
  }) {
    return LoginState(
      email: email ?? this.email,
      password: password ?? this.password,
      status: status ?? this.status,
      exceptionError: error ?? this.exceptionError,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Now let's create our LoginCubit, this will be responsible to perform logic such as getting the email and output new states through emit:

class LoginCubit extends Cubit<LoginState> {
  LoginCubit() : super(const LoginState());

  void emailChanged(String value) {
    final email = Email.dirty(value);
    emit(state.copyWith(
      email: email,
      status: Formz.validate([
        email,
        state.password
      ]),
    ));
  }

  void passwordChanged(String value) {
    final password = Password.dirty(value);
    emit(state.copyWith(
      password: password,
      status: Formz.validate([
        state.email,
        password
      ]),
    ));
  }

  Future<void> logInWithCredentials() async {
    if (!state.status.isValidated) return;
    emit(state.copyWith(status: FormzStatus.submissionInProgress));
    try {
      await Future.delayed(Duration(milliseconds: 500));
      emit(state.copyWith(status: FormzStatus.submissionSuccess));
    } on Exception catch (e) {
      emit(state.copyWith(status: FormzStatus.submissionFailure, error: e.toString()));
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

But how can we connect the Cubit to our UI? Here that comes to the rescue the BlocProvider, a widget which provides a bloc to its children using: BlocProvider.of(context)

BlocProvider(
  create: (_) => LoginCubit(),
  child: LoginForm(),
),
Enter fullscreen mode Exit fullscreen mode

Login Form

Since now seems all at his own place it's time to settle down the last piece of our puzzle, the whole UI

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

  @override
  Widget build(BuildContext context) {
    return BlocConsumer<LoginCubit, LoginState>(
        listener: (context, state) {
          if (state.status.isSubmissionFailure) {
            print('submission failure');
          } else if (state.status.isSubmissionSuccess) {
            print('success');
          }
        },
        builder: (context, state) => Stack(
          children: [
            Positioned.fill(
              child: SingleChildScrollView(
                padding: const EdgeInsets.fromLTRB(38.0, 0, 38.0, 8.0),
                child: Container(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    mainAxisAlignment: MainAxisAlignment.start,
                    children: [
                      _WelcomeText(),
                      _EmailInputField(),
                      _PasswordInputField(),
                      _LoginButton(),
                      _SignUpButton(),
                    ],
                  ),
                ),
              ),
            ),
            state.status.isSubmissionInProgress
                ? Positioned(
              child: Align(
                alignment: Alignment.center,
                child: CircularProgressIndicator(),
              ),
            ) : Container(),
          ],
        )
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

To react to the new states emitted by out Cubit we need to wrap our form in a BlocConsumer; now we'll have exposed a listener and a builder.

Listener

Here we will listen for state changes and, for example, show an error in response of an API call or perform Navigation.

Builder

Here we will show the ui reacting to state changes of our Cubit.

UI

Our UI consists of a Column with 5 children but we'll just show 2 widgets to be brief:

class _EmailInputField extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<LoginCubit, LoginState>(
      buildWhen: (previous, current) => previous.email != current.email,
      builder: (context, state) {
        return AuthTextField(
          hint: 'Email',
          key: const Key('loginForm_emailInput_textField'),
          keyboardType: TextInputType.emailAddress,
          error: state.email.error.name,
          onChanged: (email) => context
              .read<LoginCubit>()
              .emailChanged(email),
        );
      },
    );
  }
}
Enter fullscreen mode Exit fullscreen mode
class _LoginButton extends StatelessWidget {
  const _LoginButton({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<LoginCubit, LoginState>(
      buildWhen: (previous, current) => previous.status != current.status,
      builder: (context, state) {
        return CupertinoButton(
            child: Text('Login'),
            onPressed: state.status.isValidated
                ? () => context.read<LoginCubit>().logInWithCredentials()
                : null
        );
      },
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Both widgets are wrapped in a BlocBuilder that's responsible to rebuild these widgets only when the cubit emits new states for their respective evaluated properties, so for example, if the user doesn't type anything in the email field, _EmailInputField won't ever be rebuilt.

The button instead, if all fields are validated, will invoke logInWithCredentials() function that will emit a new status (failure or success) based on the API response.

Part 1 of our journey containing the login flow comes to an end, you can reach this tutorial's code inside this GitHub repository. See you the next time with the Sign Up Flow

Discussion (0)

Forem Open with the Forem app