December 27, 2021 1:26 am

valokafor

This post is a continuation of my last post where I showed how to create a Sign In and Sign Up screen UI using Jetpack Compose. In this post, we will implement click handling in this UI and the actual flow of creating an account and logging in using the Email and Password method of Firebase Authentication.

Add Click Handlers

In the Sign In screen when the user clicks on the Sign In button, we need to have a click handler that performs the actual authentication. Composable functions should not have side effects so we should have this onClick handler outside of the SignIn composable function and a ViewModel is an ideal place to have this handler.

Step 1 – Add ViewModel Class

Add a Kotlin class named AuthViewModel with the following placeholder methods.

class AuthViewModel: ViewModel() {

    fun handleSignIn(email: String, password: String) {

    }

    fun handleSignUp(email: String, password: String, confirmPassword: String) {

    }
}

Now when the user clicks on Sign In or Sign Up button the appropriate methods will be called. But first, we need to update the composable to be able to call the ViewModel method.

Step 2 – Pass ViewModel to Composable

Update the SignIn() and the SignUp() methods to accept a ViewModel parameter as shown in the next screenshot for SignIn() with the code body redacted and the call to ViewModel highlighted.

Do the same for the SignUp method. Now we need to update the SignInScreen() to receive the ViewModel from MainActivity like this.

class MainActivity : ComponentActivity() {
    val authViewModel by viewModels<AuthViewModel>()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ProntoLoginTheme {
                Surface(color = MaterialTheme.colors.background) {
                    SignUpScreen(authViewModel)
                }
            }
        }
    }
}

Step 3 – Add Firebase Authentication

To add Firebase to your project follow this guide. Once you have added Firebase to your project you can update your build.gradle file to add Firebase Authentication as shown in the next code snippet.

dependencies {
    implementation platform('com.google.firebase:firebase-bom:29.0.3')
    implementation 'com.google.firebase:firebase-auth-ktx'
}

Step 4 – Implement Sign In & Signup Method

Once you have added Firebase Authentication to the project, you can update the handleSignIn() and handleSignUp() methods on the AuthViewModel as shown in the next code snippet to implement sign in and sign up.

   fun handleSignIn(email: String, password: String) {
        FirebaseAuth.getInstance().signInWithEmailAndPassword(email, password)
            .addOnCompleteListener { task ->
                if (task.isSuccessful) {
                    Log.i(TAG,"Email login is successful")
                } else {
                    task.exception?.let {
                        Log.i(TAG,"Email login failed with error ${it.localizedMessage}")
                    }
                }
            }
    }
    fun handleSignUp(email: String, password: String, confirmPassword: String) {
        FirebaseAuth.getInstance().createUserWithEmailAndPassword(
            email, password)
            .addOnCompleteListener { task ->
                if (task.isSuccessful) {
                    Log.i(TAG,"Email signup is successful")
                } else {
                    task.exception?.let {
                        Log.i(TAG,"Email signup failed with error ${it.localizedMessage}")
                    }
                }
            }
    }

Step 5 – Add Observable State

Currently, the sign-in and sign-up methods are just writing log statements when a sign-in failure or success event occurs, ideally, we should notify the caller so that it can update itself accordingly. It can show a toast message or a new view to the user or transition to a new screen. One way to do this is to introduce a new state called AuthState.

Add a sealed class named AuthState with the following members.

sealed class AuthState {
    object Idle : AuthState()
    object Loading : AuthState()
    object Success : AuthState()
    class AuthError(val message: String? = null) : AuthState()
}

Now we need to add an observable state of this Authstate to the ViewModel like this.

private val _authState by lazy { MutableLiveData<AuthState>(AuthState.Idle) }
val authUiState: LiveData<AuthState> = _authState

We can now update this state whenever a sign-in success or failure occurs as shown in the next screenshot and any changes to the value of this state will automatically recompose any composable functions that read this state.

With the observable state added, you can now update the signup method to apply basic validation such as checking for password mismatch as shown next.

    fun handleSignUp(email: String, password: String, confirmPassword: String) {
        if (!isEmailValid(email)) {
            _authState.value = AuthState.AuthError("Invalid email")
            return
        }
        if (password != confirmPassword) {
            _authState.value = AuthState.AuthError("Password does not match")
            return
        }
        FirebaseAuth.getInstance().createUserWithEmailAndPassword(
            email, password)
            .addOnCompleteListener { task ->
                if (task.isSuccessful) {
                    Log.i(TAG,"Email signup is successful")
                    _authState.value = AuthState.Success
                } else {
                    task.exception?.let {
                        Log.i(TAG,"Email signup failed with error ${it.localizedMessage}")
                        _authState.value = AuthState.AuthError(it.localizedMessage)
                    }
                }
            }
    }

Step 6 – Handle Observable State

You can now observe the AuthState in the composable functions by obtaining the LiveData you added to the ViewModel as State. Add this state to both the SignInScreen and SignUpScreen methods.


val authState by viewModel.authState.observeAsState(AuthState.Idle)

You can now handle the state inside a LaunchedEffect block, and if the state is not a handled state you can compose or re-compose the screen. Update the SignInScreen() method to match the following snippet. Do the same for the SignUpMethod().

@Composable
fun SignInScreen(viewModel: AuthViewModel) {
   val authState by viewModel.authState.observeAsState(AuthState.Idle)
   val handledStates = listOf(AuthState.AuthError(), AuthState.Loading, AuthState.Success)
   LaunchedEffect(authState) {
       when(authState) {
           is AuthState.Loading -> {
               //Show loading screen
           }
           is AuthState.Success -> {
               //Show Success screen
           }
           is AuthState.AuthError -> {
               //show error screen
           }
           else -> {}
       }
   }

   if (authState !in handledStates) {
       SignIn(viewModel)
   }
}

Conclusion

In this post, I have shown you how to implement email and password authentication for a composable function using Firebase Authentication. I also show how you can make your composable function stateful so it can react to changes from the ViewModel. A stateful composable is composable that owns a piece of state that it can change over time. If you are not familiar with State in Compose, you should read this article.

When the AuthState changes, you can call another composable function or navigate to another screen. You cannot call a composable function inside a LaunchedEffect block so your options are to either navigate to another screen or in another post, I will introduce navigation with Compose. For now, you can show a toast message in your LaunchedEffect like so.

is AuthState.AuthError -> {
   val errorMessage = (authState as AuthState.AuthError).message
   Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT).show()
}

The complete source code for implementing authentication is included in the firebase_auth branch of the ProntoLogin App that you can download below.

About the Author

I am a Software Engineer & Entrepreneur. I create remarkable apps for my portfolio, employer and clients.

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}