Email Authentication with Bloc — Flutter | Firebase

Abhishek Doshi
Somnio Software — Flutter Agency
8 min readOct 13, 2022

Integrating flutter_bloc into the existing Email Authentication that was previously made with Firebase Authentication

Oh! Did our awesome Google Crawler get you landed on this article directly? Not a problem! For a better understanding of this article, you can go through our first part!

In the previous article, we saw how we can integrate Email Authentication using Firebase Auth into our Flutter app. But, it’s not over yet! In this article, we will see how we can integrate flutter_bloc into our existing Email Authentication.

Bloc is one of the widely used State Management solutions as it gives us control over a lot of data and how the data is being passed. If you are completely new to bloc, feel free to check our article on flutter_bloc as well.

So in our previous article, we integrated Email Authentication and used Feature-Driven architecture for it, which would look somewhat like this:

Now, let’s create a bloc folder inside login folder and create 3 files; login_bloc.dart, login_event.dart and login_state.dart.

It’s time to separate our business logic from the UI layer!

Let’s create a few event classes based on our needs. We will create 3 classes:

class LoginButtonPressedEvent extends LoginEvent { const LoginButtonPressedEvent(); }

2. LoginEmailChangedEvent: This will be used in the onChanged method of the textfield. So this event will hold the latest value of email.

class LoginEmailChangedEvent extends LoginEvent { const LoginEmailChangedEvent({required this.email}); final String email; @override List<Object> get props => [email]; }

3. LoginPasswordChangedEvent: This will be used in the same way for password.

class LoginPasswordChangedEvent extends LoginEvent { const LoginPasswordChangedEvent({required this.password}); final String password; @override List<Object> get props => [password]; }

Now, let’s create the state class!

login_state.dart

part of 'login_bloc.dart'; enum LoginStatus { success, failure, loading, } class LoginState extends Equatable { const LoginState({ this.message = '', this.status = LoginStatus.loading, this.email = '', this.password = '', }); final String message; final LoginStatus status; final String email; final String password; LoginState copyWith({ String? email, String? password, LoginStatus? status, String? message, }) { return LoginState( email: email ?? this.email, password: password ?? this.password, status: status ?? this.status, message: message ?? this.message, ); } @override List<Object?> get props => [ message, status, email, password, ]; }

Here, we created 1 class, LoginState and enum LoginStatus. Along with this, we created a copyWith method, which basically means that we will be mutating the same state variable for all the status. The main change here is that, if we are not providing any value to the copyWith method, it will be using the value which we provided during creating the object of the state. Hence, the state object will always have some values, and we can update the value whenever we have the latest values!

Now, it’s time for creating the main bloc part!

login_bloc.dart

In the bloc part, we need to handle 3 event classes that we created:

1. on<LoginButtonPressedEvent>(_handleLoginWithEmailAndPasswordEvent)

Future<void> _handleLoginWithEmailAndPasswordEvent( LoginButtonPressedEvent event, Emitter<LoginState> emit, ) async { try { await _authService.signInWithEmailAndPassword( email: state.email, password: state.password, ); emit(state.copyWith(message: 'Success', status: LoginStatus.success)); } catch (e) { emit(state.copyWith(message: e.toString(), status: LoginStatus.failure)); } }

Here, we are just calling the login method from the auth package that we created in the previous article. And if we get success, we emit the state with the new success status and message. On the other hand, if we get any exception or failure, we emit the state with failure status and the exception as the message.

2. on<LoginEmailChangedEvent>(_handleLoginEmailChangedEvent)

Future<void> _handleLoginEmailChangedEvent( LoginEmailChangedEvent event, Emitter<LoginState> emit, ) async { emit(state.copyWith(email: event.email)); }

In this event, we are just emitting the state with new email that we got from the user. This way, whenever we access, we will get the latest version of the email.

3. on<LoginPasswordChangedEvent>(_handleLoginPasswordChangedEvent)

Future<void> _handleLoginPasswordChangedEvent( LoginPasswordChangedEvent event, Emitter<LoginState> emit, ) async { emit(state.copyWith(password: event.password)); }

