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,结果回调被反复注册,界面卡得不行。

解决办法是用rememberDisposableEffect来管理生命周期。说白了,就是让BiometricPrompt只在Composable进入组合时创建一次,离开组合时自动释放。

关键点:BiometricPrompt的创建必须与Composable的生命周期绑定,不能放在重组路径里。

状态管理:用StateFlow驱动UI

你想想看,生物识别是个异步操作。用户按指纹、扫虹膜,结果什么时候回来?不知道。所以我们需要一个状态容器来承载“认证中”、“认证成功”、“认证失败”这些状态。

我推荐用StateFlowMutableState。在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集成生物识别的完整流程

Compose集成生物识别流程 用户点击“开始认证” 状态 → Authenticating BiometricPrompt.authenticate(promptInfo) 系统生物识别弹窗 成功 → 更新UI 失败 → 提示重试 错误 → 显示错误码

避坑指南:我踩过的三个坑

  1. 重复创建BiometricPrompt——前面说了,用remember包一下。如果不包,每次重组都会new一个新实例,旧实例的回调会泄漏。
  2. Activity被销毁后回调——如果用户在认证过程中旋转屏幕,Activity重建,旧的BiometricPrompt回调会指向已销毁的Activity。解决方案:在ViewModel里处理回调,或者用rememberRetained(需要依赖lifecycle-viewmodel-compose)。
  3. 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