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 thecurrentState, nextState
and also theevent
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! Check out the full code here.
Top comments (2)
Hi, how did you create a confirmPassword formz class? How did you implement the logic of checking the two passwords against each other?
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.