DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Password Strength Checker in Compose: Validation UI with Indicator

Password Strength Checker in Compose: Validation UI with Indicator

Building a robust password strength validator in Jetpack Compose requires combining real-time input validation with visual feedback. This guide demonstrates a production-ready implementation using score-based strength enums and Material Design indicators.

Core Architecture

Strength Enum with Scoring Logic

Define a sealed class to represent strength levels with associated colors and messages:

sealed class PasswordStrength(val score: Int, val label: String) {
    object Weak : PasswordStrength(score = 1, label = "Weak")
    object Fair : PasswordStrength(score = 2, label = "Fair")
    object Good : PasswordStrength(score = 3, label = "Good")
    object Strong : PasswordStrength(score = 4, label = "Strong")
}

fun calculateStrength(password: String): PasswordStrength {
    var score = 0
    if (password.length >= 8) score++
    if (password.any { it.isUpperCase() }) score++
    if (password.any { it.isDigit() }) score++
    if (password.any { !it.isLetterOrDigit() }) score++

    return when (score) {
        1 -> PasswordStrength.Weak
        2 -> PasswordStrength.Fair
        3 -> PasswordStrength.Good
        else -> PasswordStrength.Strong
    }
}
Enter fullscreen mode Exit fullscreen mode

UI Implementation

Password Input with Visual Feedback

Combine TextField with LinearProgressIndicator to provide real-time strength feedback:

@Composable
fun PasswordStrengthField(
    password: String,
    onPasswordChange: (String) -> Unit,
    modifier: Modifier = Modifier
) {
    var isVisible by rememberSaveable { mutableStateOf(false) }
    val strength = calculateStrength(password)

    Column(modifier = modifier.padding(16.dp)) {
        TextField(
            value = password,
            onValueChange = onPasswordChange,
            label = { Text("Password") },
            visualTransformation = if (isVisible) VisualTransformation.None else PasswordVisualTransformation(),
            trailingIcon = {
                IconButton(onClick = { isVisible = !isVisible }) {
                    Icon(
                        imageVector = if (isVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff,
                        contentDescription = "Toggle visibility"
                    )
                }
            },
            modifier = Modifier.fillMaxWidth()
        )

        // Strength Progress Indicator
        LinearProgressIndicator(
            progress = strength.score / 4f,
            color = when (strength) {
                is PasswordStrength.Weak -> Color.Red
                is PasswordStrength.Fair -> Color(0xFFFFA500)
                is PasswordStrength.Good -> Color.Yellow
                is PasswordStrength.Strong -> Color.Green
            },
            modifier = Modifier
                .fillMaxWidth()
                .padding(top = 8.dp)
        )

        Text(
            text = "Strength: ${strength.label}",
            style = MaterialTheme.typography.bodySmall,
            modifier = Modifier.padding(top = 4.dp)
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

Requirement Checklist

Display a checklist of password requirements with dynamic validation:

@Composable
fun PasswordRequirements(password: String) {
    val hasLength = password.length >= 8
    val hasUpper = password.any { it.isUpperCase() }
    val hasDigit = password.any { it.isDigit() }
    val hasSymbol = password.any { !it.isLetterOrDigit() }

    Column(modifier = Modifier.padding(16.dp)) {
        PasswordRequirement("At least 8 characters", hasLength)
        PasswordRequirement("Contains uppercase letter", hasUpper)
        PasswordRequirement("Contains digit", hasDigit)
        PasswordRequirement("Contains special character", hasSymbol)
    }
}

@Composable
fun PasswordRequirement(text: String, isMet: Boolean) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier.padding(8.dp)
    ) {
        Icon(
            imageVector = if (isMet) Icons.Default.Check else Icons.Default.Close,
            contentDescription = null,
            tint = if (isMet) Color.Green else Color.Red
        )
        Text(text = text, modifier = Modifier.padding(start = 8.dp))
    }
}
Enter fullscreen mode Exit fullscreen mode

State Management with rememberSaveable

Preserve password visibility state across recompositions:

var isPasswordVisible by rememberSaveable { mutableStateOf(false) }
var password by rememberSaveable { mutableStateOf("") }
Enter fullscreen mode Exit fullscreen mode

Complete Example

@Composable
fun PasswordValidationScreen() {
    var password by rememberSaveable { mutableStateOf("") }

    Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
        PasswordStrengthField(
            password = password,
            onPasswordChange = { password = it }
        )

        PasswordRequirements(password)

        Button(
            onClick = { /* Handle submission */ },
            enabled = calculateStrength(password) is PasswordStrength.Strong,
            modifier = Modifier
                .align(Alignment.End)
                .padding(top = 24.dp)
        ) {
            Text("Create Account")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Best Practices

  1. Never log passwords - exclude from debug logs
  2. Use proper hashing - on backend with bcrypt/Argon2
  3. Validate on both sides - frontend UX + backend security
  4. Consider entropy - score system can be enhanced with zxcvbn library
  5. Accessibility - provide text descriptions alongside visual indicators

Next Steps

  • Implement server-side password validation
  • Add password breach checking (HaveIBeenPwned API)
  • Consider biometric authentication for sensitive operations

8 Android App Templates → https://myougatheax.gumroad.com

Top comments (0)