You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose重发Firebase验证码报resendToken未初始化错误

问题描述

基于Android Jetpack Compose开发Firebase手机号认证模块时,已实现首次OTP验证码下发功能,在开发验证码重发功能时,为「Resend Code」控件添加如下点击逻辑:

.clickable { modelAuthentication.resendCode(phoneNumberOTP)}

点击控件后程序抛出lateinit property resendToken has not been initialized异常,核心业务代码如下:

核心代码片段

Phone.kt(手机号输入页)

@Composable
fun PhoneScreen(
    navController: NavController,
    modelAuthentication: AuthenticationViewModel
) {
    val phoneNumberState = remember { mutableStateOf("") }

    OutlinedTextField(
        value = phoneNumberState.value,
        colors = TextFieldDefaults.textFieldColors(
            backgroundColor = white,
            focusedIndicatorColor = Grey,
            unfocusedIndicatorColor = Grey,
            focusedLabelColor = Grey,
            unfocusedLabelColor = Grey,
            cursorColor = color,
            textColor = color,
        ),
        onValueChange = { phoneNumberState.value = it },
        label = { Text(text = "Phone Number") },
        placeholder = { Text(text = "Phone Number") },
        singleLine = true,
    )
    Button(
        modifier = Modifier
            .width(285.dp)
            .height(55.dp),
        onClick = {
            modelAuthentication.send(phoneNumberState.value)
        },
        colors = ButtonDefaults.buttonColors(
            backgroundColor = color
        ),
        shape = RoundedCornerShape(40),
    ) {
        Text(
            text = "send",
            style = TextStyle(
                fontSize = 18.sp,
                color = white,
            )
        )
    }
}

PhoneVerify.kt(验证码校验页)

@Composable
fun PhoneVerifyScreen(
    navController: NavController,
    modelAuthentication: AuthenticationViewModel,
) {
    // 冗余无效变量,从未被赋值使用
    lateinit var resendToken: PhoneAuthProvider.ForceResendingToken
    val focusManager = LocalFocusManager.current
    val phoneNumberOTP = remember { mutableStateOf("") }
   
    val context = LocalContext.current
    LaunchedEffect(Unit) {
        println("found activity? ${context.findActivity()}")
        val activity = context.findActivity() ?: return@LaunchedEffect
        modelAuthentication.setActivity(activity)
    }

    Column(
        Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        OutlinedTextField(
            value = phoneNumberOTP.value,
            colors = TextFieldDefaults.textFieldColors(
                backgroundColor = white,
                focusedIndicatorColor = Grey,
                unfocusedIndicatorColor = Grey,
                focusedLabelColor = Grey,
                unfocusedLabelColor = Grey,
                cursorColor = color,
                textColor = color,
            ),
            onValueChange = { phoneNumberOTP.value = it },
            label = { Text(text = "Verify code") },
            placeholder = { Text(text = "Verify code") },
            modifier = Modifier.fillMaxWidth(0.8f),
        )

        Spacer(modifier = Modifier.padding(7.dp))

        Row(
            Modifier.width(300.dp),
            horizontalArrangement = Arrangement.Center,
            verticalAlignment = Alignment.CenterVertically,
        ) {
            Text(
                "Resend Code",
                modifier = Modifier
                    .height(20.dp)
                    .clickable {
                        // 传参错误:传入的是验证码输入值,不是目标手机号
                        modelAuthentication.resendCode(phoneNumberOTP)
                    },
                textAlign = TextAlign.End,
                fontWeight = FontWeight.Medium,
                fontSize = 14.sp,
                color = color
            )
        } 
    }
}

AuthenticationViewModel.kt(认证逻辑ViewModel)

