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

如何在Kotlin Jetpack Compose中使用Google SMS Retriever API自动填充OTP

短信验证码自动填充适配Jetpack Compose方案

需求:用户提交信息后跳转至验证码页,接收服务端发送的4位验证码短信,自动填充自定义的4位OTP输入框。现有代码已集成Google SMS Retriever,但未完成Compose下的验证码传递与自动填充逻辑。

步骤1:修改短信广播接收器,提取验证码并传递

原MySMSBroadcastReceiver仅打印短信内容,需添加验证码提取逻辑,并通过本地广播将验证码传递给验证码页的ViewModel:

class MySMSBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (SmsRetriever.SMS_RETRIEVED_ACTION == intent.action) {
            val extras = intent.extras
            val status = extras?.get(SmsRetriever.EXTRA_STATUS) as Status

            when (status.statusCode) {
                CommonStatusCodes.SUCCESS -> {
                    val message = extras.get(SmsRetriever.EXTRA_SMS_MESSAGE) as String
                    // 提取4位数字验证码
                    val otpPattern = "\\d{4}".toRegex()
                    val matchResult = otpPattern.find(message)
                    matchResult?.value?.let { otpCode ->
                        // 发送本地广播传递验证码
                        val localIntent = Intent("OTP_RECEIVED_ACTION").apply {
                            putExtra("OTP_CODE", otpCode)
                        }
                        LocalBroadcastManager.getInstance(context).sendBroadcast(localIntent)
                    }
                }
                CommonStatusCodes.TIMEOUT -> {
                    // 处理超时逻辑,比如提示用户重新获取验证码
                }
            }
        }
    }
}

步骤2:在验证码ViewModel中监听广播,更新OTP状态

修改VerificationPhoneCodeViewModel,添加本地广播监听,接收验证码后更新UI状态:

@HiltViewModel
class VerificationPhoneCodeViewModel @Inject constructor(
    private val context: Context // 通过Hilt注入Context
) : ViewModel() {
    private val _state = MutableStateFlow(VerificationPhoneCodeScreenState())
    val state = _state.asStateFlow()

    private lateinit var broadcastReceiver: BroadcastReceiver

    init {
        registerOtpReceiver()
    }

    private fun registerOtpReceiver() {
        broadcastReceiver = object : BroadcastReceiver() {
            override fun onReceive(context: Context?, intent: Intent?) {
                if (intent?.action == "OTP_RECEIVED_ACTION") {
                    val otpCode = intent.getStringExtra("OTP_CODE") ?: return
                    // 更新OTP输入框内容
                    _state.update { it.copy(otpValue = otpCode) }
                    // 自动触发验证(如果需要)
                    verificationSmsCode(otpCode)
                }
            }
        }
        // 注册本地广播接收器
        LocalBroadcastManager.getInstance(context).registerReceiver(
            broadcastReceiver,
            IntentFilter("OTP_RECEIVED_ACTION")
        )
    }

    override fun onCleared() {
        super.onCleared()
        // 销毁时注销广播接收器,避免内存泄漏
        LocalBroadcastManager.getInstance(context).unregisterReceiver(broadcastReceiver)
    }

    // 保留原有的onChangeOtpValue、verificationSmsCode等方法
    fun onChangeOtpValue(value: String) {
        _state.update { it.copy(otpValue = value) }
    }

    fun verificationSmsCode(code: String) {
        // 原有的验证逻辑
    }

    fun setError(isError: Boolean) {
        _state.update { it.copy(error = isError) }
    }
}

步骤3:修复OtpTextField的显示问题

原CharView中当index == text.length时显示"0",这会导致输入前每个框都显示0,改为显示空字符串:

@Composable
private fun CharView(
    index: Int,
    text: String,
    otpVerificationStatus: OtpVerificationStatus
) {
    val isFocused = text.length == index
    val char = when {
        index < text.length -> text[index].toString()
        else -> "" // 替换原有的"0"为空
    }

    // 保留原有的颜色逻辑和Text组件
    var focusedColor = MaterialTheme.colors.DYTThemeColor
    var nonFocusedColor = MaterialTheme.colors.grayColor

    when(otpVerificationStatus){
        OtpVerificationStatus.DEFAULT -> {
            focusedColor = MaterialTheme.colors.DYTThemeColor
            nonFocusedColor = MaterialTheme.colors.grayColor
        }
        OtpVerificationStatus.VERIFICATION_TRUE -> {
            focusedColor = DYTGreenColor
            nonFocusedColor = DYTGreenColor
        }
        OtpVerificationStatus.VERIFICATION_FALSE -> {
            focusedColor = red_color
            nonFocusedColor = red_color
        }
    }

    Text(
        modifier = Modifier
            .size(50.dp, 60.dp)
            .border(
                2.dp, when {
                    isFocused -> focusedColor
                    else -> nonFocusedColor
                }, RoundedCornerShape(8.dp)
            )
            .padding(2.dp)
            .offset(y = 15.dp),
        text = char,
        style = MaterialTheme.typography.h4,
        color = MaterialTheme.colors.DYTThemeColor,
        textAlign = TextAlign.Center
    )
}

步骤4:优化SmsRetriever启动时机

原代码在MainActivity启动时调用startSmsRetriever(),会导致进入验证码页时可能已超时(默认超时5分钟),改为在进入验证码页时启动:

修改VerificationPhoneCodeRoute,添加LaunchedEffect启动SmsRetriever:

@Composable
fun VerificationPhoneCodeRoute(
    navHostController: NavHostController,
    viewModel: VerificationPhoneCodeViewModel = hiltViewModel()
) {
    val state by viewModel.state.collectAsState()
    val context = LocalContext.current

    // 进入页面时启动SmsRetriever
    LaunchedEffect(Unit) {
        val client = SmsRetriever.getClient(context)
        val task = client.startSmsRetriever()
        task.addOnSuccessListener {
            // 启动成功
        }
        task.addOnFailureListener {
            // 启动失败,提示用户手动输入
        }
    }

    VerificationPhoneCodeScreen(
        navHostController = navHostController,
        state = state,
        onChangeOtpValue = viewModel::onChangeOtpValue,
        verificationSmsCode = viewModel::verificationSmsCode,
        setError = viewModel::setError,
    )
}

同时移除MainActivity中的startSmsRetriever()代码:

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    lateinit var navHostController: NavHostController

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 移除原有的SmsRetriever启动代码

        setContent {
            DiyetkolikTheme {
                navHostController = rememberNavController()
                Surface(modifier = Modifier.fillMaxSize()) {
                    RootNavGraph(navHostController = navHostController)
                }
            }
        }
    }
}

注意事项

  • 服务端发送的短信必须符合Google SMS Retriever格式:以<#>开头,包含4位验证码,结尾以[你的应用包名]结束,例如:<#> 1234 是你的验证码。[com.example.diyetkolik]
  • 确保Manifest中广播接收器的权限和intent-filter配置正确,无需修改原有配置。
  • 若使用Hilt注入Context,需在Application类添加@HiltAndroidApp注解,并在Manifest中配置该Application。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 15:37:10