Biometric Authentication in Android 16
Biometric Authentication in Jetpack Compose (Fingerprint & Face Login)
In today’s mobile-first world, security is not optional—it is an expectation. Users want apps that are fast, convenient, and secure without forcing them to remember complex passwords. This is where biometric authentication becomes a game changer.
Android has supported biometrics such as fingerprint, face recognition, and iris scanning for years. However, with the rise of Jetpack Compose, Android’s modern declarative UI toolkit, developers often ask:
This article answers that question deeply—not just with code, but with conceptual clarity, real-world architecture, and best practices. By the end, you will understand how biometrics work, why Compose changes the implementation approach, and how to build a secure, user-friendly biometric login flow.
What Is Biometric Authentication?
Biometric authentication verifies a user’s identity using biological traits, such as:
- Fingerprint
- Face recognition
- Iris scan
- Device credentials (PIN, pattern, password as fallback)
Unlike passwords:
- Biometrics cannot be forgotten
- They are harder to replicate
- They offer faster authentication
Android provides a unified API through BiometricPrompt, which abstracts device-specific implementations and ensures consistent behavior.
Why Biometrics Matter in Modern Apps
Let’s understand the why before the how.
Problems with Traditional Authentication
- Users forget passwords
- Weak passwords reduce security
- Frequent logins hurt user experience
- Password reuse increases attack risk
Advantages of Biometrics
✅ Faster login
✅ Better UX
✅ Stronger security
✅ Less friction
✅ Trusted by users
That’s why banking apps, payment apps, and secure enterprise apps rely heavily on biometric authentication.
Jetpack Compose and Authentication: A Paradigm Shift
Jetpack Compose is declarative, while biometric APIs are imperative and callback-based. This mismatch creates confusion for many developers.
Key Differences
| XML-based UI | Jetpack Compose |
|---|---|
| UI updated manually | UI reacts to state |
| Callbacks modify views | State drives UI |
| Lifecycle tied to Views | Lifecycle-aware composables |
1. BiometricManager
2. BiometricPrompt
3. Executor
Real-World Authentication Flow
- User opens app
- App checks biometric availability
- If available → show biometric prompt
- On success → unlock app
- On failure → retry or fallback
- On error → show message
What is biometric authentication in Android?
This tutorial follows real-world app behavior, not beginner shortcuts.
π§ Navigation Structure
Auth Screen → Home Screen
We use Jetpack Navigation Compose to protect the Home screen behind biometric authentication.
@Composable
fun BiometricAuthDemo() {
val navController = rememberNavController()
NavHost(navController, startDestination = "auth") {
composable("auth") {
AuthGateScreen(
viewModel = viewModel(),
onNavigateHome = {
navController.navigate("home") {
popUpTo("auth") { inclusive = true }
}
}
)
}
composable("home") {
HomeScreen()
}
}
}
Why popUpTo?
To prevent users from pressing the back button and returning to the auth screen.
This is a security best practice.
π§ Why We Use ViewModel (Not remember)
❌ Beginner mistake:
var isAuthenticated by remember { mutableStateOf(false) }
Problems:
- State resets on screen rotation
- Logic mixed with UI
- Not testable
✅ Correct approach:
class AuthViewModel : ViewModel() {
private val _state = MutableStateFlow<AuthState>(AuthState.Idle)
val state: StateFlow<AuthState> = _state
fun startAuth() {
_state.value = AuthState.Authenticating
}
fun onSuccess() {
_state.value = AuthState.Success
}
fun onFailure() {
_state.value = AuthState.Failed
}
fun onError(message: String) {
_state.value = AuthState.Error(message)
}
}
This is called state-driven authentication.
π¦ Understanding AuthState (Very Important)
sealed class AuthState {
object Idle : AuthState()
object Authenticating : AuthState()
object Success : AuthState()
object Failed : AuthState()
data class Error(val message: String) : AuthState()
}
Each state represents a real situation in the app:
- Idle → App just opened
- Authenticating → Show biometric prompt
- Success → Navigate to Home
- Failed → Retry authentication
- Error → Show error message
⚙ Triggering Biometric Safely Using LaunchedEffect
Biometric prompt is a side-effect, so it must not be triggered directly in UI.
LaunchedEffect(state) {
when (state) {
AuthState.Idle -> viewModel.startAuth()
AuthState.Authenticating -> biometricAuthenticator.authenticate()
AuthState.Failed -> biometricAuthenticator.authenticate()
AuthState.Success -> onNavigateHome()
else -> {}
}
}
Why LaunchedEffect?
- Prevents multiple biometric dialogs
- Runs only when state changes
- Safe with recomposition
π§© Why FragmentActivity Is Required (Most Important Concept)
Many beginners ask: "Why not ComponentActivity?"
Answer:
BiometricPrompt internally depends on Fragments.
That’s why this is correct:
class MainActivity : FragmentActivity()❌ This can crash or behave incorrectly:
class MainActivity : ComponentActivity()
Rule:
If an Android API internally uses fragments → use FragmentActivity.
π BiometricAuthenticator Class (Clean Architecture)
class BiometricAuthenticator(
activity: FragmentActivity,
private val viewModel: AuthViewModel
) {
private val executor = ContextCompat.getMainExecutor(activity)
private val prompt = BiometricPrompt(
activity,
executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
viewModel.onSuccess()
}
override fun onAuthenticationFailed() {
viewModel.onFailure()
}
override fun onAuthenticationError(
errorCode: Int,
errString: CharSequence
) {
viewModel.onError(errString.toString())
}
}
)
fun authenticate() {
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Authenticate")
.setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
.build()
prompt.authenticate(promptInfo)
}
}
This separation keeps:
- UI clean
- Logic testable
- Code production-ready
π Home Screen (After Authentication)
Home screen does NOT care how authentication happened. It only shows secured content.
@Composable
fun HomeScreen() {
Column {
Text("Home Screen")
Text("You are successfully authenticated")
}
}
❌ Common Beginner Mistakes
- Calling biometric directly inside Composable
- Using remember instead of ViewModel
- Not handling back navigation
- Using ComponentActivity
- No retry logic
Your implementation avoids all of these mistakes.
Final Code:
// Main Activity
class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
ComposeAppTheme {
/*Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}*/
BiometricAuthDemo()
}
}
}
}
// BiometricAuthDemo Screen
@Composable
fun BiometricAuthDemo() {
val navController = rememberNavController()
NavHost(navController, startDestination = "auth") {
composable("auth") {
AuthGateScreen(
viewModel = viewModel(),
onNavigateHome = {
navController.navigate("home") {
popUpTo("auth") { inclusive = true }
}
}
)
}
composable("home") {
HomeScreen()
}
}
}
// First Screen (AuthGateScreen)
@Composable
fun AuthGateScreen(
viewModel: AuthViewModel,
onNavigateHome: () -> Unit
) {
val context = LocalContext.current
val activity = context as? FragmentActivity ?: return
// val activity = context as FragmentActivity
val state by viewModel.state.collectAsState()
val biometricAuthenticator = remember {
BiometricAuthenticator(activity, viewModel)
}
// π Trigger authentication based on state
LaunchedEffect(state) {
delay(300)
when (state) {
AuthState.Idle -> {
viewModel.startAuth()
}
AuthState.Authenticating -> {
biometricAuthenticator.authenticate()
}
AuthState.Failed -> {
biometricAuthenticator.authenticate()
}
AuthState.Success -> {
onNavigateHome()
}
else -> {}
}
}
// Optional UI
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Authenticating...")
}
}
// Second Screen (HomeScreen)
@Composable
fun HomeScreen(
onLogout: () -> Unit = {}
) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "π Home Screen",
style = MaterialTheme.typography.headlineLarge
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = "You are successfully authenticated",
style = MaterialTheme.typography.bodyLarge
)
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = onLogout
) {
Text("Logout")
}
}
}
}
// Biometric Authenticator Class
class BiometricAuthenticator(
activity: FragmentActivity,
private val viewModel: AuthViewModel
) {
private val executor = ContextCompat.getMainExecutor(activity)
private val prompt = BiometricPrompt(
activity,
executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
viewModel.onSuccess()
}
override fun onAuthenticationFailed() {
viewModel.onFailure()
}
override fun onAuthenticationError(
errorCode: Int,
errString: CharSequence
) {
viewModel.onError(errString.toString())
}
}
)
fun authenticate() {
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Authenticate")
.setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
.build()
prompt.authenticate(promptInfo)
}
}
// Fragment Class
class BiometricFragment(
private val onResult: (AuthState) -> Unit
) : Fragment() {
fun authenticate() {
val executor = ContextCompat.getMainExecutor(requireContext())
val biometricPrompt = BiometricPrompt(
this, // ✅ Fragment!
executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
onResult(AuthState.Success)
}
override fun onAuthenticationFailed() {
onResult(AuthState.Failed)
}
override fun onAuthenticationError(
errorCode: Int,
errString: CharSequence
) {
if (errorCode != BiometricPrompt.ERROR_CANCELED) {
onResult(AuthState.Failed)
}
}
}
)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Authenticate")
.setSubtitle("Use fingerprint or face")
.setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
.build()
biometricPrompt.authenticate(promptInfo)
}
}
// Sealed Class
sealed class AuthState {
object Idle : AuthState()
object Authenticating : AuthState()
object Success : AuthState()
object Failed : AuthState()
data class Error(val message: String) : AuthState()
}
// AuthViewModel
class AuthViewModel : ViewModel() {
private val _state = MutableStateFlow(AuthState.Idle)
val state: StateFlow = _state
fun startAuth() {
_state.value = AuthState.Authenticating
}
fun onSuccess() {
_state.value = AuthState.Success
}
fun onFailure() {
_state.value = AuthState.Failed
}
fun onError(message: String) {
_state.value = AuthState.Error(message)
}
}
Required Dependecies
implementation("androidx.biometric:biometric:1.2.0-alpha05")
// ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6")
Output:
❓ Frequently Asked Questions (FAQ)
1. What is biometric authentication in Android?
2. Does Jetpack Compose have a built-in biometric component?
3. Why is BiometricPrompt preferred over older fingerprint APIs?
- It supports multiple biometric types
- It provides a consistent UI
- It handles security and hardware differences
- It supports device credential fallback
- Older APIs like FingerprintManager are deprecated
4. Can biometric authentication work without fingerprint hardware?
- PIN
- Pattern
- Password
5. What is the correct way to use biometrics in Jetpack Compose?
- Keep biometric logic outside composables
- Use a helper or ViewModel
- Update state, not UI
- Let Compose react to state changes
6. Why should biometric logic not be written inside composables?
- Can recompose multiple times
- Should remain side-effect free
- Are not lifecycle-owners
7. What role does ViewModel play in biometric authentication?
- Stores authentication state
- Survives configuration changes
- Separates UI from business logic
- Makes the app testable and scalable
8. What is the purpose of BiometricManager?
- If biometric hardware exists
- If biometrics are enrolled
- If authentication is allowed on the device
9. Is biometric data accessible to the app?
10. Is biometric authentication completely secure?
- Combine biometrics with encrypted storage
- Re-authenticate for sensitive actions
- Avoid using biometrics as the only security layer
11. What happens if biometric authentication fails?
- The user can retry
- The app receives a failure callback
- No sensitive data is exposed
12. How should errors be handled in biometric authentication?
- Too many failed attempts
- User cancellation
- Hardware issues
- Show a clear message
- Allow retry
- Provide fallback authentication
13. Can biometric authentication be used for auto-login?
- Use biometrics to unlock a secure token
- Never bypass authentication completely
- Re-authenticate after app restart or timeout
14. Should biometric authentication be mandatory?
- Offer biometrics as an option
- Allow users to disable it
- Always provide a fallback login method
15. How do I test biometric authentication in emulator?
- Run the emulator
- Open Extended Controls
- Go to Fingerprint
- Tap Touch Sensor
16. Does biometric authentication work on all Android versions?
❌ No, biometric authentication does not work the same way on all Android versions.
Biometric authentication support depends on the Android version and the biometric APIs available on that version.
-
Android 6.0 (API 23) to Android 9 (API 28)
❌ No official Biometric API.
Fingerprint authentication was available using the oldFingerprintManager(now deprecated). -
Android 10 (API 29)
✅ IntroducedBiometricPrompt, but with limited options.
Mostly supported fingerprint authentication. -
Android 11 (API 30) and above
✅ Full support for modern biometric authentication.
Supports fingerprint, face unlock, and device credentials (PIN, pattern).
Best Practice (What professionals do):
BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL
This ensures:
- Compatibility with most Android versions
- Fallback to device PIN or pattern
- Better user experience
Important Note:
Even on supported Android versions, biometric authentication will NOT work if:
- No fingerprint or face is enrolled on the device
- Device hardware does not support biometrics
- Biometric authentication is disabled in system settings
Conclusion:
Biometric authentication works best on Android 10+, and for real apps,
you should always provide a device credential fallback to support all users.
17. What is BIOMETRIC_STRONG?
- High-security biometrics
- Resistant to spoofing
- Required for financial and enterprise apps
- Fingerprint
- Secure face unlock
18. Can biometric authentication be used with EncryptedSharedPreferences?
- Authenticate user
- Unlock encryption key
- Access sensitive data securely
19. What are common mistakes developers make?
20. Is biometric authentication suitable for all apps?
- Banking apps
- Finance apps
- Secure dashboards
- Admin panels
- Private data apps
- Casual games
- Public content apps
21. Can biometric authentication be reused for multiple screens?
- Centralize logic in a helper or ViewModel
- Trigger authentication when needed
- Use navigation guards
22. How does Compose recomposition affect biometrics?
- You avoid side effects
- You use state properly
- You trigger authentication explicitly
23. Is biometric authentication accessible?
- System UI handles accessibility
- Supports screen readers
- Provides fallback credentials
24. Can biometric authentication fail permanently?
25. What is the best architecture for biometrics in Compose?
π― Final Thoughts
If you want to build secure, professional Android apps, biometric authentication must be:
- State-driven
- Lifecycle-aware
- Separated from UI
- Handled by FragmentActivity
This architecture is used in banking apps, payment apps, and enterprise apps.



