Firebase Auth with Jetpack Compose

Firebase Auth with Jetpack Compose

Introduction

Jetpack Compose is Android’s modern toolkit for building native UI. It simplifies and accelerates UI development on Android. Quickly bring your app to life with less code, powerful tools, and intuitive Kotlin APIs.

At the moment of writing this article, jetpack compose is in beta, and the stable version is planned for next month, so it is high time to start learning jetpack compose

Installation

For the best experience developing with Jetpack Compose, you should download the latest version of Android Studio Arctic Fox . That’s because when you use Android Studio to develop your app with Jetpack Compose, you can benefit from smart editor features, such as New Project templates and the ability to immediately preview your Compose UI.

Configure Kotlin

Make sure you're using Kotlin 1.5.10 or newer in your project, the latest version of Kotlin is not compatible yet with Jetpack Compose :

plugins {
    id("org.jetbrains.kotlin.android") version "1.5.10"
}

Configure Gradle

android {
    defaultConfig {
        ...
        minSdkVersion(21)
    }

    buildFeatures {
        // Enables Jetpack Compose for this module
        compose = true
    }
    ...

    // Set both the Java and Kotlin compilers to target Java 8.

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = "1.8"
        useIR = true
    }

    composeOptions {
        kotlinCompilerVersion = "1.5.10"
        kotlinCompilerExtensionVersion = "1.0.0-beta09"
    }
}

Add Jetpack Compose toolkit dependencies

Include Jetpack Compose toolkit dependencies in your app’s build.gradle file, as shown below:

dependencies {
    implementation("androidx.compose.ui:ui:1.0.0-beta09")
    implementation("androidx.compose.ui:ui-tooling:1.0.0-beta09")
    implementation("androidx.compose.foundation:foundation:1.0.0-beta09")
    implementation("androidx.compose.material:material:1.0.0-beta09")

   //  For ViewModel
   implementation("androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07")

    // For Interoperability
    implementation("androidx.activity:activity-compose:1.0.0-beta09")
}

Once the installation of Jetpack Composer is completed, you must now configure firebase in your project, details on the configuration of Firebase are available on the following link, Firebase Setup

In order to use Firebase in our project we have to have those dependency

dependencies {
    implementation(platform("com.google.firebase:firebase-bom:28.2.0"))
    implementation("com.google.firebase:firebase-auth")
    implementation("com.google.android.gms:play-services-auth:19.0.0")

    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.5.0")
}

Implementation

For simplicity reasons, we are not going to develop all the architecture of the application because the objective is simply to show how to make the authentication with Firebase with Jetpack Compose, and we will implement authentication with email and passwords and then authentication with google

User Interface

Screenshot_20210626-125023_firebase-auth-compose.jpg

The code that generates this interface looks like, there are only two input fields and two buttons

@Composable
fun LoginScreen(viewModel: LoginScreenViewModel = viewModel()) {

  var userEmail by remember { mutableStateOf("") }
  var userPassword by remember { mutableStateOf("") }

  val snackbarHostState = remember { SnackbarHostState() }
  val state by viewModel.loadingState.collectAsState()

  // Equivalent of onActivityResult
  val launcher = rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) {
    val task = GoogleSignIn.getSignedInAccountFromIntent(it.data)
    try {
      val account = task.getResult(ApiException::class.java)!!
      val credential = GoogleAuthProvider.getCredential(account.idToken!!, null)
      viewModel.signWithCredential(credential)
    } catch (e: ApiException) {
      Log.w("TAG", "Google sign in failed", e)
    }
  }

  Scaffold(
    scaffoldState = rememberScaffoldState(snackbarHostState = snackbarHostState),
    topBar = {
      Column(modifier = Modifier.fillMaxWidth()) {
        TopAppBar(
          backgroundColor = Color.White,
          elevation = 1.dp,
          title = {
            Text(text = "Login")
          },
          navigationIcon = {
            IconButton(onClick = { /*TODO*/ }) {
              Icon(
                imageVector = Icons.Rounded.ArrowBack,
                contentDescription = null,
              )
            }
          },
          actions = {
            IconButton(onClick = { Firebase.auth.signOut() }) {
              Icon(
                imageVector = Icons.Rounded.ExitToApp,
                contentDescription = null,
              )
            }
          }
        )
        if (state.status == LoadingState.Status.RUNNING) {
          LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
        }
      }
    },
    content = {
      Column(
        modifier = Modifier
          .fillMaxSize()
          .padding(24.dp),
        verticalArrangement = Arrangement.spacedBy(18.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        content = {
          OutlinedTextField(
            modifier = Modifier.fillMaxWidth(),
            value = userEmail,
            label = {
              Text(text = "Email")
            },
            onValueChange = {
              userEmail = it
            }
          )

          OutlinedTextField(
            modifier = Modifier.fillMaxWidth(),
            visualTransformation = PasswordVisualTransformation(),
            value = userPassword,
            label = {
              Text(text = "Password")
            },
            onValueChange = {
              userPassword = it
            }
          )

          Button(
            modifier = Modifier.fillMaxWidth().height(50.dp),
            enabled = userEmail.isNotEmpty() && userPassword.isNotEmpty(),
            content = {
              Text(text = "Login")
            },
            onClick = {
              viewModel.signInWithEmailAndPassword(userEmail.trim(), userPassword.trim())
            }
          )

          Text(
            modifier = Modifier.fillMaxWidth(),
            textAlign = TextAlign.Center,
            style = MaterialTheme.typography.caption,
            text = "Login with"
          )

          Spacer(modifier = Modifier.height(18.dp))

          val context = LocalContext.current
          val token = stringResource(R.string.default_web_client_id)

          OutlinedButton(
            border = ButtonDefaults.outlinedBorder.copy(width = 1.dp),
            modifier = Modifier.fillMaxWidth().height(50.dp),
            onClick = {
              val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
                .requestIdToken(token)
                .requestEmail()
                .build()

              val googleSignInClient = GoogleSignIn.getClient(context, gso)
              launcher.launch(googleSignInClient.signInIntent)
            },
            content = {
              Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.SpaceBetween,
                verticalAlignment = Alignment.CenterVertically,
                content = {
                  Icon(
                    tint = Color.Unspecified,
                    painter = painterResource(id = R.drawable.googleg_standard_color_18),
                    contentDescription = null,
                  )
                  Text(
                    style = MaterialTheme.typography.button,
                    color = MaterialTheme.colors.onSurface,
                    text = "Google"
                  )
                  Icon(
                    tint = Color.Transparent,
                    imageVector = Icons.Default.MailOutline,
                    contentDescription = null,
                  )
                }
              )
            }
          )

          when(state.status) {
            LoadingState.Status.SUCCESS -> {
              Text(text = "Success")
            }
            LoadingState.Status.FAILED -> {
              Text(text = state.msg ?: "Error")
            }
            else -> {}
          }
        }
      )
    }
  )
}

