如何在PIN输入对话框关闭后保存状态,维持后台计时器运行
解决PIN码对话框关闭后状态丢失、计时器中断的问题
当前你的PIN验证对话框存在的核心问题是:状态(错误尝试次数、计时器)完全绑定在Dialog实例中,Dialog关闭销毁时,计时器和状态也会被回收,导致重新打开对话框时回到初始状态。要实现后台保持计时器运行、再次打开时恢复状态,需要将状态和计时器逻辑从Dialog中剥离,放到独立于Dialog生命周期的组件中。
解决方案思路
- 将状态与计时器迁移到ViewModel:ViewModel在Activity/Fragment的生命周期内持续存在,Dialog关闭后依然能保留状态,计时器可以在后台正常运行。
- Dialog仅负责UI展示:Dialog不再维护状态,而是通过观察ViewModel的状态变化来更新UI,打开时直接复用ViewModel中的现有状态。
- 持久化关键时间点:记录倒计时结束的时间戳,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
相关产品推荐
相关产品推荐

