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
}
}
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)
)
}
}
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))
}
}
State Management with rememberSaveable
Preserve password visibility state across recompositions:
var isPasswordVisible by rememberSaveable { mutableStateOf(false) }
var password by rememberSaveable { mutableStateOf("") }
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")
}
}
}
Best Practices
- Never log passwords - exclude from debug logs
- Use proper hashing - on backend with bcrypt/Argon2
- Validate on both sides - frontend UX + backend security
- Consider entropy - score system can be enhanced with zxcvbn library
- 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)