The button that launches the authentication with email is password is relatively simple, but the one that takes care of the authentication with seems a bit complicated and we will explain it

Activity Result

Screen_Recording_20210626-133408_firebase-auth-compose_1.gif

When we need to authenticate with Google if we were in a Classic Android application, at some level we would need the onActivityResult method which is called after the authentication, but with Jetpack Compose there is no more this method but there is an alternative as the following code shows

  val launcher = rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) {
    val task = GoogleSignIn.getSignedInAccountFromIntent(it.data)
    try {
      val account = task.getResult(ApiException::class.java)!!
      val credential = GoogleAuthProvider.getCredential(account.idToken!!, null)
      viewModel.signWithCredential(credential)
    } catch (e: ApiException) {
      Log.w("TAG", "Google sign in failed", e)
    }
  }

This piece of code is the equivalent of the onActivityResult method, the code in the lambda is almost identical to the one we could use in the onActivityResult method.

Auth with Google

To authenticate with Google you just have to create an Intent to pass it to the launch method as shown in the following code which is in the lambda onClick

val context = LocalContext.current
val token = stringResource(R.string.default_web_client_id)

OutlinedButton(
    border = ButtonDefaults.outlinedBorder.copy(width = 1.dp),
    modifier = Modifier.fillMaxWidth().height(50.dp),
     onClick = {
         val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
            .requestIdToken(token)
            .requestEmail()
            .build()

         val googleSignInClient = GoogleSignIn.getClient(context, gso)
         launcher.launch(googleSignInClient.signInIntent)
      },
     ....
 )

The GoogleSignIn.getClient method need the context, and in order to have the context you have just to use LocalContext.current

The VIewModel

The ViewModel is relatively simple, to facilitate the task, we use in the viewModel a util class to have an idea related to the state of an operation launched by the viewModel

The ViewModel Code

class LoginScreenViewModel : ViewModel() {

  val loadingState = MutableStateFlow(LoadingState.IDLE)

  fun signInWithEmailAndPassword(email: String, password: String) = viewModelScope.launch {
    try {
      loadingState.emit(LoadingState.LOADING)
      Firebase.auth.signInWithEmailAndPassword(email, password).await()
      loadingState.emit(LoadingState.LOADED)
    } catch (e: Exception) {
      loadingState.emit(LoadingState.error(e.localizedMessage))
    }
  }

  fun signWithCredential(credential: AuthCredential) = viewModelScope.launch {
    try {
      loadingState.emit(LoadingState.LOADING)
      Firebase.auth.signInWithCredential(credential).await()
      loadingState.emit(LoadingState.LOADED)
    } catch (e: Exception) {
      loadingState.emit(LoadingState.error(e.localizedMessage))
    }
  }
}

Util class

data class LoadingState private constructor(val status: Status, val msg: String? = null) {
  companion object {
    val LOADED = LoadingState(Status.SUCCESS)
    val IDLE = LoadingState(Status.IDLE)
    val LOADING = LoadingState(Status.RUNNING)
    fun error(msg: String?) = LoadingState(Status.FAILED, msg)
  }

  enum class Status {
    RUNNING,
    SUCCESS,
    FAILED,
    IDLE,
  }
}

Authenticating Your Client

After implementing the authentication with Google, there is a strong chance that the authentication will not succeed, to solve the problem you need to add the SHA-1 of your application to firebase console as explained here

Conclusion

As I mentioned above, Jetpack Compose is currently in beta version, so it's a good time to start learning how to use it, I hope this little article will make you want to get started with Jetpack Compose.

The full code is available in the link I put in the reference section.

Reference

Did you find this article valuable?

Support Eric Ampire by becoming a sponsor. Any amount is appreciated!