Build a login screen in Jetpack Compose and move to a dashboard after signing in. You will learn how Compose screens hold state and how Navigation Compose moves between them.
In Android Studio choose New Project → Empty Activity (the one with the Compose logo). Set Minimum SDK to API 24. The project comes with Compose, Material 3 and a theme ready to use.
2. Add Navigation Compose
In app/build.gradle.kts, inside dependencies { }, add:
The icons line needs no version: the Compose BoM already in your project picks it. Press Sync Now.
3. Hold the form in state
In LoginScreen, keep username, password, showPassword and error with rememberSaveable { mutableStateOf(...) }. Each text field shows its value and updates it in onValueChange.
4. Build the fields
Use OutlinedTextField with a label and a leadingIcon (a person and a lock). singleLine = true keeps each on one line.
5. Hide and show the password
visualTransformation = PasswordVisualTransformation() shows dots. A trailingIcon eye button flips showPassword, switching to VisualTransformation.None so the user can check what they typed.
6. Use the keyboard's keys
ImeAction.Next on the username moves to the password. ImeAction.Done on the password, with keyboardActions = KeyboardActions(onDone = { logIn() }), logs in straight from the keyboard.
7. Check the form
loginError(username, password) returns a message, or null when all is well. Keeping the rules in a plain function makes them easy to read and change. Show the message with isError and supportingText under the password field. This demo checks a fixed account (user / 123456).
8. Go to the dashboard
On success, navController.navigate("dashboard/user") opens the dashboard and passes the username in the route. popUpTo("login") { inclusive = true } removes the login screen, so Back closes the app instead of returning to the form.
9. Log out
Log out navigates back to "login" and removes the dashboard from the back stack the same way.
10. Run it
Try an empty form, a wrong password, then user / 123456. Press Back on the dashboard.
The latest Compose and Navigation, a show/hide password button, icons, keyboard actions, clear error messages under the field, a back stack that behaves like a real app, and state that survives rotation.
Files affected
Check what to add and what to replace before copying code.
The NavHost: login, then dashboard, with the back stack cleared each way.
MainActivity.kt
package com.example.composelogin
import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
class MainActivity :ComponentActivity(){overridefunonCreate(savedInstanceState: Bundle?){super.onCreate(savedInstanceState)enableEdgeToEdge()
setContent {
ComposeLoginTheme {App()}}}}/**
* Login and Dashboard. After logging in, the login screen is removed from the back stack,
* so Back on the dashboard closes the app instead of returning to the form.
*/@ComposablefunApp(){val navController =rememberNavController()NavHost(navController = navController, startDestination ="login"){composable("login"){LoginScreen(onLoggedIn ={ username ->
navController.navigate("dashboard/"+ Uri.encode(username)){popUpTo("login"){ inclusive =true}}})}composable("dashboard/{username}"){ entry ->DashboardScreen(
username = entry.arguments?.getString("username").orEmpty(),
onLogOut ={
navController.navigate("login"){popUpTo("dashboard/{username}"){ inclusive =true}}},)}}}