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
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,
);
}
}
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()));
}
}
}
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(),
),
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(),
],
)
);
}
}
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),
);
},
);
}
}
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
);
},
);
}
}
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
Top comments (1)
I am a dumb beginner, so maybe a very dumb question - how do I access the logged in state to establish if the user is logged in or not from another screen, e.g. the home_scaffold.dart?