20、人脸识别与Jetpack Compose:Compose中使用BiometricPrompt、状态管理、Compose UI集成
嗯,终于到了把生物识别和现代UI框架结合的时候了。
说实话,Jetpack Compose刚出来那会儿,我还在用老一套的View系统做指纹识别。当时就觉得,Compose这种声明式UI,跟BiometricPrompt这种异步回调式的API,天生就有点「八字不合」。你想想看,Compose里到处都是State、Effect、LaunchedEffect,而BiometricPrompt还在用callback和listener。怎么把它们揉到一起?
今天我们就来彻底解决这个问题。我会从状态管理、生命周期绑定、UI集成三个角度,带你走一遍我在实际项目中踩过的坑和总结出的最佳实践。
核心要点:Compose + BiometricPrompt 的关键在于「把回调变成状态」。用MutableState或StateFlow来承载认证结果,用LaunchedEffect来管理生命周期,用remember来保持实例。
20.1 状态管理:把回调变成状态
我个人习惯,在Compose里做任何异步操作,第一步就是定义状态。对于人脸识别,我们需要三个核心状态:
- 认证状态:空闲、进行中、成功、失败、错误
- 错误信息:当失败或错误时,给用户看的文案
- 生物识别是否可用:设备是否支持、是否已录入人脸
看一个我常用的状态定义:
// 认证状态枚举
enum class BiometricState {
IDLE,
AUTHENTICATING,
SUCCESS,
FAILED,
ERROR
}
// 在Composable中持有的状态
@Composable
fun rememberBiometricState(): MutableState<BiometricState> {
return remember { mutableStateOf(BiometricState.IDLE) }
}
@Composable
fun rememberErrorMessage(): MutableState<String> {
return remember { mutableStateOf("") }
}
为什么不用LiveData?因为Compose原生支持State和StateFlow。我在项目中试过LiveData,但每次都要observeAsState,多一层转换,没必要。直接用mutableStateOf,干净利落。
小技巧:如果你在ViewModel里管理状态,建议用StateFlow,然后在Composable里用collectAsState()收集。这样状态可以跨配置变更存活。
20.2 生命周期绑定:LaunchedEffect 与 remember
BiometricPrompt需要一个Activity或Fragment的引用。在Compose里,我们通常用LocalContext.current拿到Activity。但有个坑——Composable会重组,如果你每次重组都new一个BiometricPrompt,那就会重复认证。
解决办法:用remember把BiometricPrompt实例缓存起来。同时用DisposableEffect或LaunchedEffect来管理认证的启动和取消。
我曾经在项目里犯过一个错:把BiometricPrompt的创建放在Composable函数体里,没加remember。结果每次状态变化都重新创建,导致认证弹窗连续弹出好几次。用户差点给差评……
正确的做法是这样的:
@Composable
fun BiometricAuthButton(
onAuthResult: (BiometricState) -> Unit
) {
val context = LocalContext.current
val activity = context as? Activity ?: return
// 用remember缓存BiometricPrompt实例
val biometricPrompt = remember {
BiometricPrompt(
activity,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
onAuthResult(BiometricState.SUCCESS)
}
override fun onAuthenticationFailed() {
onAuthResult(BiometricState.FAILED)
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
onAuthResult(BiometricState.ERROR)
}
}
)
}
// 用LaunchedEffect来触发认证
var triggerAuth by remember { mutableStateOf(false) }
Button(onClick = { triggerAuth = true }) {
Text("人脸识别")
}
if (triggerAuth) {
LaunchedEffect(Unit) {
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("人脸验证")
.setSubtitle("请面向屏幕")
.setNegativeButtonText("取消")
.build()
biometricPrompt.authenticate(promptInfo)
triggerAuth = false
}
}
}
嗯,这里要注意:LaunchedEffect(Unit)只在triggerAuth变为true时执行一次。执行完后把triggerAuth重置为false,避免重复触发。
警告:不要在Composable里直接调用biometricPrompt.authenticate()!因为Composable会因重组而多次调用,导致认证弹窗多次弹出。一定要用LaunchedEffect或CoroutineScope来控制调用时机。
20.3 Compose UI集成:状态驱动的界面
状态管理好了,生命周期绑定了,剩下的就是UI展示了。说白了,就是根据BiometricState来显示不同的界面元素。
我一般会封装一个BiometricAuthScreen,把认证按钮、状态提示、错误信息都放在一起。这样调用方只需要传一个onAuthSuccess回调即可。
看一个完整的例子:
@Composable
fun BiometricAuthScreen(
onAuthSuccess: () -> Unit
) {
val authState = rememberBiometricState()
val errorMessage = rememberErrorMessage()
// 当认证成功时,触发回调
LaunchedEffect(authState.value) {
if (authState.value == BiometricState.SUCCESS) {
onAuthSuccess()
}
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// 根据状态显示不同图标和文字
when (authState.value) {
BiometricState.IDLE -> {
Icon(
imageVector = Icons.Default.Face,
contentDescription = "人脸识别",
modifier = Modifier.size(80.dp),
tint = MaterialTheme.colorScheme.primary
)
Text("点击下方按钮开始人脸验证")
}
BiometricState.AUTHENTICATING -> {
CircularProgressIndicator()
Text("正在验证...")
}
BiometricState.SUCCESS -> {
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = "成功",
modifier = Modifier.size(80.dp),
tint = Color.Green
)
Text("验证成功!")
}
BiometricState.FAILED -> {
Icon(
imageVector = Icons.Default.Cancel,
contentDescription = "失败",
modifier = Modifier.size(80.dp),
tint = Color.Red
)
Text("人脸不匹配,请重试")
}
BiometricState.ERROR -> {
Icon(
imageVector = Icons.Default.Error,
contentDescription = "错误",
modifier = Modifier.size(80.dp),
tint = Color.Red
)
Text(errorMessage.value.ifEmpty { "发生错误" })
}
}
Spacer(modifier = Modifier.height(24.dp))
// 认证按钮
Button(
onClick = {
authState.value = BiometricState.AUTHENTICATING
// 这里触发认证逻辑,实际项目中会调用上面封装的BiometricPrompt
},
enabled = authState.value != BiometricState.AUTHENTICATING
) {
Text("开始人脸识别")
}
// 失败或错误时显示重试按钮
if (authState.value == BiometricState.FAILED || authState.value == BiometricState.ERROR) {
TextButton(onClick = { authState.value = BiometricState.IDLE }) {
Text("重试")
}
}
}
}
你看,整个UI完全由状态驱动。状态变了,界面自动重组。这就是Compose的魅力。
20.4 整体架构图
下面这张图展示了我个人在项目中使用的Compose + BiometricPrompt集成架构。从UI层到状态管理层,再到系统API层,一目了然。
20.5 避坑指南
最后,分享几个我在实际集成中遇到的坑,希望能帮你少走弯路。
- 不要在主线程外调用authenticate():BiometricPrompt的API要求在主线程调用。如果你在协程里用Dispatchers.IO,记得切回Main。
- Activity重建问题:如果Activity因为配置变更重建,remember缓存的BiometricPrompt实例会失效。建议在ViewModel里持有BiometricPrompt,或者用rememberSaveable保存关键状态。
- 模拟器兼容性:Android模拟器上的人脸识别经常不稳定。我曾经在模拟器上测试,发现onAuthenticationError回调频繁触发。后来才发现是模拟器的人脸录入数据有问题。建议真机测试。
- 负面按钮的文本:setNegativeButtonText是必须的,而且不能为空。否则会抛异常。嗯,这个坑我踩过。
我的个人习惯:在Compose项目里,我会把BiometricPrompt的创建和认证逻辑封装成一个单独的rememberBiometricPrompt()函数,返回一个认证函数和状态。这样UI层只需要调用一个函数,非常干净。
好了,关于Compose中集成人脸识别,核心就是这三步:状态管理、生命周期绑定、UI驱动。你想想看,其实并不复杂,关键是把回调式的API转换成声明式的状态流。一旦掌握了这个思路,以后集成任何系统API到Compose里,你都能游刃有余。