Firebase Authentication With Jetpack Compose. Part 1
Tags: kotlin, android, androiddev, mobile
Published: 2024-06-18 23:10:21

Hey! This post is part of a series of building an expense tracker app. In today's guide we're going to implement firebase authentication with input verification in jetpack compose. Also, this post features the usage of `CompositionLocalProvider`, which enables a clear and reusable way of showing a snackbar instead of injecting a separate callback into composables which is very junky. In one of the next guides I'll show you how to implement Google sign in, UI, and Unit testing of authentication, so stay tuned. If you have any questions or suggestions feel free to leave them in comments

Here's how the end result might look like


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ghpw7ra4h2op214zdinx.gif)

And the source code is [here](https://github.com/EvgenSuit/MoneyMonocle)

---

# Firebase setup

Head on to firebase and click on add project
![Add project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vb8baengfyq28wxvz008.png)
After filling in the required info start configuring the project for Android by clicking on the corresponding icon
![Configure for Android](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l5v3m7o27zivv54s7nqk.png)
After following instructions head on to Authentication tab and add Email/Password provider
![Enable Email/Password authentication](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o1buvpqqi12d6hisp56h.png)

---

# Hilt setup
Hilt is a popular dependency injection tool which simplifies inserting dependencies (classes, objects, mocks, etc.) into other dependencies.

You can pick versions of plugins that suit you [here](https://mvnrepository.com)

Insert the following code into your root `build.gradle` file
```gradle
plugins {
  id("com.google.dagger.hilt.android") version "2.50" apply false
// compatible with the 1.9.22 version of Kotlin
  id("com.google.devtools.ksp") version "1.9.22-1.0.16" apply false
}
```
After syncing gradle, apply the below dependencies in your `app/build.gradle` file
```gradle
plugins {
  id("com.google.dagger.hilt.android")
  id("com.google.devtools.ksp")
}
dependencies {
  implementation("com.google.dagger:hilt-android:2.50")
// required if using navigation together with hilt
  implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
  ksp("com.google.dagger:hilt-compiler:2.50")
  ksp("com.google.dagger:hilt-android-compiler:2.50")
}
```
Then annotate your `MainActivity` class with `@AndroidEntryPoint`

```kotlin
@AndroidEntryPoint
class MainActivity : ComponentActivity()
```
And last, create a `${YourAppName}Application.kt` file and insert the following code

```kotlin
@HiltAndroidApp
// name it however you want
class MyApplication: Application()
```
To learn more about Hilt, visit [this page](https://developer.android.com/training/dependency-injection/hilt-android): 

---

# Repository and View Model
First let's create a `AuthUseCases.kt` file and put 3 input validators

```kotlin
class UsernameValidator {
    operator fun invoke(username: String): UsernameValidationResult {
        return if (username.isBlank()) UsernameValidationResult.IS_EMPTY
        else UsernameValidationResult.CORRECT
    }
}
class EmailValidator {
    operator fun invoke(email: String): EmailValidationResult {
        return if (Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
            EmailValidationResult.CORRECT
        }
        else EmailValidationResult.INCORRECT_FORMAT
    }
}

class PasswordValidator {
    operator fun invoke(password: String): PasswordValidationResult {
        return if (password.length < 8) PasswordValidationResult.NOT_LONG_ENOUGH
        else if (password.count(Char::isUpperCase) == 0) PasswordValidationResult.NOT_ENOUGH_UPPERCASE
        else if (!password.contains("[0-9]".toRegex())) PasswordValidationResult.NOT_ENOUGH_DIGITS
        else PasswordValidationResult.CORRECT
    }
}
enum class UsernameValidationResult {
    IS_EMPTY,
    CORRECT
}
enum class EmailValidationResult {
    INCORRECT_FORMAT,
    CORRECT
}
enum class PasswordValidationResult {
    NOT_LONG_ENOUGH,
    NOT_ENOUGH_DIGITS,
    NOT_ENOUGH_UPPERCASE,
    CORRECT
}
```
`operator fun invoke()` is a special type of function in Kotlin that allows us to insert parameters into a class instance. Here's an example

```kotlin
val passwordValidator = PasswordValidator()
passwordValidator(password)
```
For email validation we're using an email address checker of `android.util` package that returns true if a given email passes the check
In password validator, `password.contains("[0-9]".toRegex())` returns true if a given input contains at least 1 number, otherwise, we will inform a user of this requirement

---

Next, we'll create a `AuthRepository.kt` file that will consist of a class responsible for authentication, which we'll later inject into our view model

```kotlin
class AuthRepository(
    private val auth: FirebaseAuth,
    private val firestore: FirebaseFirestore) {
    suspend fun signUp(authState: AuthState) {
       auth.createUserWithEmailAndPassword(authState.email!!, authState.password!!).await()
       auth.currentUser?.updateProfile(UserProfileChangeRequest.Builder()
            .setDisplayName(authState.username!!).build())?.await()
    }
    suspend fun signIn(authState: AuthState) {
        auth.signInWithEmailAndPassword(authState.email!!, authState.password!!).await()
    }
}
```
The code above contains a function that creates a user with email, password, and a username, which is set with the help of `UserProfileChangeRequest`.

---

Next, create a `CoroutineScopeProvider.kt` file and define the class that will come in handy during view model testing, as with it we'll be able to use `TestScope` in our view model

```kotlin
class CoroutineScopeProvider(private val coroutineScope: CoroutineScope? = null) {
    fun provide() = coroutineScope
}
```
---

Create a `StringValue.kt` file and define the `StringValue` class. It will enable us to match input validation results to strings in an easy and clean way

```kotlin
sealed class StringValue {

    data class DynamicString(val value: String) : StringValue()

    data object Empty : StringValue()

    class StringResource(
        @StringRes val resId: Int
    ) : StringValue()

    fun asString(context: Context): String {
        return when (this) {
            is Empty -> ""
            is DynamicString -> value
            is StringResource -> context.getString(resId))
        }
    }
}
```
---
Create a `AuthModule.kt` file and define `AuthModule`, that will tell hilt which instance of `AuthRepository` and `CoroutineScopeProvider` to provide (e.g mocked or real)
```kotlin
@Module
@InstallIn(SingletonComponent::class)
object AuthModule {
    @Provides
    @Singleton
    fun provideAuthRepository(): AuthRepository =
        AuthRepository(Firebase.auth, Firebase.firestore)

    @Provides
    @Singleton
    fun provideCoroutineScopeProvider(): CoroutineScopeProvider =
        CoroutineScopeProvider()
}
```
Singleton components are created only once per lifecycle of the application. Since there's absolutely no need to provide a different instance of `AuthRepository` and `CoroutineScopeProvider` each time they're requested, using `@Singleton` is a perfect choice.

---
Create a `Result.kt` file containing the `Result` class.

```kotlin
sealed class CustomResult(val error: StringValue = StringValue.Empty) {
    data object Idle: CustomResult()
    data object InProgress: CustomResult()
    data object Empty: CustomResult()
    data object Success: CustomResult()
    class DynamicError(error: String): CustomResult(StringValue.DynamicString(error))
    class ResourceError(@StringRes res: Int): CustomResult(StringValue.StringResource(res))
}
```
This class will be responsible for providing smooth user experience as well as other important features (like disabling auth fields and buttons when the result of a sign in operation is `InProgress`)

---

After that, create a `AuthViewModel.kt` file and start defining auth view model

```kotlin
@HiltViewModel
class AuthViewModel @Inject constructor(
    private val authRepository: AuthRepository,
    coroutineScopeProvider: CoroutineScopeProvider
): ViewModel() {
    private val usernameValidator = UsernameValidator()
    private val emailValidator = EmailValidator()
    private val passwordValidator = PasswordValidator()
    private val scope = coroutineScopeProvider.provide() ?: viewModelScope
    private val _uiState = MutableStateFlow(UiState())
    val uiState = _uiState.asStateFlow()

    fun onUsername(username: String) {
        val result = when (usernameValidator(username)) {
            UsernameValidationResult.IS_EMPTY -> StringValue.StringResource(R.string.username_not_long_enough)
            else -> StringValue.Empty
        }
        _uiState.update { it.copy(validationState = it.validationState.copy(usernameValidationError = result),
            authState = it.authState.copy(username = username)) }
    }
    fun onEmail(email: String) {
        val result = when (emailValidator(email)) {
            EmailValidationResult.INCORRECT_FORMAT -> StringValue.StringResource(R.string.invalid_email_format)
            else -> StringValue.Empty
        }
        _uiState.update { it.copy(validationState = it.validationState.copy(emailValidationError = result),
            authState = it.authState.copy(email = email)) }
    }
    fun onPassword(password: String) {
        val result = when (passwordValidator(password)) {
            PasswordValidationResult.NOT_LONG_ENOUGH -> StringValue.StringResource(R.string.password_not_long_enough)
            PasswordValidationResult.NOT_ENOUGH_UPPERCASE -> StringValue.StringResource(R.string.password_not_enough_uppercase)
            PasswordValidationResult.NOT_ENOUGH_DIGITS -> StringValue.StringResource(R.string.password_not_enough_digits)
            else -> StringValue.Empty
        }
        _uiState.update { it.copy(validationState = it.validationState.copy(passwordValidationError = result),
            authState = it.authState.copy(password = password)) }
    }
    data class UiState(
        val authType: AuthType = AuthType.SIGN_IN,
        val authState: AuthState = AuthState(),
        val validationState: ValidationState = ValidationState(),
        val authResult: CustomResult = CustomResult.Idle)
    data class ValidationState(
        val usernameValidationError: StringValue = StringValue.Empty,
        val emailValidationError: StringValue = StringValue.Empty,
        val passwordValidationError: StringValue = StringValue.Empty,
    )
}
data class AuthState(
    val username: String? = null,
    val email: String? = null,
    val password: String? = null,
)
enum class AuthType {
    SIGN_IN,
    SIGN_UP
}
enum class FieldType {
    USERNAME,
    EMAIL,
    PASSWORD
}
```
In this code `AuthViewModel` is annotated with `@HiltViewModel`, and its constructor is annotated with `@Inject`, which makes hilt inject the dependencies we defined in `AuthModule`.
`onUsername`, `onEmail`, and `onPassword` functions are responsible for updating auth fields and the appropriate verification results. These will be called every time a user types something in.
Depending on the value of `authType` variable we'll decide what kind of authentication is currently chosen (sign in or sign up), and make the UI and authentication pipeline react accordingly.

---
Now lets add a couple of functions to `AuthViewModel`, which are directly responsible for authenticating a user

```kotlin
fun changeAuthType() {
        _uiState.update { it.copy(authType = AuthType.entries[it.authType.ordinal xor 1]) }
    }

    fun onCustomAuth() {
        val authType = _uiState.value.authType
        updateAuthResult(CustomResult.InProgress)
        scope.launch {
            try {
                if (authType == AuthType.SIGN_UP) {
                    authRepository.signUp(_uiState.value.authState)
                }
                authRepository.signIn(_uiState.value.authState)
                updateAuthResult(CustomResult.Success)
            } catch (e: Exception) {
                updateAuthResult(CustomResult.DynamicError(e.toStringIfMessageIsNull()))
            }
        }
    }
```
`changeAuthType` changes authentication type using XOR logical operator. For example, when a user is viewing a sign in composable, `authType` ordinal has a value of 0. Upon calling the function, 0 xor 1 will be equal to 1, selecting the auth type at index 1, which is sign up. And vise versa, when a user is viewing a sign up composable and calls `changeAuthType` function, the result of 1 xor 1 will be 0.
`onCustomAuth` function starts off by telling a user that the authentication is running, then launches a coroutine scope with a `try` `catch` block. If a user currently wants to sign up, a corresponding repository function is executed. After successful sign up, an automatic sign in attempt is performed. If auth is successful, the function updates the result to `Success`.

---

# UI code

**(Optional)** 
Let's create a fancy title for our app with gradient animation
```kotlin
@Composable
fun Title() {
    val gradientColors = listOf(MaterialTheme.colorScheme.onBackground,
        MaterialTheme.colorScheme.primary,
        MaterialTheme.colorScheme.onPrimary.copy(0.5f))
    var offsetX by remember { mutableStateOf(0f) }
    LaunchedEffect(Unit) {
        animate(
            initialValue = 0f,
            targetValue = 1000f,
            animationSpec = tween(3000)) {value, _ ->
            offsetX = value
        }
    }
    val brush = Brush.linearGradient(
        colors = gradientColors,
        start = Offset(offsetX, 0f),
        end = Offset(offsetX + 200f, 100f)
    )
    Text(stringResource(id = R.string.app_name),
        style = MaterialTheme.typography.titleLarge.copy(brush = brush))
}
```

---

Let's define an auth field
```kotlin
@Composable
fun CustomInputField(
    enabled: Boolean,
    fieldType: FieldType,
    value: String?,
    error: String,
    onValueChange: (String) -> Unit) {
    val shape = RoundedCornerShape(dimensionResource(id = R.dimen.auth_corner))
    val fieldTypeString = stringResource(id = when(fieldType) {
        FieldType.USERNAME -> R.string.username
        FieldType.EMAIL -> R.string.email
        FieldType.PASSWORD -> R.string.password
    })
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier.fillMaxWidth()
    ) {
        OutlinedTextField(
            value = value ?: "",
            isError = error.isNotEmpty(),
            onValueChange = onValueChange,
            enabled = enabled,
            shape = shape,
            keyboardOptions = KeyboardOptions(imeAction =
            if (fieldType == FieldType.USERNAME || fieldType == FieldType.EMAIL) ImeAction.Next else ImeAction.Done),
            placeholder = {
                if (value.isNullOrBlank())
                    Text(fieldTypeString)
            },
            singleLine = true,
            modifier = Modifier.fillMaxWidth().testTag(fieldTypeString)
        )
        if (error.isNotEmpty()) {
            Text(error,
                textAlign = TextAlign.Center,
                color = MaterialTheme.colorScheme.error,
                modifier = Modifier.testTag(error))
        }
    }
}
```
Here, we use `fieldType` to decide the Ime action and show a placeholder. Also, we use the result of input validators to either show or not show an error text composable

---
Let's define an auth button

```kotlin
@Composable
fun CustomAuthButton(
    authType: AuthType,
    enabled: Boolean,
    onClick: () -> Unit) {
    val label = stringResource(
        id = when (authType) {
            AuthType.SIGN_IN -> R.string.sign_in
            AuthType.SIGN_UP -> R.string.sign_up
        }
    )
    ElevatedButton(onClick = onClick,
        shape = RoundedCornerShape(dimensionResource(id = R.dimen.button_corner)),
        colors = ButtonDefaults.buttonColors(),
        enabled = enabled,
        modifier = Modifier.fillMaxWidth()
    ) {
        Text(label, style = MaterialTheme.typography.displaySmall,
            modifier = Modifier.padding(10.dp))
    }
}
```
Here `authType` comes in handy, as we can show an appropriate label for a button.

---
After that follows a "go to text" which would enable a user to transition between desired auth states

```kotlin
@Composable
fun GoToText(authType: AuthType,
             enabled: Boolean,
             onClick: () -> Unit) {
    val label = stringResource(
        id = if (authType == AuthType.SIGN_IN) R.string.go_to_signup else R.string.go_to_signin
    )
    TextButton(onClick = onClick,
        enabled = enabled) {
        Text(label, style = MaterialTheme.typography.labelSmall.copy(
            textDecoration = TextDecoration.Underline
        ))
    }
}
```
Here we show "Go to sign up" text if a user is in sign in state. And vise versa.

---
Now we combine all the composables above into a single card

```kotlin
@Composable
fun AuthFieldsColumn(
    uiState: AuthViewModel.UiState,
    authEnabled: Boolean,
    onUsername: (String) -> Unit,
    onEmail: (String) -> Unit,
    onPassword: (String) -> Unit,
    onChangeAuthType: () -> Unit,
    onAuth: () -> Unit
) {
    val shape = RoundedCornerShape(dimensionResource(id = R.dimen.auth_corner))
    val validationState = uiState.validationState
    val authState = uiState.authState
    val authResult = uiState.authResult
    val context = LocalContext.current
    val usernameValidationError = validationState.usernameValidationError.asString(context)
    val emailValidationError = validationState.emailValidationError.asString(context)
    val passwordValidationError = validationState.passwordValidationError.asString(context)
    val authButtonEnabled = (usernameValidationError.isEmpty() && authState.username != null || uiState.authType != AuthType.SIGN_UP) &&
            (emailValidationError.isEmpty() && authState.email != null) &&
            (passwordValidationError.isEmpty() && authState.password != null) && authEnabled
    ElevatedCard(
        modifier = Modifier
            .width(350.dp)
            .shadow(
                dimensionResource(id = R.dimen.shadow_elevation),
                shape = shape,
                spotColor = MaterialTheme.colorScheme.primary
            )
            .clip(shape)
            .background(MaterialTheme.colorScheme.onBackground.copy(0.1f))
            .border(1.dp, MaterialTheme.colorScheme.primary, shape)
    ) {
        Column(
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.spacedBy(20.dp),
            modifier = Modifier
                .fillMaxWidth()
                .padding(10.dp)
        ) {
            AnimatedVisibility(uiState.authType == AuthType.SIGN_UP) {
                CustomInputField(
                    fieldType = FieldType.USERNAME,
                    enabled = authEnabled,
                    value = authState.username,
                    error = usernameValidationError,
                    onValueChange = onUsername)
            }
            CustomInputField(
                fieldType = FieldType.EMAIL,
                enabled = authEnabled,
                value = authState.email,
                error = emailValidationError,
                onValueChange = onEmail)
            CustomInputField(
                fieldType = FieldType.PASSWORD,
                enabled = authEnabled,
                value = authState.password,
                error = passwordValidationError,
                onValueChange = onPassword)
            CustomAuthButton(
                authType = uiState.authType,
                enabled = authButtonEnabled,
                onClick = onAuth)
            if (authResult is CustomResult.InProgress) {
                LinearProgressIndicator()
            }
            Column(horizontalAlignment = Alignment.CenterHorizontally) {
                if (uiState.authType == AuthType.SIGN_IN) {
                    Text(stringResource(id = R.string.dont_have_an_account),
                        style = MaterialTheme.typography.labelSmall)
                }
                GoToText(authType = uiState.authType,
                    enabled = authEnabled,
                    onClick = onChangeAuthType)
            }
        }
    }
}
```
In the code above `authButtonEnabled` will be true in 2 cases: either if a current auth option is sign in and email with password pass the input check, or if a current option is sign up and username together with email and password pass the check. Also, username text field will be shown only on sign up.

---

After that I've defined another composable that accepts ui state and callback methods. This is useful for previewing composables.

```kotlin
@Composable
fun AuthContentColumn(
    uiState: AuthViewModel.UiState,
    onUsername: (String) -> Unit,
    onEmail: (String) -> Unit,
    onPassword: (String) -> Unit,
    onChangeAuthType: () -> Unit,
    onCustomAuth: () -> Unit,
    onSignGoogleSignIn: suspend () -> IntentSender?,
    onSignInWithIntent: (ActivityResult) -> Unit) {
    val authResult = uiState.authResult
    val authEnabled = authResult !is CustomResult.InProgress && authResult !is CustomResult.Success
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.spacedBy(40.dp),
        modifier = Modifier
            .fillMaxSize()
            .verticalScroll(rememberScrollState())
            .padding(10.dp)
    ) {
        Title()
        AuthFieldsColumn(
            uiState = uiState,
            authEnabled = authEnabled,
            onUsername = onUsername,
            onEmail = onEmail,
            onPassword = onPassword,
            onChangeAuthType = onChangeAuthType,
            onAuth = onCustomAuth)
    }
}
```

---
Next create a top auth screen composable

```kotlin
@Composable
fun AuthScreen(
    onSignIn: () -> Unit,
    viewModel: AuthViewModel = hiltViewModel()) {
    val uiState by viewModel.uiState.collectAsState()
    val focusManger = LocalFocusManager.current
    val snackbarController = LocalSnackbarController.current
    LaunchedEffect(uiState.authResult) {
        if (uiState.authResult is CustomResult.Success) {
            focusManger.clearFocus(true)
            onSignIn()
        }
    }
    LaunchedEffect(uiState.authResult) {
        snackbarController.showErrorSnackbar(uiState.authResult)
    }
    AuthContentColumn(
        uiState = uiState,
        onUsername = viewModel::onUsername,
        onEmail = viewModel::onEmail,
        onPassword = viewModel::onPassword,
        onChangeAuthType = viewModel::changeAuthType,
        onCustomAuth = viewModel::onCustomAuth,
        onSignGoogleSignIn = viewModel::onGoogleSignIn,
        onSignInWithIntent = viewModel::onSignInWithIntent,
    )
}
```
On successful authentication, the composable clears keyboard focus and calls `onSignIn` callback, which is usually responsible for navigating to another screen. On unsuccessful authentication, an error snackbar is shown. Here's the code for it:

In `MainActivity.kt` create a `LocalSnackbarController` variable

```kotlin
val LocalSnackbarController = compositionLocalOf<SnackbarController> {
    error("No snackbar host state provided")
}
```
Then inside of `setContent` function wrap the app's content with `CompositionLocalProvider` and define a dismissable snackbar. To learn more about `CompositionLocalProvider`, visit [this page](https://developer.android.com/develop/ui/compose/compositionlocal)
```kotlin
setContent {
    val snackbarHostState = remember {
        SnackbarHostState()
    }
    val swipeToDismissBoxState = rememberSwipeToDismissBoxState(confirmValueChange = {value ->
        if (value != SwipeToDismissBoxValue.Settled) {
            snackbarHostState.currentSnackbarData?.dismiss()
            true
        } else false
    })
    val snackbarController by remember(snackbarHostState) {
        mutableStateOf(SnackbarController(snackbarHostState, lifecycleScope, applicationContext))
    }
    LaunchedEffect(swipeToDismissBoxState.currentValue) {
        if (swipeToDismissBoxState.currentValue != SwipeToDismissBoxValue.Settled) {
            swipeToDismissBoxState.reset()
        }
    }
    MoneyMonocleTheme(darkTheme = isThemeDark.value) {
        CompositionLocalProvider(LocalSnackbarController provides snackbarController) {
            Surface(
                Modifier
                    .fillMaxSize()
                    .background(MaterialTheme.colorScheme.background)
            ) {
                Box(
                    Modifier
                        .fillMaxSize()
                        .imePadding()
                ) {
                    MoneyMonocleNavHost(
                        navController = navController
                    )
                    CustomErrorSnackbar(snackbarHostState = snackbarHostState,
                        swipeToDismissBoxState = swipeToDismissBoxState)
                }
            }
        }

    }
}
```
After that in a separate file create `SnackbarController` with `CustomErrorSnackbar`

```kotlin
class SnackbarController(
    private val snackbarHostState: SnackbarHostState,
    private val coroutineScope: CoroutineScope,
    private val context: Context,
) {
    fun showErrorSnackbar(result: CustomResult) {
        if (result is CustomResult.DynamicError || result is CustomResult.ResourceError) {
            coroutineScope.launch {
                snackbarHostState.currentSnackbarData?.dismiss()
                snackbarHostState.showSnackbar(result.error.asString(context))
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomErrorSnackbar(snackbarHostState: SnackbarHostState,
                        swipeToDismissBoxState: SwipeToDismissBoxState) {
    SwipeToDismissBox(state = swipeToDismissBoxState, backgroundContent = {}) {
        SnackbarHost(hostState = snackbarHostState,
                    snackbar = {data ->
                Snackbar(
                    containerColor = MaterialTheme.colorScheme.errorContainer,
                    modifier = Modifier
                        .padding(20.dp)
                ) {
                    Row(
                        verticalAlignment = Alignment.CenterVertically,
                        modifier = Modifier.fillMaxWidth()
                    ) {
                        Text(data.visuals.message,
                            color = MaterialTheme.colorScheme.onErrorContainer,
                            modifier = Modifier.testTag(stringResource(id = R.string.error_snackbar)))
                    }
                }
            })
        }
}
```
And that's all there is to it! Good luck on your Android Development journey.