One of the crucial parts of mobile app development is to perform authentication. It ensures that only authorized users will get the crucial information and perform tasks in the application. This article is all about Authentication With Firebase.
In this tutorial, we will explore how to perform secure authentication with Firebase in Flutter Application. Along with it, we will explore the BloC state management to handle the state of the application. By the end, you’ll have a solid concept and understanding of how to execute a firm authentication perform a secure sign-up process, and log in with Bloc.
Prerequisites:
To get the most out of this tutorial, you should have the following:
- A good understanding of Flutter and Dart
- A Firebase account: Create a Firebase account if you don’t have one. You can set up a Firebase project through this link.
What You’ll Build
In this article, you’ll build and Flutter application that allows users to log in or register using social IDs like Google, or a set of credentials, such as email and password. Your application will have the home screen and logout button.
Working Mechanism of Firebase Authentication
Firebase Authentication is a great service with simplifies the process of authentication in your application. It supports multiple authentication methods, including Google, social media, email/password, and many more.
One of the main advantages of Firebase Authentication is its built-in security features like secure storage of user credentials as well as the encryption of sensitive information.
Flow chart
The image above is the flow chart to understand how it is going to work.
- When the app is started, the Splash screen will appear. Here, we check if the user is logged in or not.
- If the user is logged in, then we will redirect the user to the Home Screen.
- Otherwise, if the user is not logged in, then we will redirect the user to the Auth Screen (Register/Login Screen).
- Again when the user is registered or logged in, the user will be redirected to the Home Screen.
So this is going to be the simple mechanism, of how the application would behave.
Bloc State management
As you can see in the picture above.
- First, we have the UI, and from the UI we request the bloc.
- Bloc will have two things, the event and the state. First, when the UI connects to a bloc it creates and triggers events.
- The event eventually calls the repositories to the server through an endpoint.
- From the server now we get the data and it is passed back to the bloc. As we have the data we trigger the state.
- As we have the change in the state UI knows from the Bloc pattern and updates the UI.
If you want to learn more about BloC visit here.
Let’s Dive In
To get started with Firebase Authentication, you must set up Firebase in your Flutter project.
Setup Firebase
Follow these instructions to set up Firebase in your projects.
Import Packages
Find the pubsec.yaml
file, add the following dependencies.
dependencies:
...
flutter_bloc: ^8.1.3
equatable: ^2.0.5
google_sign_in: ^6.2.1
email_validator: ^2.1.17
Initialise Firebase
First, open your main.dart
file inside the lib
folder, add
To initialize Firebase, add the following code:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
runApp(const MyApp());
}
Set UpFirebase Authentication Bloc
A bloc contains events, states, and blocs. Firstly, let’s create states and events for the Bloc. After creating states and events, we’ll create an AuthenticationBloc which will operate the logic by using the events, states, and services we have created.
The AuthState class
This AuthState class is responsible for holding the state of the Authentication process. As in the code, there are loading, success, and failure states. These states ensure we know what is happening during the authentication process.
First, create an auth_state.dart
file in your project’s bloc/auth
directory.
@immutable
abstract class AuthState extends Equatable {}
class Loading extends AuthState {
@override
List<Object?> get props => [];
}
class Authenticated extends AuthState {
@override
List<Object?> get props => [];
}
class UnAuthenticated extends AuthState {
@override
List<Object?> get props => [];
}
class AuthError extends AuthState {
final String error;
AuthError(this.error);
@override
List<Object?> get props => [error];
}
Breaking Down Code
AuthState
abstract class:
AuthState
is the base class for multiple states where the authentication process can happen at any time.- All classes are state.
There are multiple states.
Loading
:Loading
State holds the information that the user authentication process is going on.
Authenticated
:- Authenticated State holds the information that the user authentication is successful.
UnAuthenticated
:- UnAuthenticated State holds the information that the user is not logged in.
AuthError
:- AuthError State holds the information of the errors that occurred during the registration or login process.
AuthEvent
abstract class:
The AuthEvent
is responsible for the events the AuthBlog
performs.
Create auth_event.dart
the class file inside your project’s bloc/auth
directory.
import 'package:equatable/equatable.dart';
abstract class AuthEvent extends Equatable {
@override
List<Object> get props => [];
}
class SignInRequested extends AuthEvent {
final String email;
final String password;
SignInRequested(this.email, this.password);
}
class SignUpRequested extends AuthEvent {
final String email;
final String password;
SignUpRequested(this.email, this.password);
}
class GoogleSignInRequested extends AuthEvent {}
class SignOutRequested extends AuthEvent {}
In the code above there are multiple events the AuthBloc will perform.
SignInRequested
- This event will get triggered when the user requests to register the account. It takes two string parameters.
email
andpassword
.
- This event will get triggered when the user requests to register the account. It takes two string parameters.
SignUpRequested
- It will get fired when the user tries to log in with an email and password.
GoogleSignInRequested
- When the user tries to log in with a Google account, this event triggers.
SignOutRequested
- When the user tries to log out from the system.
With every event, AuthRepository
is called to perform requested task.
AuthBloc
abstract class:
This AuthBloc
will handle the overall activity, from what happens when the user clicks a button to what is displayed on the screen. It also communicates with the Firebase service we created.
Create auth_bloc.dart
file inside your project’s bloc/auth
directory.
Add the following code to define the AuthBloc
class.
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final AuthRepository authRepository;
AuthBloc({required this.authRepository}) : super(UnAuthenticated()) {
on<SignUpRequested>((event, emit) async {
emit(Loading());
try {
await authRepository.signUp(
email: event.email, password: event.password);
emit(Authenticated());
} catch (e) {
emit(AuthError(e.toString()));
emit(UnAuthenticated());
}
});
on<SignInRequested>((event, emit) async {
emit(Loading());
try {
await authRepository.signIn(
email: event.email, password: event.password);
emit(Authenticated());
} catch (e) {
emit(AuthError(e.toString()));
emit(UnAuthenticated());
}
});
on<GoogleSignInRequested>((event, emit) async {
emit(Loading());
try {
await authRepository.signInWithGoogle();
emit(Authenticated());
} catch (e) {
emit(AuthError(e.toString()));
emit(UnAuthenticated());
}
});
on<SignOutRequested>((event, emit) async {
emit(Loading());
await authRepository.signOut();
emit(UnAuthenticated());
});
}
}
In this code snippet, we have created an instance AuthRepository
class, that handles user authentication tasks like signing up and signing out.
on<SignUpRequested>((event, emit) async {...}
defines a handler for the SignUpRequested
event. When this event gets triggered, the bloc
passes through the following steps:
- It emits a
Loading
State to indicate that the authentication process is going on. - It calls for the
signUp
method ofauthRepository
to attempt to create a user account with the provided email and password. - If the user account creation is successful, it emits an
Authenticated
State. - If the user account creation fails, it emits an
AuthError
state with an error message followed byUnAuthenticated
State.
All the process goes through the same states.
At the end on<SignOutRequested>((event, emit) async {…} define a handler for the SignOutRequested
event. When this event gets triggered, the bloc
passes through the following steps:
- It emits a
Loading
State to indicate that the logout process is going on. - It calls for the
signOut
method ofauthRepository
attempting to sign out from the account. - If the user account signout success, it emits an
UnAuthenticated
State.
How to Implement the Authentication With Firebase with Bloc
First, let’s create a file named signup_page.dart
inside lib/pages
the folder. Create a StatefulWidget
named SignUp
.
class SignUp extends StatefulWidget {
const SignUp({Key? key}) : super(key: key);
@override
State<SignUp> createState() => _SignUpState();
}
class _SignUpState extends State<SignUp> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("SignUp"),
),
body: StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
// if (snapshot.hasData) {
print("User Data email");
print(snapshot.data!.email);
// }
return BlocConsumer<AuthBloc, AuthState>(
listener: (context, state) {
if (state is Authenticated) {
// Navigating to the dashboard screen if the user is authenticated
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => const HomePage(),
),
);
}
if (state is AuthError) {
// Displaying the error message if the user is not authenticated
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(state.error)));
}
},
builder: (context, state) {
if (state is Loading) {
// Displaying the loading indicator while the user is signing up
return const Center(child: CircularProgressIndicator());
}
if (state is UnAuthenticated) {
// Displaying the sign up form if the user is not authenticated
return Center(
child: Padding(
padding: const EdgeInsets.all(18.0),
child: SingleChildScrollView(
reverse: true,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
"Sign Up",
style: TextStyle(
fontSize: 38,
fontWeight: FontWeight.bold,
),
),
const SizedBox(
height: 18,
),
Center(
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
hintText: "Email",
border: OutlineInputBorder(),
),
autovalidateMode:
AutovalidateMode.onUserInteraction,
validator: (value) {
return value != null &&
!EmailValidator.validate(value)
? 'Enter a valid email'
: null;
},
),
const SizedBox(
height: 10,
),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(
hintText: "Password",
border: OutlineInputBorder(),
),
autovalidateMode:
AutovalidateMode.onUserInteraction,
validator: (value) {
return value != null && value.length < 6
? "Enter min. 6 characters"
: null;
},
),
const SizedBox(
height: 12,
),
SizedBox(
width: MediaQuery.of(context).size.width *
0.7,
child: ElevatedButton(
onPressed: () {
_createAccountWithEmailAndPassword(
context);
},
child: const Text('Sign Up'),
),
)
],
),
),
),
const Text("Already have an account?"),
OutlinedButton(
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const SignIn()),
);
},
child: const Text("Sign In"),
),
const Text("Or"),
InkWell(
onTap: () {
_authenticateWithGoogle(context);
},
child: Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Image.asset(
"assets/google.png",
height: 30,
width: 30,
),
const SizedBox(
width: 10,
),
const Text("Continue with Google")
]),
),
),
),
],
),
),
),
);
}
return Container();
},
);
}),
);
}
void _createAccountWithEmailAndPassword(BuildContext context) {
if (_formKey.currentState!.validate()) {
BlocProvider.of<AuthBloc>(context).add(
SignUpRequested(
_emailController.text,
_passwordController.text,
),
);
}
}
void _authenticateWithGoogle(context) {
BlocProvider.of<AuthBloc>(context).add(
GoogleSignInRequested(),
);
}
}
So this code creates a simple SignUp screen that consists of two textfields
email and password. It also consists of two ElevatedButton
. The BlocConsumer
widget wraps the body items and listens for AuthBloc
state changes. When a user clicks the button, it triggers an event to AuthBloc to start the user registration process.
- If the state is
Authenticated
, we navigate to theHomePage
. - If the state is
AuthError
, we display aSnackBar
with an error message. - If the state is
Loading
, we showCircularProgressIndicator
. - If the state is
UnAuthenticated
, theSignUp
screen elements are shown. - Else we are going to return an empty
Container
.
builder: Builds the UI based on the current state received from the AuthBloc
. As per the authentication state, this button may display different feedback, navigate to another screen, or show different views.
void _createAccountWithEmailAndPassword(BuildContext context) {
if (_formKey.currentState!.validate()) {
BlocProvider.of<AuthBloc>(context).add(
SignUpRequested(
_emailController.text,
_passwordController.text,
),
);
}
}
_createAccountWithEmailAndPassword
this method is called when a user presses the Sign Up
button. It takes a parameter BuildContext
.
if (_formKey.currentState!.validate()) {}
this means if the email and password fields are not empty we are going to call the AuthBloc‘s SignUpRequested
with the inputs of email and password textfields.
validator: (value) {
return value != null &&
!EmailValidator.validate(value)
? 'Enter a valid email'
: null;
}
validator: (value) {
return value != null && value.length < 6
? "Enter min. 6 characters"
: null;
},
If the email and password fields are empty, then this validator
comes to an action. It will display the error messages if certain defined criteria are not fulfilled.
void _authenticateWithGoogle(context) {
BlocProvider.of<AuthBloc>(context).add(
GoogleSignInRequested(),
);
}
void _authenticateWithGoogle(context){...}
this method is called when the user presses the Continue with Google
button.
To make the Firebase accepting Email Password authentication and Google login you need to go to your Firebase Console and select your project. Inside the project find Authentication.
Go to Sign-in method.
- Click on Add new Provider. Enable Email/Password and Google.
LoginPage
Let’s create a file named signin_page.dart
inside lib/pages
the folder. Create a StatefulWidget
named SignIn
.
class SignIn extends StatefulWidget {
const SignIn({Key? key}) : super(key: key);
@override
State<SignIn> createState() => _SignInState();
}
class _SignInState extends State<SignIn> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("SignIn"),
),
body: BlocListener<AuthBloc, AuthState>(
listener: (context, state) {
if (state is Authenticated) {
// Navigating to the dashboard screen if the user is authenticated
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (context) => const HomePage()));
}
if (state is AuthError) {
// Showing the error message if the user has entered invalid credentials
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(state.error)));
}
},
child: BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is Loading) {
// Showing the loading indicator while the user is signing in
return const Center(
child: CircularProgressIndicator(),
);
}
if (state is UnAuthenticated) {
// Showing the sign in form if the user is not authenticated
return Center(
child: Padding(
padding: const EdgeInsets.all(18.0),
child: SingleChildScrollView(
reverse: true,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
"Sign In",
style: TextStyle(
fontSize: 38,
fontWeight: FontWeight.bold,
),
),
const SizedBox(
height: 18,
),
Center(
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
keyboardType: TextInputType.emailAddress,
controller: _emailController,
decoration: const InputDecoration(
hintText: "Email",
border: OutlineInputBorder(),
),
autovalidateMode:
AutovalidateMode.onUserInteraction,
validator: (value) {
return value != null &&
!EmailValidator.validate(value)
? 'Enter a valid email'
: null;
},
),
const SizedBox(
height: 10,
),
TextFormField(
keyboardType: TextInputType.text,
controller: _passwordController,
decoration: const InputDecoration(
hintText: "Password",
border: OutlineInputBorder(),
),
autovalidateMode:
AutovalidateMode.onUserInteraction,
validator: (value) {
return value != null && value.length < 6
? "Enter min. 6 characters"
: null;
},
),
const SizedBox(
height: 12,
),
SizedBox(
width:
MediaQuery.of(context).size.width * 0.7,
child: ElevatedButton(
onPressed: () {
_authenticateWithEmailAndPassword(
context);
},
child: const Text('Sign In'),
),
)
],
),
),
),
const SizedBox(height: 10,),
InkWell(
onTap: () {
_authenticateWithGoogle(context);
},
child: Card(child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
"assets/google.png",
height: 30,
width: 30,
),
const SizedBox(width: 10,),
const Text("Continue with Google")
]),
),),
),
const SizedBox(height: 10,),
const Text("Don't have an account?"),
const SizedBox(height: 10,),
OutlinedButton(
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const SignUp()),
);
},
child: const Text("Sign Up"),
)
],
),
),
),
);
}
return Container();
},
),
),
);
}
void _authenticateWithEmailAndPassword(context) {
if (_formKey.currentState!.validate()) {
BlocProvider.of<AuthBloc>(context).add(
SignInRequested(_emailController.text, _passwordController.text),
);
}
}
void _authenticateWithGoogle(context) {
BlocProvider.of<AuthBloc>(context).add(
GoogleSignInRequested(),
);
}
}
So, this returns this screen.
As similar to SignUp, works similarly.
Opposite of SignUp, when a user presses the button, it triggers this method:
void _authenticateWithEmailAndPassword(context) {
if (_formKey.currentState!.validate()) {
BlocProvider.of<AuthBloc>(context).add(
SignInRequested(_emailController.text, _passwordController.text),
);
}
}
This method requests for SignInRequested with email and password.
HomePage
Let’s create a file named homepage.dart
inside lib/pages
the folder. Create a StatefulWidget
named HomePage
.
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Firestore'),
actions: [
InkWell(
onTap: () {
context.read<AuthBloc>().add(SignOutRequested());
},
child: const Text("Logout"))
],
),
body: Container()
);
}
}
In the AppBar you can see there is a action Text widget named Logout, when clicked will call the SignOutRequested from AuthBloc.And user will be logged out the system.
To implement the authentication flow and continue user process with the state, inside the main.dart
, you have MyApp
Stateless widget.
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return RepositoryProvider(
create: (context) => AuthRepository(),
child: MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => AuthBloc(
authRepository:
RepositoryProvider.of<AuthRepository>(context),
)),
],
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
// If the snapshot has user data, then they're already signed in. So Navigating to the Dashboard.
if (snapshot.hasData) {
return const HomePage();
}
// Otherwise, they're not signed in. Show the sign in page.
return const SignIn();
}),
),
),
);
}
}
In the code above, you have MyApp
StatelessWidget with a RepositoryProvider
where we create the AuthRepository
. RepositoryProvider
consists of a child MultiBlocProvider
where we register the AuthBloc
. And the child of
has a home with an MultiBlocProvider
MaterialAppStreamBuilder
as the child. The StreamBuilder
acts as a judge, using Firebase to check the state changes and if a user has logged in or not. If a user has logged in, it directs them to the home screen, else it goes to the sign-up screen.
Run
When completed you can now run your Flutter app and test the registration, login, and logout functionalities. If you want to clone the repo, you can check it out on GitHub here and leave a like.
Conclusion
In this article, we explored building a user authentication flow in Flutter using Firebase for authentication and the Bloc state management pattern for handling application state.
We learned how to set up Firebase in a Flutter project, create Blocs for authentication, and implement the authentication flow using Bloc.
By leveraging the power of Firebase and the predictability of Bloc, you can ensure a secure and seamless user authentication experience in your Flutter apps.
Thanks for reading this article.
Also, follow to get updated on exciting articles and projects.
If I got something wrong? Let me know in the comments. I would love to improve.
Let’s get connected
We can be friends. Find on Facebook, Linkedin, Github, YouTube,
BuyMeACoffee, and Instagram.
Contribute: BuyMeACoffee
Contact: Contact Us
Leave a Reply