Simple Login Page in Jetpack Compose

WhiteBatCodes
7 min readMay 19

--

This post is based on my YouTube video that you can find here or at the end of this post

In this post We’ll see how to create the simple login interface using JetPack Compose.

After you create an empty Jetpack project, you’ll have the default MainActivity.kt class. We’ll make some changes to the preview. We’ll use it later for the Login Page. The MainActivity will serve as the page that the user can access to after they successfully login.

Let’s add at the bottom two preview functions :

@Preview(showBackground = true, device = "id:Nexus One", showSystemUi = true)
@Composable
fun GreetingPreview() {
MyLoginApplicationTheme {
Greeting("Android")
}
}

@Preview(showBackground = true, device = "id:Nexus One", showSystemUi = true)
@Composable
fun GreetingPreviewDark() {
MyLoginApplicationTheme(darkTheme = true) {
Greeting("Android")
}
}

The dark theme looks no different from the light mode because our preview doesn’t use the Surface as a parent. Let’s update the Greetings() method to make that happen

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Surface {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
}
}

This is enough to update the dark them preview and also to center everything in the middle of the screen.

you can also Update the onCreate() method of the MainActivity.kt class to show the changes in your application after running it

setContent {
MyLoginApplicationTheme {
Greeting("Android")
}
}

Enough with the MainActivity, let’s now create our LoginActivity.kt class
I will let you decide on the best approach you want to do it but my result looks like this after the creation :

package com.whitebatcodes.myloginapplication

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.whitebatcodes.myloginapplication.interfaces.LoginForm
import com.whitebatcodes.myloginapplication.ui.theme.MyLoginApplicationTheme

class LoginActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContent{

}
}
}

in the setContent block, add the following

MyLoginApplicationTheme {
LoginForm()
}

Now we’ll have to define the LoginForm() method somewhere. I chose to create a new Kotlin file where I’ll have all my views. The form will contain: a Login Field, a Password Field, a checkbox with a label and a button. Let’s define each element separately:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LoginField(
value: String,
onChange: (String) -> Unit,
modifier: Modifier = Modifier,
label: String = "Login",
placeholder: String = "Enter your Login"
) {

val focusManager = LocalFocusManager.current
val leadingIcon = @Composable {
Icon(
Icons.Default.Person,
contentDescription = "",
tint = MaterialTheme.colorScheme.primary
)
}

TextField(
value = value,
onValueChange = onChange,
modifier = modifier,
leadingIcon = leadingIcon,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(
onNext = { focusManager.moveFocus(FocusDirection.Down) }
),
placeholder = { Text(placeholder) },
label = { Text(label) },
singleLine = true,
visualTransformation = VisualTransformation.None
)
}

The LoginField() method contains uses a leading icon that you can find in the default Icons class, the Field also changes the IME to Next so that we move the focus to the next field, in this case the password field. We’ll need to use the focus manager for that. We also added a label and a placehoder with the default values passed as parameters, made the field single line and didn’t set any visual transformation. This parameter is relevant for the Password field.

The password field will require some icons that don’t exist in the default Icons class. Go to your build.gradle (app) and add the following implementation :

implementation "androidx.compose.material:material-icons-extended:1.4.3"

The password field method is as follows :

@Composable
fun PasswordField(
value: String,
onChange: (String) -> Unit,
submit: () -> Unit,
modifier: Modifier = Modifier,
label: String = "Password",
placeholder: String = "Enter your Password"
) {

var isPasswordVisible by remember { mutableStateOf(false) }

val leadingIcon = @Composable {
Icon(
Icons.Default.Key,
contentDescription = "",
tint = MaterialTheme.colorScheme.primary
)
}
val trailingIcon = @Composable {
IconButton(onClick = { isPasswordVisible = !isPasswordVisible }) {
Icon(
if (isPasswordVisible) Icons.Default.VisibilityOff else Icons.Default.Visibility,
contentDescription = "",
tint = MaterialTheme.colorScheme.primary
)
}
}


TextField(
value = value,
onValueChange = onChange,
modifier = modifier,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
keyboardType = KeyboardType.Password
),
keyboardActions = KeyboardActions(
onDone = { submit() }
),
placeholder = { Text(placeholder) },
label = { Text(label) },
singleLine = true,
visualTransformation = if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation()
)
}

We don’t need a focus manager for the password as the IME button is Done and we’ll submit the form. We’ll also need a mutable variable to make to recompose the field if the password is visible. this is controlled using the IconButton configured in the trailingIcon attribute. We’ll also change the keyboard type to disable the auto correct and the clipboard suggestions. We’ll also configure the visual transformation based on the variable “isPasswordVisible” to either show or hide the password. We’ll also add a new functions: submit() that will be triggered when the done button in the keyboard is pressed.

We can preview this by changing the method LoginForm()

