In today’s mobile-first world, application security has become paramount. Users expect robust protection for their sensitive data, and biometric authentication provides a seamless yet secure way to deliver this. In this comprehensive guide, we’ll explore how to secure Android App with Biometric Lock using modern Android development tools.
With over 80% of modern smartphones equipped with biometric sensors, integrating fingerprint and face recognition into your apps is no longer a luxury but a necessity. This implementation not only enhances security but also improves user experience by eliminating the need to remember complex passwords.
Understanding the Architecture
Core Components Overview
Our app lock system is built on several key Android architecture components:
- Jetpack Compose: Modern declarative UI toolkit
- Biometric API: Standardized biometric authentication
- DataStore: Type-safe persistence solution
- Lifecycle-Aware Components: Proper lifecycle management
Why This Architecture Matters
class MainActivity : FragmentActivity() {
private lateinit var prefs: AppLockPrefs
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
prefs = AppLockPrefs(this)
// ... rest of initialization
}
}
The choice of FragmentActivity over regular AppCompatActivity is strategic. It provides better compatibility with both traditional fragments and modern Compose while maintaining consistency across different Android versions.
Deep Dive into Secure Android App with Biometric Lock
The BiometricUtil object serves as the cornerstone of our authentication system:
object BiometricUtil {
fun canAuthenticate(activity: ComponentActivity): Boolean {
val bm = BiometricManager.from(activity)
return bm.canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_STRONG
or BiometricManager.Authenticators.DEVICE_CREDENTIAL
) == BiometricManager.BIOMETRIC_SUCCESS
}
fun showPrompt(
activity: FragmentActivity,
title: String = "Unlock App",
subtitle: String = "Authenticate to continue",
onSuccess: () -> Unit,
onFailure: () -> Unit = {}
) {
// BiometricPrompt implementation
}
}
Key Security Considerations:
- BIOMETRIC_STRONG vs BIOMETRIC_WEAK: We use
BIOMETRIC_STRONGwhich requires Class 3 biometrics (high security), suitable for financial and sensitive data applications. - Device Credential Fallback: By including
DEVICE_CREDENTIAL, we ensure users can fall back to PIN/pattern/password if biometrics aren’t available. - Proper Error Handling: The authentication callback handles both success and failure scenarios gracefully.
Biometric Prompt Best Practices
The biometric prompt is configured with careful consideration:
val info = BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setSubtitle(subtitle)
.setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG
or BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
.build()
Why This Configuration Matters:
- Clear User Communication: Title and subtitle explain exactly what authentication is for
- Security Level Consistency: Maintains the same security level across authentication methods
- Platform Compliance: Follows Android’s biometric guidelines
Jetpack Compose UI Implementation
We also need some to screen to demonstrate how to Secure Android App with Biometric Lock.
State Management in Compose
The app lock screen demonstrates sophisticated state management:
@Composable
fun AppLockScreen(prefs: AppLockPrefs) {
val context = LocalActivity.current as FragmentActivity
val isAppLockEnabled by prefs.isAppLockEnabled.collectAsState(initial = false)
var isAuthenticated by remember { mutableStateOf(false) }
// Lifecycle management
DisposableEffect(lifecycleOwner, isAppLockEnabled) {
// Observer implementation
}
// Conditional rendering
if (!isAuthenticated && isAppLockEnabled) {
LockedScreen()
} else {
UnlockedScreen(isAppLockEnabled = isAppLockEnabled, prefs = prefs)
}
}
State Management Patterns:
- Derived State:
isAppLockEnabledis derived from DataStore - Local UI State:
isAuthenticatedmanages local authentication state - Lifecycle-Aware State: Proper handling of app background/foreground transitions
The LockedScreen Component
@Composable
fun LockedScreen() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
Icons.Default.Lock,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(80.dp)
)
// ... rest of the content
}
}
}
UX Considerations:
- Clear Visual Feedback: Lock icon immediately communicates the app state
- Accessibility: Proper content descriptions and semantic meaning
- Consistent Theming: Uses MaterialTheme for consistent appearance
The UnlockedScreen Component
The unlocked screen provides the control interface:
@Composable
fun UnlockedScreen(isAppLockEnabled: Boolean, prefs: AppLockPrefs) {
val context = LocalActivity.current as FragmentActivity
val scope = rememberCoroutineScope()
var toggleState by remember { mutableStateOf(isAppLockEnabled) }
// Switch implementation with biometric confirmation
Switch(
checked = toggleState,
onCheckedChange = { newValue ->
if (!newValue) {
// Disable with biometric confirmation
BiometricUtil.showPrompt(
context,
title = "Confirm to disable App Lock",
// ... callback handlers
)
} else {
// Enable with biometric confirmation
}
}
)
}
Security UX Patterns:
- Re-authentication for Critical Actions: Changing security settings requires re-authentication
- Immediate Visual Feedback: Toggle state reflects current setting
- Coroutine Scope Management: Proper async handling for preference updates
DataStore Implementation for Preferences
Modern Preferences Management
The AppLockPrefs class demonstrates modern preferences handling:
class AppLockPrefs(private val context: Context) {
companion object {
private val IS_APP_LOCK_ENABLED = booleanPreferencesKey("is_app_lock_enabled")
}
val isAppLockEnabled: Flow<Boolean> = context.dataStore.data.map {
it[IS_APP_LOCK_ENABLED] ?: false
}
suspend fun setAppLockEnabled(enabled: Boolean) {
context.dataStore.edit { prefs ->
prefs[IS_APP_LOCK_ENABLED] = enabled
}
}
}
Why DataStore Over SharedPreferences:
- Type Safety: Compile-time safety with preference keys
- Asynchronous Operations: Built-in coroutine support
- Error Handling: Better error handling mechanisms
- Flow Integration: Natural fit with Compose’s reactive paradigm
Lifecycle Management
Proper Lifecycle Integration
The DisposableEffect in AppLockScreen ensures proper lifecycle management:
DisposableEffect(lifecycleOwner, isAppLockEnabled) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> {
if (isAppLockEnabled && !isAuthenticated && BiometricUtil.canAuthenticate(context)) {
BiometricUtil.showPrompt(
context,
onSuccess = { isAuthenticated = true },
onFailure = { isAuthenticated = false }
)
}
}
Lifecycle.Event.ON_PAUSE -> {
// Reset auth every time leaving app
isAuthenticated = false
}
else -> {}
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
Lifecycle Strategy Benefits:
- Automatic Authentication on Resume: Users get prompted when returning to the app
- Security on Pause: Immediate lock when app goes to background
- Resource Cleanup: Proper observer removal prevents memory leaks
Security Best Practices Implemented
1. Defense in Depth
Our implementation employs multiple security layers:
- Biometric Authentication: Primary authentication method
- Device Credential Fallback: Secondary authentication method
- Automatic Locking: Session management
- Re-authentication for Settings: Critical action verification
2. Privacy Considerations
private val colorScheme = darkColorScheme()
@Composable
fun AppLockTheme(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = colorScheme,
content = content
)
}
The use of a dark theme by default respects user preferences and reduces eye strain while maintaining security.
3. Proper Error Handling
The biometric implementation includes comprehensive error handling:
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
onFailure()
}
This ensures the app remains in a secure state even when authentication fails.
Performance Considerations
Efficient State Updates
The use of collectAsState with DataStore ensures efficient updates:
val isAppLockEnabled by prefs.isAppLockEnabled.collectAsState(initial = false)
This pattern:
- Minimizes unnecessary recompositions
- Provides type-safe state management
- Integrates seamlessly with Compose’s reactive system
Memory Management
The DisposableEffect ensures proper cleanup:
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
Common Pitfalls and Solutions
1. Biometric Availability
Always check biometric capability:
if (BiometricUtil.canAuthenticate(context)) {
// Proceed with biometric authentication
} else {
// Fallback or disable feature
}
2. Lifecycle Race Conditions
The implementation handles potential race conditions through proper state management and lifecycle awareness.
3. Configuration Changes
The use of remember and state hoisting ensures state preservation across configuration changes.
Future Enhancements
1. Advanced Security Features
- Time-based auto-lock
- Incorrect attempt limits
- Emergency access codes
2. Accessibility Improvements
- Voice guidance
- High contrast themes
- Larger touch targets
Conclusion
Implementing a robust app lock feature requires careful consideration of security, user experience, and modern Android architecture patterns. The solution presented here demonstrates how to leverage Jetpack Compose, Biometric API, and DataStore to create a secure, user-friendly authentication system.
Key takeaways:
- Security First: Always authenticate critical actions
- User Experience: Balance security with convenience
- Modern Architecture: Leverage the latest Android components
- Lifecycle Awareness: Proper resource and state management
This implementation provides a solid foundation that can be extended to meet specific application requirements while maintaining security best practices and delivering an excellent user experience.
Remember that security is an ongoing process. Regular updates, staying informed about new vulnerabilities, and continuous testing are essential for maintaining a secure application environment.
This is how we can Secure Android App with Biometric Lock.
Thanks for reading this article. ❤
Also, follow to get updated on exciting articles and projects.
If I got something wrong? Let me know in the comments. I would love to improve.
Full Working Example: BiometricAppLock
Let’s get connected
We can be friends. Find on Facebook, Linkedin, GitHub, YouTube,
BuyMeACoffee, and Instagram.
Contribute: BuyMeACoffee
Contact: Contact Us
