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实例缓存起来。同时用DisposableEffectLaunchedEffect来管理认证的启动和取消。

我曾经在项目里犯过一个错:把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层,一目了然。

Compose + BiometricPrompt 集成架构 UI 层 (Composable) BiometricAuthScreen | Button | Icon | Text | CircularProgressIndicator 根据 BiometricState 显示不同界面元素 状态管理层 (State) rememberBiometricState() | rememberErrorMessage() MutableState<BiometricState> 驱动UI重组 生命周期 & API 层 LaunchedEffect | remember | DisposableEffect BiometricPrompt.authenticate() 调用系统人脸识别 UI 状态 系统

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里,你都能游刃有余。

公众号:蓝海资料掘金营,微信deep3321