@Composable
fun LoginForm() {
Surface {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 30.dp)
) {
LoginField(
value = "login",
onChange = { },
modifier = Modifier.fillMaxWidth()
)
PasswordField(
value = "password",
onChange = { },
submit = { },
modifier = Modifier.fillMaxWidth()
)
}
}
}

Now we will have to add a checkbox to remember the user login. We will not define saving the login nor the authentication process in this tutorial.

For the labeled checkbox, the method should look like this:

@Composable
fun LabeledCheckbox(
label: String,
onCheckChanged: () -> Unit,
isChecked: Boolean
) {

Row(
Modifier
.clickable(
onClick = onCheckChanged
)
.padding(4.dp)
) {
Checkbox(checked = isChecked, onCheckedChange = null)
Spacer(Modifier.size(6.dp))
Text(label)
}
}

The method basically doesn’t use the checkchange of the checkbox itself as it forces the user to select the checkbox and the text will not be considered in the check. That’s why we forced the checkchange in the Row that hosts the checkbox and the label inside a Text composable.

After updating the LoginForm() method, it should look like this :

@Composable
fun LoginForm() {
Surface {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 30.dp)
) {
LoginField(
value = "login",
onChange = { },
modifier = Modifier.fillMaxWidth()
)
PasswordField(
value = "password",
onChange = { },
submit = { },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(10.dp))
LabeledCheckbox(
label = "Remember Me",
onCheckChanged = { },
isChecked = false
)
}
}
}

The last thing to add to the view is the Button and we can add it directly to the LoginForm() method as follows:

@Composable
fun LoginForm() {
Surface {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 30.dp)
) {
LoginField(
value = "login",
onChange = { },
modifier = Modifier.fillMaxWidth()
)
PasswordField(
value = "password",
onChange = { },
submit = { },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(10.dp))
LabeledCheckbox(
label = "Remember Me",
onCheckChanged = { },
isChecked = false
)
Button(
onClick = { },
enabled = true,
shape = RoundedCornerShape(5.dp),
modifier = Modifier.fillMaxWidth()
) {
Text("Login")
}
}
}
}

Let’s add some logic now to store the data from the form and use it to submit the form and check the credentials. First, let’s add a data class:

data class Credentials(
var login: String = "",
var pwd: String = "",
var remember: Boolean = false
) {
fun isNotEmpty(): Boolean {
return login.isNotEmpty() && pwd.isNotEmpty()
}
}

Then, let’s define a method checkCredentials() that should check the credentials and then allow the user to navigate to the MainActivity view. Here, the condition is just not to be empty and the login to be admin:

fun checkCredentials(creds: Credentials, context: Context): Boolean {
if (creds.isNotEmpty() && creds.login == "admin") {
context.startActivity(Intent(context, MainActivity::class.java))
(context as Activity).finish()
return true
} else {
Toast.makeText(context, "Wrong Credentials", Toast.LENGTH_SHORT).show()
return false
}
}

notice that we need a context for this method, so we’ll have to pass it as a parameter as well as have a credentials variable of type Credentials in our LoginPage. Let’s make those changes by adding to variables :

var credentials by remember { mutableStateOf(Credentials()) }
val context = LocalContext.current

If you’re struggling with the imports, here they are :

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue

Now let’s update our LoginPage() method to add the value of the Fields, setup the change methods and configure the submit method

@Composable
fun LoginForm() {
Surface {
var credentials by remember { mutableStateOf(Credentials()) }
val context = LocalContext.current

Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 30.dp)
) {
LoginField(
value = credentials.login,
onChange = { data -> credentials = credentials.copy(login = data) },
modifier = Modifier.fillMaxWidth()
)
PasswordField(
value = credentials.pwd,
onChange = { data -> credentials = credentials.copy(pwd = data) },
submit = {
if (!checkCredentials(credentials, context)) credentials = Credentials()
},
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(10.dp))
LabeledCheckbox(
label = "Remember Me",
onCheckChanged = {
credentials = credentials.copy(remember = !credentials.remember)
},
isChecked = credentials.remember
)
Spacer(modifier = Modifier.height(20.dp))
Button(
onClick = {
if (!checkCredentials(credentials, context)) credentials = Credentials()
},
enabled = credentials.isNotEmpty(),
shape = RoundedCornerShape(5.dp),
modifier = Modifier.fillMaxWidth()
) {
Text("Login")
}
}
}
}

In this code, if the credentials don’t match, the fields will be emptied. We used a copy of the credentials because it’s an object and if we only change the variables inside, Compose will not detect it because the object itself didn’t change. So we’re creating a copy and we’re changing the element that changed.

And we’re done! You can run your application to see how that looks like.

The GitHub code can be found here : GitHub repo

Here’s a YouTube video if you want to follow the same steps more visually :

--

--

WhiteBatCodes

A software engineer👨‍💻 with a passion for IT 📱💻 and cyber security 🔐.