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

如何在PIN输入对话框关闭后保存状态,维持后台计时器运行

解决PIN码对话框关闭后状态丢失、计时器中断的问题

当前你的PIN验证对话框存在的核心问题是:状态(错误尝试次数、计时器)完全绑定在Dialog实例中,Dialog关闭销毁时,计时器和状态也会被回收,导致重新打开对话框时回到初始状态。要实现后台保持计时器运行、再次打开时恢复状态,需要将状态和计时器逻辑从Dialog中剥离,放到独立于Dialog生命周期的组件中。

解决方案思路

  1. 将状态与计时器迁移到ViewModel:ViewModel在Activity/Fragment的生命周期内持续存在,Dialog关闭后依然能保留状态,计时器可以在后台正常运行。
  2. Dialog仅负责UI展示:Dialog不再维护状态,而是通过观察ViewModel的状态变化来更新UI,打开时直接复用ViewModel中的现有状态。
  3. 持久化关键时间点:记录倒计时结束的时间戳,Dialog重新打开时通过当前时间计算剩余时长,恢复计时器。

具体实现代码

1. 创建PinVerifyViewModel管理状态与计时器

import android.os.CountDownTimer
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class PinVerifyViewModel : ViewModel() {
    // 错误尝试次数
    private val _failedAttempts = MutableLiveData(0)
    val failedAttempts: LiveData<Int> = _failedAttempts

    // 剩余倒计时时间(毫秒),null表示无倒计时
    private val _remainingTime = MutableLiveData<Long?>(null)
    val remainingTime: LiveData<Long?> = _remainingTime

    // PIN输入框是否可用
    private val _isPinInputEnabled = MutableLiveData(true)
    val isPinInputEnabled: LiveData<Boolean> = _isPinInputEnabled

    private var countDownTimer: CountDownTimer? = null
    // 记录倒计时结束的时间戳,用于重新打开Dialog时计算剩余时间
    private var countdownEndTime: Long? = null

    // 增加错误尝试次数
    fun incrementFailedAttempts() {
        val current = _failedAttempts.value ?: 0
        _failedAttempts.value = current + 1

        // 达到3次错误,启动倒计时
        if (current + 1 == 3) {
            startCountdown()
        }
    }

    // 启动30秒倒计时
    private fun startCountdown() {
        val totalTime = 30000L
        countdownEndTime = System.currentTimeMillis() + totalTime
        _isPinInputEnabled.value = false
        _remainingTime.value = totalTime

        // 先取消之前的计时器(防止重复创建)
        countDownTimer?.cancel()
        countDownTimer = object : CountDownTimer(totalTime, 1000) {
            override fun onTick(millisUntilFinished: Long) {
                _remainingTime.value = millisUntilFinished
            }

            override fun onFinish() {
                // 倒计时结束,重置所有状态
                resetState()
            }
        }.start()
    }

    // 重置验证状态
    fun resetState() {
        countDownTimer?.cancel()
        _failedAttempts.value = 0
        _remainingTime.value = null
        _isPinInputEnabled.value = true
        countdownEndTime = null
    }

    // 计算当前剩余的倒计时时间(Dialog打开时调用)
    fun calculateRemainingTime() {
        countdownEndTime?.let { endTime ->
            val currentTime = System.currentTimeMillis()
            val remaining = endTime - currentTime
            if (remaining > 0) {
                // 还有剩余时间,恢复计时器和UI状态
                _remainingTime.value = remaining
                _isPinInputEnabled.value = false

                countDownTimer?.cancel()
                countDownTimer = object : CountDownTimer(remaining, 1000) {
                    override fun onTick(millisUntilFinished: Long) {
                        _remainingTime.value = millisUntilFinished
                    }

                    override fun onFinish() {
                        resetState()
                    }
                }.start()
            } else {
                // 倒计时已结束,直接重置状态
                resetState()
            }
        }
    }

    // ViewModel销毁时取消计时器,避免内存泄漏
    override fun onCleared() {
        super.onCleared()
        countDownTimer?.cancel()
    }
}

2. 重构EnterPinDialog,依赖ViewModel更新UI