The way we had for email, same way we are emitting the state for password.

Time to create our bloc files for Signup functionality!

Create a folder for bloc inside signup folder and create the event, state and bloc files into it.

We are going to follow the same approach that we followed for login part!

So, let me show you quickly how all the 3 files should look like for signup bloc.

part of 'signup_bloc.dart'; @immutable abstract class SignupEvent extends Equatable { const SignupEvent(); @override List<Object?> get props => []; } class SignupButtonPressedEvent extends SignupEvent { const SignupButtonPressedEvent(); } class SignupEmailChangedEvent extends SignupEvent { SignupEmailChangedEvent({required this.email}); final String email; @override List<Object> get props => [email]; } class SignupPasswordChangedEvent extends SignupEvent { SignupPasswordChangedEvent({required this.password}); final String password; @override List<Object> get props => [password]; }

signup_state.dart

part of 'signup_bloc.dart'; enum SignupStatus { success, failure, loading, } class SignupState extends Equatable { SignupState({ this.email = '', this.password = '', this.message = '', this.status = SignupStatus.loading, }); final String message; final SignupStatus status; final String email; final String password; SignupState copyWith({ String? email, String? password, SignupStatus? status, String? message, }) { return SignupState( email: email ?? this.email, password: password ?? this.password, status: status ?? this.status, message: message ?? this.message, ); } @override List<Object?> get props => [ message, status, email, password, ]; }

signup_bloc.dart

import 'package:auth_service/auth.dart'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; import 'package:equatable/equatable.dart'; part 'signup_event.dart'; part 'signup_state.dart'; class SignupBloc extends Bloc<SignupEvent, SignupState> { SignupBloc({ required AuthService authService, }) : _authService = authService, super(SignupState()) { on<SignupButtonPressedEvent>(_handleCreateAccountEvent); on<SignupEmailChangedEvent>(_handleSignupEmailChangedEvent); on<SignupPasswordChangedEvent>(_handleSignupPasswordChangedEvent); } final AuthService _authService; Future<void> _handleSignupEmailChangedEvent( SignupEmailChangedEvent event, Emitter<SignupState> emit, ) async { emit(state.copyWith(email: event.email)); } Future<void> _handleSignupPasswordChangedEvent( SignupPasswordChangedEvent event, Emitter<SignupState> emit, ) async { emit(state.copyWith(password: event.password)); } Future<void> _handleCreateAccountEvent( SignupButtonPressedEvent event, Emitter<SignupState> emit, ) async { try { await _authService.createUserWithEmailAndPassword( email: state.email, password: state.password, ); emit(state.copyWith(status: SignupStatus.success)); } catch (e) { emit(state.copyWith(message: e.toString(), status: SignupStatus.failure)); } } }

Just the change here is that we are calling the signup method that we created in the auth package!

Now, let’s use this bloc in our UI!

Firstly, we need to provide this bloc to our widget tree.

If you notice, our bloc constructors take AuthService as the parameter. So, we will have to provide our FirebaseAuthService to the widget tree first. To do this, we can provide it in main.dart!

@override Widget build(BuildContext context) { return MaterialApp( title: 'Material App', home: RepositoryProvider( create: (context) => FirebaseAuthService( authService: FirebaseAuth.instance, ), child: LoginPage(), ), ); }

Here, we have used RepositoryProvider as this is something that we are going to inject as a parameter in our bloc class!

For login, let’s create a new file called login_page.dart which will be just a skeleton class that will provide our LoginBloc to our login_view.dart

import 'package:auth_service/auth.dart'; import 'package:firebase_auth_bloc_example/login/bloc/login_bloc.dart'; import 'package:firebase_auth_bloc_example/login/view/login_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class LoginPage extends StatelessWidget { const LoginPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => LoginBloc( authService: context.read<FirebaseAuthService>(), ), child: LoginView(), ); } }

For Signup, similarly, we can create a file called signup_page.dart

