Firebase Phone Authentication in Jetpack Compose using MVVM architecture
In this post, I will show you how to do Firebase authentication using SMS.
Project Set up
Create a new Jetpack Compose project in Android Studio using the Jetpack Compose project template. The next thing we will have to do is to add the hilt-android-gradle-plugin and google-services plugin to your project’s root build.gradle file and add the required Firebase and Play services dependencies
Then next, apply these two plugins to your app/build.gradle file and kotlin-kapt.
Ensure that you connect your Android Studio project to a Firebase project so as to download the google-services.json file into your project.
The next thing for us to do is to write the actual code. Before we proceed, note that making Firebase phone authentication with the latest versions requires that we set the activity on which the authentication will be made. This is so that Firebase knows the Activity to return to when Captcha verification is complete. Thus we are forced to use an anti-pattern in this case. This is how our project will be structured.
AuthService->AuthServiceImpl->AuthViewModel->PhoneLoginUI()->MainActivity
We will also create a Response sealed class to specify the state in which the authentication is in. It is either initialized, loading, error, or successful.
The next thing for us to do is to create the AuthService interface. It will have basic methods required for doing a firebase phone authentication.
The signUpdateState variable will be used to update the state of the authentication. The method onCodeSent is called when the SMS is sent. At this point, the state will be updated accordingly. The method authenticate will be used to call the Firebase service to send the SMS and the method onVerifyOtp will be called when the user manually types the SMS code sent to their phone number. If all is good, we will have onVerificationCompleted called else we will have onverificationFailed. Note that we modeled some of these methods to follow the methods in OnVerificationStateChangedCallbacks.
The next class thing for us is to implement the AuthService interface through AuthServiceImpl. Here, we will create an authCallbacks variable of the type PhoneAuthProvider.OnVerificationStateChangedCallbacks. We are concerned with three important methods of OnVerificationStateChangedCallbacks, which are onCodeSent,onVerificationCompleted and onVerificationFailed. Each method serves a purpose. Below, let us see what each method serves.
onCodeSent:Called when the SMS code is sent to the user’s device
onVerificationCompleted: Called when the SMS is automatically retrieved from the user’s device without them typing the code. This will likely happen when their SIM card is present on this device and the SMS code is automatically picked. We with then use this code and complete the verification process.
onVerificationFailed: Called when verification failed. It might be due to typing a wrong SMS code or sending too many requests to Firebase to send a code.
Below is the AuthServiceImpl class.
The AuthServiceImpl class appears to be much code but it is rather simple. We start by constructor-injecting FirebaseAuth and MainActivity. This is where the ani-pattern we talked about comes in. We could have passed a Context rather than our MainActivity and annotated it with @ActivityContext to provide us the MainActivity(since our composables will be hosted by it). We do this simply to call the setActivity method when creating the authBuilder variable. Not setting an Activity, we will not be able to do the authentication. Somehow, I tried passing a Context parameter and annotating it with @ActivityContext so that it acts as the Activity but it kept failing. I had to hack this way and provide MainActivity instead. We will be using this class in ViewModel and we might worry that it might leak the Activity but do not worry, we will ensure that this does not happen in MainActivity by creating a static instance of MainActivity and setting it to null in onDestroy.
We used the FirebaseAuth auth variable to create the authBuilder variable as well and we set the authCallbacks as the callbacks when creating authBuilder.In each of the overridden methods, we call the appropriate methods of the authCallbacks accordingly. If the code is sent successfully and the device picks up the code automatically,onVerificationCompleted is called. We now use this code to retrieve PhoneAuthCredential using this code and use this credential to sign the user into the app. This is the purpose of the signInWithAuthCredential method. If the user’s SIM card is in a different device, they will have to type the code manually and in this case, we will also call signInWithAuthCredential.This is thus the purpose of the onVerifyOtp method.
The method called when the user submits the phone number is authenticate.We call setPhoneNumber(phone) on authBuilder and then build a PhoneAuthOptions object and go further to call PhoneAuthProvider.verifyPhoneNumber(options).Note how the state of the signUpState is updated in each case.
The next thing for us to do is to create the AuthViewModel class.
Because we delegated most of the work to be done to the AuthServiceImpl class, the ViewModel will just call each method from the AuthServiceImpl accordingly. The number and phone I will be used for the phone number to be typed and the code respectively. Each of the onXChange methods is used to update the states of the phone number and code. The ViewModel will be used in our root composable for authentication.
The next thing for us to do is to write the actual UI code for the authentication. We will not just use one large composable for all the code but we will rather break down the composables according to the application state(signUpState). We will thus have the UI for the initialized state, the loading state, and the error states. We assume that during the success state, we will navigate to the next screen. Finally, we will have the root composable which will house themes composables.
The uninitialized state has a text field for the user to input their phone number and a button to proceed. There will be the UI provided for the user when the code is sent and the error UI when an error occurs. Let us first see the phone input composable.
The phone number UI takes the phone number,onPhoneChange,onClick and onDone. I am assuming you understand the practice of state hoisting. We have dedicated a separate composable for the phone number that takes the phone number, attached to the value parameter, onNumberChange which is assigned to the OutlinedTextField’s onValueChange method and onDone which will be called when the user hits the done button on the keyboard. That is why we set the imeAction to onDone.
The next composable is the one for inputting the code. It is almost similar to the one for the phone number but this time around, we have no button. We will call the appropriate method when the user hits the Go action key. That is why we set the imeAction here to onGo.
The error UI which is next just displays the error message based on the Throwable. It provides a restart button for the user to restart.
The last composable is our main Composable which will show each of the above composables based on the states. It is in this composable that we will use the AuthViewModel.
For state changes, we use StateFlows and the collectAsState composable which is an extension function on Flow.
The next things which are trivial will be providing the various objects such as FirebaseAuth,MainActivity, and AccountService. We will use @Provides annotation for providing FirebaseAuth and MainActivity and we will use @Binds to construct the AuthService.In this case, we pass the implementation to the constructor of the method, and the return type is the interface. We could have still explicitly created it using @Binds. Below are the modules.
Next, we will create the getInstance method in our MainActivity to return the instance. We will of course destroy it in the Activity’s onDestory method to avoid leaking the Activity context.Do not forget to annotate with MainActivity class with @AndroidEntryPoint.
Also, create our Hilt App class and set it as the name in AndroidManifest.xml.
@HiltAndroidApp
class MainHiltApp : Application()
Setting the app in Manifest.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:name=".ui.MainHiltApp"
....../>
/>
I hope this long post will help someone out there. Leave your comments and criticisms below.