class EnterPinDialog(
    context: Context,
    var repository: ItemRepository,
    var itemList: Input<ArrayList<String>>,
    private val viewModel: PinVerifyViewModel
) : Dialog(context) {

    lateinit var dataBind: EnterPinDialogBinding
    var itemPinList: ArrayList<String> = ArrayList()
    var confirm_pin: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        dataBind = DataBindingUtil.inflate(layoutInflater, R.layout.enter_pin_dialog, null, false)
        setContentView(dataBind.root)
        initBasicViews()
        observeViewModelStates()
        // 打开Dialog时恢复剩余倒计时状态
        viewModel.calculateRemainingTime()
    }

    private fun initBasicViews() {
        dataBind.materialTextView53.visibility = View.GONE
        dataBind.pinView.transformationMethod = AsteriskPasswordTransformationMethod()

        dataBind.pinView.addTextChangedListener(object : TextWatcher {
            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}

            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
                // 输入框不可用时,直接拦截输入逻辑
                if (viewModel.isPinInputEnabled.value != true) return

                confirm_pin = s.toString()
                val epin = md5(confirm_pin!!)
                for (ls in itemPinList.indices) {
                    if (s!!.length > 5) {
                        if (epin.equals(itemPinList[ls])) {
                            // PIN验证成功,调用API并重置状态
                            setItemLockAPi(itemList, false)
                            viewModel.resetState()
                            dismiss()
                        } else {
                            // PIN错误,通知ViewModel增加错误次数
                            viewModel.incrementFailedAttempts()
                            dataBind.pinView.setLineColor(ContextCompat.getColor(context, R.color.red))
                            dataBind.materialTextView53.visibility = View.VISIBLE
                            dataBind.pinView.text?.clear()
                        }
                    }
                }
            }

            override fun afterTextChanged(s: Editable?) {}
        })

        dataBind.backPin.setOnClickListener { dismiss() }
    }

    // 观察ViewModel的状态变化,实时更新UI
    private fun observeViewModelStates() {
        viewModel.failedAttempts.observe(this) { count ->
            if (count > 0) {
                dataBind.attemptsLeft.visibility = View.VISIBLE
                dataBind.attemptsLeft.text = "${3 - count} attempts left"
            } else {
                dataBind.attemptsLeft.visibility = View.GONE
            }

            // 达到3次错误,显示锁定提示
            dataBind.failedAttempts.visibility = if (count == 3) View.VISIBLE else View.GONE
            if (count == 3) {
                dataBind.materialTextView53.visibility = View.GONE
            }
        }

        viewModel.isPinInputEnabled.observe(this) { enabled ->
            dataBind.pinView.isEnabled = enabled
            if (enabled) {
                dataBind.pinView.setLineColor(ContextCompat.getColor(context, R.color.white))
            }
        }

        viewModel.remainingTime.observe(this) { remaining ->
            if (remaining != null && remaining > 0) {
                dataBind.timerThirty.visibility = View.VISIBLE
                dataBind.timerThirty.text = "${remaining / 1000} Seconds"
            } else {
                dataBind.timerThirty.visibility = View.GONE
            }
        }
    }

    // 请补充你的md5加密实现
    private fun md5(input: String): String {
        // 你的现有md5逻辑
    }

    // 请补充你的API调用实现
    private fun setItemLockAPi(itemList: Input<ArrayList<String>>, isLocked: Boolean) {
        // 你的现有API逻辑
    }
}

3. 修改Dialog调用代码,传入ViewModel

在你的Activity/Fragment中初始化ViewModel,并在打开Dialog时传入:

// 初始化ViewModel(需依赖AndroidX Lifecycle库)
private val pinViewModel by viewModels<PinVerifyViewModel>()

lockCardView.setOnClickListener {
    showEnterPinDialog(Input.fromNullable(ItemList))
}

fun showEnterPinDialog(itemList: Input<ArrayList<String>>) {
    val dialog = EnterPinDialog(requireActivity(), Repository, itemList, pinViewModel)
    dialog.show()
    dialog.setCanceledOnTouchOutside(false)
}

方案优势

  • 状态持久化:ViewModel在Activity生命周期内不会销毁,Dialog关闭后计时器依然在后台运行,状态不会丢失。
  • UI与逻辑分离:Dialog仅负责展示,状态逻辑集中在ViewModel,代码更易维护。
  • 内存安全:ViewModel销毁时会自动取消计时器,避免内存泄漏。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 12:24:14