@HiltViewModel
class AuthenticationViewModel @Inject constructor(
    private val auth: FirebaseAuth
) : ViewModel() {

    lateinit var otp: String
    var verificationOtp = ""
    var popNotification = mutableStateOf<Event<String>?>(null)
    private lateinit var baseBuilder: PhoneAuthOptions.Builder

    fun setActivity(activity: Activity) {
        baseBuilder = PhoneAuthOptions.newBuilder(auth).setActivity(activity)
    }

    lateinit var resendToken: PhoneAuthProvider.ForceResendingToken

    val mCallBack = object : PhoneAuthProvider.OnVerificationStateChangedCallbacks() {
        override fun onVerificationCompleted(p0: PhoneAuthCredential) {
            handledException(customMessage = "Verification Completed")
        }

        override fun onVerificationFailed(p0: FirebaseException) {
            handledException(customMessage = "Verification Failed")
        }

        override fun onCodeSent(otp: String, p1: PhoneAuthProvider.ForceResendingToken) {
            super.onCodeSent(otp, p1)
            verificationOtp = otp
            resendToken = p1
            handledException(customMessage = "Otp Send Successfully")
        }
    }

    fun sendVerificationCode(mobileNum: String) {
        val options = baseBuilder
            .setPhoneNumber(mobileNum)
            .setTimeout(5L, TimeUnit.SECONDS)
            .setCallbacks(mCallBack)
            .build()
        PhoneAuthProvider.verifyPhoneNumber(options)
    }
    
    fun signInWithPhoneAuthCredential(otp: String) {
        val credential = PhoneAuthProvider.getCredential(verificationOtp, otp)
        auth.currentUser?.linkWithCredential(credential)
        FirebaseAuth.getInstance().signInWithCredential(credential)
            .addOnCompleteListener { task ->
                if (task.isSuccessful) {
                    handledException(customMessage = "Verification Successful")
                } else {
                    handledException(customMessage =  "Wrong Otp")
                }
            }
    }

    private fun handledException(exception: Exception? = null, customMessage: String = "") {
        exception?.printStackTrace()
        val errorMsg = exception?.message ?: ""
        val message = if (customMessage.isEmpty()) {
            errorMsg
        } else {
            "$customMessage: $errorMsg"
        }
        popNotification.value = Event(message)
    }
    // 缺失resendCode方法实现
}
问题根因
  • 核心逻辑缺失:ViewModel中未实现resendCode方法,重发验证码的逻辑完全没有编写
  • lateinit变量时序风险:resendToken仅在首次发送验证码触发onCodeSent回调时才会被赋值,若回调未执行完成就点击重发按钮,直接访问未初始化的lateinit变量就会抛出异常
  • 传参错误:重发验证码需要传入目标手机号,代码中传入的是验证码输入框的状态对象phoneNumberOTP,参数类型和值完全不匹配
  • 冗余代码干扰:PhoneVerifyScreen中定义的局部resendToken变量从未被赋值、从未参与业务逻辑,属于无效代码,容易引发变量混淆
修复方案
  1. 改造ViewModel代码:将resendToken改为可空类型规避lateinit初始化异常,补充resendCode方法实现,增加token有效性校验,同时存储用户输入的手机号用于重发
    修复后的ViewModel核心代码:
@HiltViewModel
class AuthenticationViewModel @Inject constructor(
    private val auth: FirebaseAuth
) : ViewModel() {
    // 存储用户输入的手机号,供重发使用
    private var currentPhoneNumber: String = ""
    lateinit var otp: String
    var verificationOtp = ""
    var popNotification = mutableStateOf<Event<String>?>(null)
    private lateinit var baseBuilder: PhoneAuthOptions.Builder

    fun setActivity(activity: Activity) {
        baseBuilder = PhoneAuthOptions.newBuilder(auth).setActivity(activity)
    }

    // 改为可空类型,避免lateinit未初始化崩溃
    private var resendToken: PhoneAuthProvider.ForceResendingToken? = null

    val mCallBack = object : PhoneAuthProvider.OnVerificationStateChangedCallbacks() {
        override fun onVerificationCompleted(p0: PhoneAuthCredential) {
            handledException(customMessage = "Verification Completed")
        }

        override fun onVerificationFailed(p0: FirebaseException) {
            handledException(customMessage = "Verification Failed")
        }

        override fun onCodeSent(otp: String, p1: PhoneAuthProvider.ForceResendingToken) {
            super.onCodeSent(otp, p1)
            verificationOtp = otp
            resendToken = p1
            handledException(customMessage = "Otp Send Successfully")
        }
    }

    // 对齐Compose里调用的send方法,实际触发发送逻辑并存入手机号
    fun send(mobileNum: String) {
        currentPhoneNumber = mobileNum
        sendVerificationCode(mobileNum)
    }

    fun sendVerificationCode(mobileNum: String) {
        val options = baseBuilder
            .setPhoneNumber(mobileNum)
            .setTimeout(5L, TimeUnit.SECONDS)
            .setCallbacks(mCallBack)
            .build()
        PhoneAuthProvider.verifyPhoneNumber(options)
    }

    // 补充重发逻辑
    fun resendCode() {
        val token = resendToken
        if (token == null) {
            handledException(customMessage = "Please wait for the first verification code to be sent")
            return
        }
        val options = baseBuilder
            .setPhoneNumber(currentPhoneNumber)
            .setTimeout(5L, TimeUnit.SECONDS)
            .setCallbacks(mCallBack)
            .setForceResendingToken(token) // 传入重发token
            .build()
        PhoneAuthProvider.verifyPhoneNumber(options)
    }
    
    // 其余signInWithPhoneAuthCredential、handledException方法保持原有逻辑不变
}
  1. 修复Compose页面代码:
  • 删除PhoneVerifyScreen中冗余的局部resendToken变量
  • 修正重发按钮的点击调用,不需要传参,直接调用modelAuthentication.resendCode()
  • 可增加重发冷却逻辑,首次发送验证码后60秒内禁用重发按钮,避免用户频繁点击触发接口拦截
  1. 路由参数对齐:如果手机号是通过导航从PhoneScreen传到PhoneVerifyScreen的,要确保ViewModel中存储的currentPhoneNumber和导航传递的值一致,避免重发时手机号错误。

内容的提问来源于stack exchange,提问作者Huma33

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 06:22:03