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