Firebase Email/Password Authentication: An In-Depth Guide for Android Developers using Kotlin

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:

  1. The user provides their credentials (such as email and password) to Firebase.

  2. Firebase securely transmits the credentials to its servers for verification.

  3. Firebase checks the provided credentials against its database of registered users.

  4. If the provided credentials match an existing user, Firebase generates a secure token and sends it back to the app.

  5. The app stores the token locally and sends it with each subsequent request to Firebase.

  6. Firebase verifies the token on each request to ensure that the user is still authenticated.

  7. 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 the TextInputLayout and TextInputEditText 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 the EditText. 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 the TextInputEditText 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>

<?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>

<?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.

FunctionsDescription
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.
signOutthe function simply signs the user out.
deletethe 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 the InputValidation class.

  • setupObserver(): This method sets up an observer to monitor the loginStatus of the EmailAuthViewModel. If the login process is successful, it navigates the user to the EmailHomeActivity 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 and updateProfile both of these operations are asynchronous and the results are handled using addOnSuccessListener and addOnFailureListener 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

EmailAuthViewModel.kt

  • 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.

ExceptionDescription
FirebaseAuthInvalidCredentialsExceptionThis 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.
FirebaseAuthUserCollisionExceptionThis 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
FirebaseAuthInvalidUserExceptionThis 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
FirebaseNetworkExceptionThis exception is thrown if there is a problem with the network connection, such as a timeout or a lost connection.
FirebaseTooManyRequestsExceptionThis 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!

Did you find this article valuable?

Support Krish Parekh by becoming a sponsor. Any amount is appreciated!