21、Jetpack Compose集成:在Compose中使用BiometricPrompt、状态管理、生命周期处理
好,我们终于聊到Compose了。
说实话,现在新项目基本都用Compose写UI。如果你还在用XML写界面,嗯,也不是不行,但Compose这套声明式框架跟生物识别结合,确实更优雅。我个人习惯是把BiometricPrompt封装成一个Composable函数,这样调用方只需要传几个参数,剩下的交给状态管理去处理。
核心思路:把BiometricPrompt“装进”Compose
传统写法里,BiometricPrompt需要Activity或Fragment的context。在Compose里,我们通过LocalContext.current拿到context,然后创建一个BiometricPrompt实例。但这里有个坑——Compose的重组机制会导致重复创建。我刚开始写的时候,每次重组都new一个BiometricPrompt,结果回调被反复注册,界面卡得不行。
解决办法是用remember或DisposableEffect来管理生命周期。说白了,就是让BiometricPrompt只在Composable进入组合时创建一次,离开组合时自动释放。
关键点:BiometricPrompt的创建必须与Composable的生命周期绑定,不能放在重组路径里。
状态管理:用StateFlow驱动UI
你想想看,生物识别是个异步操作。用户按指纹、扫虹膜,结果什么时候回来?不知道。所以我们需要一个状态容器来承载“认证中”、“认证成功”、“认证失败”这些状态。
我推荐用StateFlow或MutableState。在ViewModel里定义一个sealed class来描述认证状态:
sealed class BiometricState {
object Idle : BiometricState()
object Authenticating : BiometricState()
data class Success(val cryptoObject: BiometricPrompt.AuthenticationResult?) : BiometricState()
data class Error(val message: String) : BiometricState()
}
然后在Composable里用collectAsState()收集这个Flow。当用户点击“开始认证”按钮时,把状态切到Authenticating,UI上显示一个加载动画。认证完成后,根据结果更新状态,UI自动重组。
我的经验:记得在onAuthenticationError回调里处理错误码。比如BIOMETRIC_LOCKOUT表示尝试次数过多,这时候应该显示“请稍后再试”,而不是直接弹失败提示。我曾经没处理这个,用户被锁了5分钟还以为是App坏了。
生命周期处理:DisposableEffect与remember
Compose的生命周期跟Activity/Fragment不一样。它没有onPause/onResume,但有DisposableEffect。这个API会在Composable离开组合时执行清理逻辑。
举个例子:当用户按Home键回到桌面,你的Composable可能还在组合中(如果Activity没被销毁),但BiometricPrompt的认证会话应该被取消。否则用户再回来时,回调可能已经过期了。
@Composable
fun BiometricAuthButton(
onAuthResult: (BiometricState) -> Unit
) {
val context = LocalContext.current
val activity = context.findActivity()
// 用remember保存BiometricPrompt实例
val biometricPrompt = remember {
BiometricPrompt(
activity as FragmentActivity,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
onAuthResult(BiometricState.Success(result))
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
onAuthResult(BiometricState.Error(errString.toString()))
}
override fun onAuthenticationFailed() {
onAuthResult(BiometricState.Error("认证失败,请重试"))
}
}
)
}
// 生命周期绑定
DisposableEffect(Unit) {
onDispose {
// 离开组合时取消认证
biometricPrompt.cancelAuthentication()
}
}
// UI部分
Button(onClick = {
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("虹膜认证")
.setSubtitle("请注视屏幕")
.setNegativeButtonText("取消")
.build()
biometricPrompt.authenticate(promptInfo)
}) {
Text("开始认证")
}
}
注意看DisposableEffect(Unit)——它的key是Unit,意味着只在首次进入组合时执行一次。当Composable被移除时,onDispose里的cancelAuthentication()会被调用。嗯,这里要注意:如果用户已经认证成功,再取消就没意义了。所以最好加个标志位判断。
SVG流程图:Compose集成生物识别的完整流程
避坑指南:我踩过的三个坑
- 重复创建BiometricPrompt——前面说了,用
remember包一下。如果不包,每次重组都会new一个新实例,旧实例的回调会泄漏。 - Activity被销毁后回调——如果用户在认证过程中旋转屏幕,Activity重建,旧的BiometricPrompt回调会指向已销毁的Activity。解决方案:在ViewModel里处理回调,或者用
rememberRetained(需要依赖lifecycle-viewmodel-compose)。 - DisposableEffect的key选择——如果你把某个可变状态作为key,每次状态变化都会触发
onDispose和重新执行。这会导致认证被意外取消。我建议key用Unit,除非你有特殊需求。
警告:不要在Composable的lambda里直接创建PromptInfo。每次重组都会重建,虽然不影响功能,但会造成不必要的内存分配。用remember缓存一下。
完整示例:一个可复用的BiometricAuthButton
最后,我贴一个我项目里用的完整组件。它封装了状态管理、生命周期和UI,调用方只需要传一个onResult回调:
@Composable
fun BiometricAuthButton(
modifier: Modifier = Modifier,
onResult: (BiometricState) -> Unit
) {
val context = LocalContext.current
val activity = context.findActivity() ?: return
var isAuthenticating by remember { mutableStateOf(false) }
val biometricPrompt = remember {
BiometricPrompt(
activity as FragmentActivity,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
isAuthenticating = false
onResult(BiometricState.Success(result))
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
isAuthenticating = false
onResult(BiometricState.Error(errString.toString()))
}
override fun onAuthenticationFailed() {
isAuthenticating = false
onResult(BiometricState.Error("认证失败"))
}
}
)
}
DisposableEffect(Unit) {
onDispose {
if (isAuthenticating) {
biometricPrompt.cancelAuthentication()
}
}
}
Button(
onClick = {
isAuthenticating = true
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("虹膜认证")
.setSubtitle("请将眼睛对准屏幕")
.setNegativeButtonText("取消")
.build()
biometricPrompt.authenticate(promptInfo)
},
modifier = modifier,
enabled = !isAuthenticating
) {
if (isAuthenticating) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
strokeWidth = 2.dp,
color = MaterialTheme.colorScheme.onPrimary
)
Spacer(modifier = Modifier.width(8.dp))
}
Text(if (isAuthenticating) "认证中..." else "开始虹膜认证")
}
}
这个组件里,isAuthenticating状态控制按钮的启用和加载动画。当用户点击后,按钮立刻变成禁用+转圈,防止重复点击。认证完成后,状态恢复。如果用户在认证过程中退出页面,DisposableEffect会取消认证,避免回调泄漏。
嗯,基本上就是这样。Compose集成BiometricPrompt并不复杂,核心就是记住三点:remember实例、DisposableEffect管理生命周期、State驱动UI。你按这个思路写,基本不会出问题。
公众号:蓝海资料掘金营,微信deep3321