Firebase Email/Password Authentication: An In-Depth Guide for Android Developers using Kotlin
Continuing from our previous blog, where we discussed the basics of Firebase and the introduction to Firebase authentication, we now dive deeper into the implementation of Firebase email and password authentication in your Android app using Kotlin. Follow along with the GitHub project.
In this comprehensive guide, we'll show you step-by-step how to set up Firebase email and password authentication in your Android app, building upon the foundation laid in our previous blog. From creating a Firebase project to integrating the authentication process, we'll provide you with all the information and best practices you need to make your app's login process seamless and secure.
How firebase handles authentication?
Firebase ensures the identity of a user using various secure methods such as email/password, phone number, or third-party providers such as Google or Facebook. The email authentication process involves the following steps:
The user provides their credentials (such as email and password) to Firebase.
Firebase securely transmits the credentials to its servers for verification.
Firebase checks the provided credentials against its database of registered users.
If the provided credentials match an existing user, Firebase generates a secure token and sends it back to the app.
The app stores the token locally and sends it with each subsequent request to Firebase.
Firebase verifies the token on each request to ensure that the user is still authenticated.
If the token is valid, Firebase grants the user access to its resources.
Understanding the firebase authentication token
The secure token generated by Firebase during the authentication process is stored on the client side, typically in the device's local storage or memory Firebase provides JSON Web Tokens (JWTs) for authentication. JWTs are a compact and self-contained way to represent claims to be transferred between two parties, such as between a client and a server. In the context of Firebase authentication, the JWT contains information about the user and the authentication session, such as the user ID, the issued and expiration timestamps, and a signature that verifies that the token was generated by Firebase. Later in this guide, we'll dive into accessing this valuable token in your app.
From Theory to Practice
Before diving into Firebase authentication in your Android app, there are a few things you need to have in place. These pre-configurations will ensure a smooth integration of authentication into your app, and make the authentication process quick and hassle-free.
Required Dependencies
Adding Firebase Authentication and lifecycle-ktx dependencies at your app-level build.gradle
file.
plugins {
...
id 'com.google.gms.google-services'
}
dependencies {
...
// Firebase Authentication Dependency
implementation 'com.google.firebase:firebase-auth-ktx:21.1.0'
// Lifecycle Dependency
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
}
In the buildscript
section of your project-level build.gradle
file, you'll need to add the following dependency.
buildscript {
dependencies {
classpath 'com.google.gms:google-services:4.3.15'
}
}
Utility classes
- The
Resource.kt
class helps you manage the authentication response by keeping track of the request status, data, and error messages. It simplifies the display of relevant information to the user during the authentication process. It acts as a wrapper class for the network response.
enum class Status {
SUCCESS,
ERROR,
LOADING
}
data class Resource<out T>(
val status: Status,
val data: T?,
val message: String?
) {
companion object {
fun <T> success(data: T?): Resource<T> {
return Resource(Status.SUCCESS, data, null)
}
fun <T> error(msg: String): Resource<T> {
return Resource(Status.ERROR, null, msg)
}
fun <T> loading(): Resource<T> {
return Resource(Status.LOADING, null, null)
}
}
}
- The
InputValidation.kt
class helps validate user input during the authentication process. It checks if the email and password entered by the user are in the correct format and not empty. This ensures that the user provides valid information for successful authentication.
class InputValidation {
companion object {
private const val EMAIL_ADDRESS_PATTERN = "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+\$"
private const val PASSWORD_PATTERN = "^.*(?=.{4,})(?=.*[a-zA-Z])(?=.*\\d)(?=.*[!&\$%&?#_@ ]).*\$"
fun checkNullity(input: String): Boolean {
return input.isNotEmpty();
}
fun isUsernameValid(username: String): Pair<Boolean, String> {
if (username.isEmpty()) {
return Pair(false, "Username cannot be empty.")
}
if (username.length > 100) {
return Pair(false, "Username cannot be more than 100 characters.")
}
if (username[0].isDigit()) {
return Pair(false, "Username cannot start with a number.")
}
if (username.matches("^[a-zA-Z0-9 ]+$".toRegex()).not()) {
return Pair(false, "Username can only contain alphabets and numbers.")
}
return Pair(true, "")
}
fun isEmailValid(email: String): Pair<Boolean, String> {
if (email.isEmpty()) {
return Pair(false, "Email cannot be empty.")
}
if (email[0].isDigit()) {
return Pair(false, "Email cannot start with a number.")
}
if (EMAIL_ADDRESS_PATTERN.toRegex().matches(email).not()) {
return Pair(false, "Email is not valid.")
}
return Pair(true, "")
}
fun isPasswordValid(password: String): Pair<Boolean, String> {
if (password.isEmpty()) {
return Pair(false, "Password cannot be empty.")
}
if (PASSWORD_PATTERN.toRegex().matches(password).not()) {
return Pair(false, "Password is not valid.")
}
return Pair(true, "")
}
}
}
The
TextInputUtils.kt
file provides two simple, yet useful extensions for theTextInputLayout
andTextInputEditText
widgets in Android.The first function, addTextWatcher, add a text watcher to the
TextInputLayout
and clears any error message displayed when text is entered into theEditText
. This ensures that the user doesn't see any irrelevant error messages while typing.The second function, getValue, returns the value entered into the
TextInputEditText
as a string. This function returns the string entered into theTextInputEditText
after being trimmed, ensuring that any leading or trailing spaces are removed from the returned value.
fun TextInputLayout.addTextWatcher() {
editText?.addTextChangedListener {
error = null
}
}
fun TextInputEditText.getValue(): String {
return text.toString().trim()
}
- This code defines a custom
LoadingDialog.kt
class for a context-based dialog with a loading animation.
class LoadingDialog(context: Context) : Dialog(context) {
init {
setContentView(R.layout.loading_dialog)
window?.setLayout(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.WRAP_CONTENT
)
window?.setGravity(Gravity.CENTER)
setCancelable(false)
}
}
Building UI
activity_main.xml
you'll find buttons that will take you to screens for email authentication. Further, we will be adding more authentication options.
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="16dp"
tools:context=".MainActivity">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnEmailAuth"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="@color/btn_email_auth"
android:paddingVertical="8dp"
android:text="@string/btn_email_authentication"
android:textAllCaps="false"
app:icon="@drawable/ic_email"
app:iconGravity="textStart" />
</androidx.appcompat.widget.LinearLayoutCompat>
activity_email_login.xml
includes fields for entering both an email address and a password, and also includes a TextView labeled "Forgot Password?".
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
tools:context=".ui.email_authentication.EmailLoginActivity">
<com.google.android.material.textview.MaterialTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="4dp"
android:text="@string/header_login"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/etEmailContainer"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_email"
app:errorEnabled="true"
app:placeholderText="Brandonelouis@gmail.com"
app:startIconDrawable="@drawable/ic_email">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/etPasswordContainer"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_password"
app:endIconMode="password_toggle"
app:errorEnabled="true"
app:startIconDrawable="@drawable/ic_password">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/ctaForgotPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="4dp"
android:text="@string/forgot_password_prompt"
android:textAlignment="textEnd"
android:textAppearance="@style/TextAppearance.Material3.LabelLarge" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnLogin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/btn_login"
android:textAllCaps="false" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/ctaSignup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="4dp"
android:text="@string/signup_prompt"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Material3.LabelMedium" />
</LinearLayout>
activity_email_signup.xml
includes fields for entering a username, email address and password.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
tools:context=".ui.email_authentication.EmailSignupActivity">
<com.google.android.material.textview.MaterialTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="4dp"
android:text="@string/header_signup"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/etUsernameContainer"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_username"
app:errorEnabled="true"
app:placeholderText="eg. Brandone Louis"
app:startIconDrawable="@drawable/ic_person">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etUsername"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/etEmailContainer"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_email"
app:errorEnabled="true"
app:placeholderText="Brandonelouis@gmail.com"
app:startIconDrawable="@drawable/ic_email">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/etPasswordContainer"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_password"
app:endIconMode="password_toggle"
app:errorEnabled="true"
app:startIconDrawable="@drawable/ic_password">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSignup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/btn_signup"
android:textAllCaps="false" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/ctaLogin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="4dp"
android:text="@string/login_prompt"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Material3.LabelMedium" />
</LinearLayout>
activity_email_forgot_password.xml
include a field for entering, an email address.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
tools:context=".ui.email_authentication.EmailForgotPasswordActivity">
<com.google.android.material.textview.MaterialTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="4dp"
android:text="@string/header_forgot_password"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/etEmailContainer"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_email"
app:errorEnabled="true"
app:placeholderText="Brandonelouis@gmail.com"
app:startIconDrawable="@drawable/ic_email">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSendEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/btn_send_email"
android:textAllCaps="false" />
</LinearLayout>
activity_email_home.xml
display the user information and provides the functionality to logout and delete the account.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
tools:context=".ui.email_authentication.EmailHomeActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_id"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/userId"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
tools:text="53003205035" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_username"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
tools:text="Krish Parekh" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_email"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/email"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
tools:text="krish@gmail.com" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnLogout"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginHorizontal="8dp"
android:layout_weight="1"
android:text="@string/btn_logout"
android:textAllCaps="false" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnDeleteAccount"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginHorizontal="8dp"
android:layout_weight="1"
android:text="@string/btn_delete_account"
android:textAllCaps="false" />
</LinearLayout>
</LinearLayout>
Business Logic
The EmailAuthViewModel
is a class that handles the authentication-related business logic for your application using Firebase. It serves as a bridge between the Firebase authentication and the user interface. Currently, it uses callback functions to communicate the result of each action to the UI. However, this results in verbose code that can get difficult to read and maintain over time. Further, we will convert this code to use coroutines which will help simplify the code and make it easier to read and understand. This will remove the "callback hell" and make our code more readable and maintainable in the long run.
The class has data classes and several live data objects that represent the status of different authentication operations like login, signup, password reset, signout, and delete accounts. These live data objects are observed by the UI and update in real time, keeping the user informed of the current state of the authentication process.
Functions | Description |
signInWithEmailAndPassword(email, password) | the function takes an email and password as argument and signs the user. |
createUserWithEmailAndPassword(email, password) | the function creates a new user account. |
sendPasswordResetEmail(email) | the function sends a password reset email to the user. |
signOut | the function simply signs the user out. |
delete | the function deletes the user's account. |
If the user account creation is successful, the function retrieves the current user from the mAuth
object and creates a UserProfileChangeRequest
using the UserProfileChangeRequest.Builder
class. The username parameter is set as the display name in the profile using setDisplayName(username)
.
Finally, the function calls the updateProfile
method on the user object to update the user's profile with the display name.
data class LoginUiState(
val userId: String,
val username: String,
val email: String,
)
class EmailAuthViewModel : ViewModel() {
private val mAuth: FirebaseAuth by lazy { Firebase.auth }
private val _loginStatus = MutableLiveData<Resource<LoginUiState>>()
val loginStatus: LiveData<Resource<LoginUiState>> = _loginStatus
private val _signupStatus = MutableLiveData<Resource<String>>()
val signupStatus: LiveData<Resource<String>> = _signupStatus
private val _resetPasswordStatus = MutableLiveData<Resource<String>>()
val resetPasswordStatus: LiveData<Resource<String>> = _resetPasswordStatus
private val _deleteAccountStatus = MutableLiveData<Resource<String>>()
val deleteAccountStatus: LiveData<Resource<String>> = _deleteAccountStatus
fun login(email: String, password: String) {
_loginStatus.postValue(Resource.loading())
mAuth.signInWithEmailAndPassword(email, password)
.addOnSuccessListener {
val user = mAuth.currentUser!!
val loginUiState = LoginUiState(
userId = user.uid,
username = user.displayName!!,
email = user.email!!
)
_loginStatus.postValue(Resource.success(loginUiState))
}
.addOnFailureListener { exception ->
_loginStatus.postValue(Resource.error("Login failed : ${exception.message}"))
}
}
fun signup(
username: String,
email: String,
password: String
) {
_signupStatus.postValue(Resource.loading())
mAuth.createUserWithEmailAndPassword(email, password)
.addOnSuccessListener {
val user = mAuth.currentUser!!
val profileBuilder = UserProfileChangeRequest.Builder()
val profile = profileBuilder.setDisplayName(username).build()
user.updateProfile(profile)
.addOnSuccessListener {
_signupStatus.postValue(Resource.success("Signup success."))
}
.addOnFailureListener { exception ->
_signupStatus.postValue(Resource.error("Signup failed : ${exception.message}"))
}
}
.addOnFailureListener { exception ->
_signupStatus.postValue(Resource.error("Signup failed : ${exception.message}"))
}
}
fun resetPassword(email: String) {
_resetPasswordStatus.postValue(Resource.loading())
mAuth.sendPasswordResetEmail(email)
.addOnSuccessListener {
_resetPasswordStatus.postValue(Resource.success("Resent email sent."))
}
.addOnFailureListener { exception ->
_resetPasswordStatus.postValue(Resource.error("Resent email failed : ${exception.message}"))
}
}
fun signOut() = mAuth.signOut()
fun deleteAccount() {
_deleteAccountStatus.postValue(Resource.loading())
val user = mAuth.currentUser!!
user.delete()
.addOnSuccessListener {
_deleteAccountStatus.postValue(Resource.success("Delete account success."))
}
.addOnFailureListener { exception ->
_deleteAccountStatus.postValue(Resource.error("Delete account failed : ${exception.message}"))
}
}
}
MainActivity.kt
class contains setupUI
method which attaches a click listener to the btnEmailAuth button. When the button is clicked, the code creates a new Intent
object with Email/buLoginActivity
as the target class. Finally, the code calls startActivity
to navigate from the MainActivity
to the EmailLoginActivity
.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupUI()
}
private fun setupUI() {
binding.btnEmailAuth.setOnClickListener {
val emailIntent = Intent(this, EmailLoginActivity::class.java)
startActivity(emailIntent)
}
}
}
The EmailLoginActivity
is a class that handles the email authentication process for the user. It uses the viewModels delegate from the viewmodel-ktx library to initialize the EmailAuthViewModel
object, which is responsible for handling the login process.
setupUI()
: This method is used to set up the user interface. It sets click listeners on various UI elements like the "Sign Up" and "Forgot Password" buttons and the login button. It also sets text watchers on the email and password text fields to verify their validity using theInputValidation
class.setupObserver()
: This method sets up an observer to monitor theloginStatus
of theEmailAuthViewModel
. If the login process is successful, it navigates the user to theEmailHomeActivity
class and passes some data such as the user ID, username, and email. If the login process fails, it displays an error message using a toast.
class EmailLoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityEmailLoginBinding
private val emailAuthViewModel by viewModels<EmailAuthViewModel>()
private val loadingDialog by lazy { LoadingDialog(this) }
companion object {
const val UID = "uid"
const val USERNAME = "username"
const val EMAIL = "email"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityEmailLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
setupUI()
setupObserver()
}
private fun setupUI() {
binding.ctaSignup.setOnClickListener {
val signupIntent = Intent(this, EmailSignupActivity::class.java)
startActivity(signupIntent)
}
binding.ctaForgotPassword.setOnClickListener {
val forgetPassIntent = Intent(this, EmailForgotPasswordActivity::class.java)
startActivity(forgetPassIntent)
}
binding.etEmailContainer.addTextWatcher()
binding.etPasswordContainer.addTextWatcher()
binding.btnLogin.setOnClickListener {
val email = binding.etEmail.getValue()
val password = binding.etPassword.getValue()
if (verifyDetails(email, password)) {
emailAuthViewModel.login(email, password)
}
}
}
private fun setupObserver() {
emailAuthViewModel.loginStatus.observe(this) { login ->
when (login.status) {
LOADING -> {
loadingDialog.show()
}
SUCCESS -> {
loadingDialog.dismiss()
val state = login.data!!
navigateToHome(state)
}
ERROR -> {
loadingDialog.dismiss()
val error = login.message!!
Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
}
}
}
}
private fun navigateToHome(state: LoginUiState) {
val homeIntent = Intent(this, EmailHomeActivity::class.java)
homeIntent.putExtra(UID, state.userId)
homeIntent.putExtra(USERNAME, state.username)
homeIntent.putExtra(EMAIL, state.email)
startActivity(homeIntent)
finish()
}
private fun verifyDetails(email: String, password: String): Boolean {
val (isEmailValid, emailError) = InputValidation.isEmailValid(email)
if (isEmailValid.not()) {
binding.etEmailContainer.error = emailError
return isEmailValid
}
val (isPasswordValid, passwordError) = InputValidation.isPasswordValid(password)
if (isPasswordValid.not()) {
binding.etPasswordContainer.error = passwordError
return isPasswordValid
}
return true
}
}
The EmailSignupActivity.kt
handles the sign-up process. In the setupUI
method, user input is verified and the sign-up function of the EmailAuthViewModel
is called to sign up the user. In the setupObserver
method, the sign-up status is monitored and the UI is updated accordingly.
class EmailSignupActivity : AppCompatActivity() {
private lateinit var binding: ActivityEmailSignupBinding
private val emailAuthViewModel by viewModels<EmailAuthViewModel>()
private val loadingDialog by lazy { LoadingDialog(this) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityEmailSignupBinding.inflate(layoutInflater)
setContentView(binding.root)
setupUI()
setupObserver()
}
private fun setupUI() {
binding.ctaLogin.setOnClickListener {
finish()
}
binding.etUsernameContainer.addTextWatcher()
binding.etEmailContainer.addTextWatcher()
binding.etPasswordContainer.addTextWatcher()
binding.btnSignup.setOnClickListener {
val username = binding.etUsername.getValue()
val email = binding.etEmail.getValue()
val password = binding.etPassword.getValue()
if (verifyDetails(username, email, password)) {
emailAuthViewModel.signup(username, email, password)
}
}
}
private fun setupObserver() {
emailAuthViewModel.signupStatus.observe(this) { signup ->
when (signup.status) {
LOADING -> {
loadingDialog.show()
}
SUCCESS -> {
loadingDialog.dismiss()
val state = signup.data!!
Toast.makeText(this, state, Toast.LENGTH_SHORT).show()
finish()
}
ERROR -> {
loadingDialog.dismiss()
val error = signup.message!!
Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
}
}
}
}
private fun verifyDetails(
username: String,
email: String,
password: String
): Boolean {
val (isUsernameValid, usernameError) = InputValidation.isUsernameValid(username)
if (isUsernameValid.not()) {
binding.etUsernameContainer.error = usernameError
return isUsernameValid
}
val (isEmailValid, emailError) = InputValidation.isEmailValid(email)
if (isEmailValid.not()) {
binding.etEmailContainer.error = emailError
return isEmailValid
}
val (isPasswordValid, passwordError) = InputValidation.isPasswordValid(password)
if (isPasswordValid.not()) {
binding.etPasswordContainer.error = passwordError
return isPasswordValid
}
return true
}
}
The EmailForgotPasswordActivity.kt
file handles the process of forgotten passwords.
class EmailForgotPasswordActivity : AppCompatActivity() {
private lateinit var binding: ActivityEmailForgotPasswordBinding
private val emailAuthViewModel by viewModels<EmailAuthViewModel>()
private val loadingDialog by lazy { LoadingDialog(this) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityEmailForgotPasswordBinding.inflate(layoutInflater)
setContentView(binding.root)
setupUI()
setupObserver()
}
private fun setupUI() {
binding.etEmailContainer.addTextWatcher()
binding.btnSendEmail.setOnClickListener {
val email = binding.etEmail.getValue()
if (verifyDetail(email)) {
emailAuthViewModel.resetPassword(email)
}
}
}
private fun setupObserver() {
emailAuthViewModel.resetPasswordStatus.observe(this) { resetPassword ->
when (resetPassword.status) {
LOADING -> {
loadingDialog.show()
}
SUCCESS -> {
loadingDialog.dismiss()
val state = resetPassword.data!!
Toast.makeText(this, state, Toast.LENGTH_SHORT).show()
finish()
}
ERROR -> {
loadingDialog.dismiss()
val error = resetPassword.message!!
Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
}
}
}
}
private fun verifyDetail(email: String): Boolean {
val (isEmailValid, emailError) = InputValidation.isEmailValid(email)
if (isEmailValid.not()) {
binding.etEmailContainer.error = emailError
return isEmailValid
}
return true
}
}
The EmailHomeActivity.kt
is the go-to place for a user once they have successfully logged in. It showcases information about the current user and offers options such as deleting the account and logging out.
class EmailHomeActivity : AppCompatActivity() {
private lateinit var binding: ActivityEmailHomeBinding
private val emailAuthViewModel by viewModels<EmailAuthViewModel>()
private val loadingDialog by lazy { LoadingDialog(this) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityEmailHomeBinding.inflate(layoutInflater)
setContentView(binding.root)
setupUI()
setupObserver()
}
private fun setupUI() {
val bundle = intent.extras!!
val uid = bundle.getString(EmailLoginActivity.UID)
val username = bundle.getString(EmailLoginActivity.USERNAME)
val email = bundle.getString(EmailLoginActivity.EMAIL)
binding.userId.text = uid
binding.username.text = username
binding.email.text = email
binding.btnLogout.setOnClickListener {
emailAuthViewModel.signOut()
navigateToLogin()
}
binding.btnDeleteAccount.setOnClickListener {
emailAuthViewModel.deleteAccount()
navigateToLogin()
}
}
private fun setupObserver() {
emailAuthViewModel.deleteAccountStatus.observe(this){ delete ->
when(delete.status){
LOADING -> {
loadingDialog.show()
}
SUCCESS -> {
loadingDialog.dismiss()
val state = delete.data!!
Toast.makeText(this, state, Toast.LENGTH_SHORT).show()
navigateToLogin()
}
ERROR -> {
loadingDialog.dismiss()
val error = delete.message!!
Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
}
}
}
}
private fun navigateToLogin() {
finish()
val loginIntent = Intent(this, EmailLoginActivity::class.java)
startActivity(loginIntent)
}
}
Escaping "Callback Hell" with "Coroutines"
What is CallBack Hell?
Callback Hell is a term used to describe a situation in which the code for handling asynchronous operations becomes deeply nested and difficult to read and maintain.
In the signup code, we have multiple nested callbacks to handle the results of asynchronous operations.
createUserWithEmailAndPassword
andupdateProfile
both of these operations are asynchronous and the results are handled usingaddOnSuccessListener
andaddOnFailureListener
callback.The problem with callback hell is that it makes the code harder to read, maintain, and debug.
fun signup(...) {
mAuth.createUserWithEmailAndPassword(email, password)
.addOnSuccessListener {
...
user.updateProfile(profile)
.addOnSuccessListener {
...
}
.addOnFailureListener { exception ->
...
}
}
.addOnFailureListener { exception ->
...
}
}
What is Coroutine?
Coroutines are a powerful and lightweight tool for handling asynchronous operations in Kotlin. They allow you to write asynchronous code in a way that is more concise and readable than traditional callback-based approaches.
It allows us to write asynchronous code in a way that is similar to writing synchronous code. we can use suspend function to sequentially run asynchronous code.
To learn more about coroutine link.
Leveraging Coroutine
viewModelScope.launch(IO)
is used to start a new coroutine within the scope of the ViewModel.viewModelScope
our coroutine is tied to ViewModel, which means the coroutine will be automatically canceled when the ViewModel is destroyed.Dispatcher
is a way to control the execution of coroutines.IO
is a dispatcher that is optimized for input/output operations, such as reading from or writing to a database or a network.
await()
is used to wait for an asynchronous function to complete.
class EmailAuthViewModel : ViewModel() {
private val mAuth: FirebaseAuth by lazy { Firebase.auth }
private val _loginStatus = MutableLiveData<Resource<LoginUiState>>()
val loginStatus: LiveData<Resource<LoginUiState>> = _loginStatus
private val _signupStatus = MutableLiveData<Resource<String>>()
val signupStatus: LiveData<Resource<String>> = _signupStatus
private val _resetPasswordStatus = MutableLiveData<Resource<String>>()
val resetPasswordStatus: LiveData<Resource<String>> = _resetPasswordStatus
private val _deleteAccountStatus = MutableLiveData<Resource<String>>()
val deleteAccountStatus: LiveData<Resource<String>> = _deleteAccountStatus
fun login(email: String, password: String) {
viewModelScope.launch(IO) {
try {
_loginStatus.postValue(Resource.loading())
mAuth.signInWithEmailAndPassword(email, password).await()
val user = mAuth.currentUser!!
val loginUiState = LoginUiState(
userId = user.uid,
username = user.displayName!!,
email = user.email!!
)
_loginStatus.postValue(Resource.success(loginUiState))
} catch (exception: Exception) {
_loginStatus.postValue(Resource.error("Login failed : ${exception.message}"))
}
}
}
fun signup(
username: String,
email: String,
password: String
) {
viewModelScope.launch(IO) {
try {
_signupStatus.postValue(Resource.loading())
mAuth.createUserWithEmailAndPassword(email, password)
val user = mAuth.currentUser!!
val profileBuilder = UserProfileChangeRequest.Builder()
val profile = profileBuilder.setDisplayName(username).build()
user.updateProfile(profile).await()
_signupStatus.postValue(Resource.success("Signup success."))
} catch (exception: Exception) {
_signupStatus.postValue(Resource.error("Signup failed : ${exception.message}"))
}
}
}
fun resetPassword(email: String) {
viewModelScope.launch(IO) {
try {
_resetPasswordStatus.postValue(Resource.loading())
mAuth.sendPasswordResetEmail(email).await()
_resetPasswordStatus.postValue(Resource.success("Resent email sent."))
} catch (exception: Exception) {
_resetPasswordStatus.postValue(Resource.error("Resent email failed : ${exception.message}"))
}
}
}
fun signOut() = mAuth.signOut()
fun deleteAccount() {
viewModelScope.launch(IO) {
try {
_deleteAccountStatus.postValue(Resource.loading())
val user = mAuth.currentUser!!
user.delete().await()
_deleteAccountStatus.postValue(Resource.success("Delete account success."))
} catch (exception: Exception) {
_deleteAccountStatus.postValue(Resource.error("Delete account failed : ${exception.message}"))
}
}
}
}
Dealing with Exception in Firebase Authentication
Common exceptions you should take into consideration while dealing with Firebase Authentication.
Exception | Description |
FirebaseAuthInvalidCredentialsException | This exception is thrown if the email address or password provided is invalid. For example, if the password is too short or doesn't meet the minimum requirements set by Firebase. |
FirebaseAuthUserCollisionException | This exception is thrown if the email address provided is already in use by another user. This typically occurs when trying to create a new user |
FirebaseAuthInvalidUserException | This exception is thrown if the user corresponding to the email address provided in the request does not exist. This typically occurs when trying to sign in |
FirebaseNetworkException | This exception is thrown if there is a problem with the network connection, such as a timeout or a lost connection. |
FirebaseTooManyRequestsException | This exception is thrown if the client has made too many requests to Firebase within a short period. This is often used to prevent abuse of the Firebase service. |
Conclusion
In conclusion, we have covered everything you need to know about Firebase email authentication. From how Firebase handles authentication under the hood, to building a complete project using the Firebase API. We also optimized the code by using coroutines, reducing callback hell and addressing common exceptions that should be considered. If you want to follow along with the blog, you can check out my GitHub project. Don't forget to follow me on GitHub and social media for more updates. In our next blog, we will be discussing phone number authentication. So stay tuned and keep learning!