import 'package:firebase_auth_bloc_example/signup/bloc/signup_bloc.dart'; import 'package:firebase_auth_bloc_example/signup/view/signup_view.dart'; import 'package:auth_service/auth.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SignupPage extends StatelessWidget { const SignupPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => SignupBloc( authService: context.read<FirebaseAuthService>(), ), child: SignUpView(), ); } }

Here, we access the FirebaseAuthService using the read method that we have in the bloc library as an extension on context.

Now, let’s do some real stuff! Let’s use this bloc in our UI part!

Login

So, we created an event which would hold our latest version of email for login (LoginEmailChangedEvent). We can add this event in the onChanged method of our TextField

class _LoginEmail extends StatelessWidget { _LoginEmail({ Key? key, }) : super(key: key); @override Widget build(BuildContext context) { return SizedBox( width: MediaQuery.of(context).size.width / 2, child: TextField( onChanged: ((value) { context.read<LoginBloc>().add(LoginEmailChangedEvent(email: value)); }), decoration: const InputDecoration(hintText: 'Email'), ), ); } }

Here, we read the instance of LoginBloc from the widget tree and add the LoginEmailChangedEvent with the latest value of the text field.

Similarly, we can do this for password:

class _LoginPassword extends StatelessWidget { _LoginPassword({ Key? key, }) : super(key: key); @override Widget build(BuildContext context) { return SizedBox( width: MediaQuery.of(context).size.width / 2, child: TextField( onChanged: ((value) { context .read<LoginBloc>() .add(LoginPasswordChangedEvent(password: value)); }), obscureText: true, decoration: const InputDecoration( hintText: 'Password', ), ), ); } }

Now, we can add the LoginButtonPressedEvent in the onPressed method of the button:

class _SubmitButton extends StatelessWidget { _SubmitButton({ Key? key, }) : super(key: key); @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () { context.read<LoginBloc>().add( LoginButtonPressedEvent(), ); }, child: const Text('Login'), ); } }

Finally, we need to wrap our form with BlocListener so that we can listen to our success and failure states:

class LoginView extends StatelessWidget { @override Widget build(BuildContext context) { return BlocListener<LoginBloc, LoginState>( listener: (context, state) { if (state.status == LoginStatus.success) { Navigator.of(context).pushReplacement(Home.route()); } if (state.status == LoginStatus.failure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.message), ), ); } }, child: const _LoginForm(), ); } }

Create Account

Now, we can do the exact same thing to create an account too!

For email:

class _CreateAccountEmail extends StatelessWidget { _CreateAccountEmail({ Key? key, }) : super(key: key); @override Widget build(BuildContext context) { return SizedBox( width: MediaQuery.of(context).size.width / 2, child: TextField( onChanged: (value) => context .read<SignupBloc>() .add(SignupEmailChangedEvent(email: value)), decoration: const InputDecoration(hintText: 'Email'), ), ); } }

For Password:

class _CreateAccountPassword extends StatelessWidget { _CreateAccountPassword({ Key? key, }) : super(key: key); @override Widget build(BuildContext context) { return SizedBox( width: MediaQuery.of(context).size.width / 2, child: TextField( obscureText: true, decoration: const InputDecoration( hintText: 'Password', ), onChanged: (value) => context .read<SignupBloc>() .add(SignupPasswordChangedEvent(password: value)), ), ); } }

For submit button:

class _SubmitButton extends StatelessWidget { _SubmitButton({ Key? key, }) : super(key: key); @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () => context.read<SignupBloc>().add( SignupButtonPressedEvent(), ), child: const Text('Create Account'), ); } }

Bloc Listener part:

class SignUpView extends StatelessWidget { @override Widget build(BuildContext context) { return BlocListener<SignupBloc, SignupState>( listener: (context, state) { if (state.status == SignupStatus.success) { Navigator.of(context).pushReplacement(Home.route()); } if (state.status == SignupStatus.failure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.message), ), ); } }, child: const _SignupForm(), ); } }

And that’s it!

We have successfully integrated bloc to our Email Authentication.

I hope you learned something new today!

Would you like to try it by yourself? Feel free to check out our GitHub Repository and give it a go!

Originally published at https://somniosoftware.com.

--

--