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

在CoroutineWorker中使用CountDownTimer出现Looper报错求助

问题分析与解决方案

错误原因

你遇到的Can't create handler inside thread Thread[DefaultDispatcher-worker-1,5,main] that has not called Looper.prepare()错误,核心原因是:

  • CountDownTimer内部依赖Handler实现定时逻辑,而Handler必须绑定在初始化过Looper的线程(比如主线程)中。
  • CoroutineWorker的doWork方法运行在无Looper的后台线程,因此无法创建Handler,直接触发报错。
  • 更关键的是:你对WorkManager的用法完全错误——WorkManager是用来调度延迟/周期性任务的,不是让Worker持续运行30分钟做倒计时,这种方式不仅浪费资源,还会被系统主动回收进程。

正确实现表单锁定的思路

表单锁定不需要后台持续运行倒计时,只需要:

  1. 当失败次数达到阈值时,记录锁定结束时间(当前时间+锁定时长)到本地存储(SharedPreferences/数据库)。
  2. 每次用户尝试操作时,对比当前时间与锁定结束时间:
    • 未到时间:提示锁定状态,计算剩余时长。
    • 已到时间:解除锁定,允许操作。
  3. 可选:用WorkManager调度一个延迟任务,在锁定结束时自动清除锁定记录或发送通知。

修正后的代码实现

1. ViewModel层(处理失败逻辑与锁定状态判断)

@HiltViewModel
class RegisterViewModel @Inject constructor(
    private val registerUseCase: RegisterUseCase,
    @ApplicationContext private val context: Context
) : ViewModel() {

    private val _cardBalance = MutableLiveData<YourDataModel?>()
    val cardBalance: LiveData<YourDataModel?> = _cardBalance

    // 本地存储相关配置
    private val lockPrefs = context.getSharedPreferences("register_lock", Context.MODE_PRIVATE)
    private val KEY_LOCK_END_TIME = "lock_end_time"
    private val KEY_FAILED_ATTEMPTS = "failed_attempts"
    private val MAX_FAILED_ATTEMPTS = 3 // 自定义允许的最大失败次数
    private val LOCK_DURATION = 30 * 60 * 1000L // 30分钟锁定时长

    fun makeApiPost(card: String/*, 其他参数*/) {
        viewModelScope.launch {
            val retroInstance = RetroInstance.getRetroInstanceAuth(context).create(RetroService::class.java)
            val call = retroInstance.getGiftCardBalance(CardBalanceRequest(card/*, 其他参数*/))
            
            call.enqueue(object : Callback<BalanceResponse> {
                override fun onResponse(call: Call<BalanceResponse>, response: Response<AppBalanceResponse>) {
                    if (response.isSuccessful) {
                        // 成功后重置失败次数与锁定状态
                        resetLockState()
                        _cardBalance.postValue(response.body()?.payload?.data)
                    } else {
                        handleFailedAttempt()
                        _cardBalance.postValue(null)
                    }
                }

                override fun onFailure(call: Call<CardBalanceResponse>, t: Throwable) {
                    t.printStackTrace()
                    handleFailedAttempt()
                    _cardBalance.postValue(null)
                }
            })
        }
    }

    /**
     * 处理失败尝试,达到阈值则触发锁定
     */
    private fun handleFailedAttempt() {
        val currentAttempts = lockPrefs.getInt(KEY_FAILED_ATTEMPTS, 0) + 1
        lockPrefs.edit().putInt(KEY_FAILED_ATTEMPTS, currentAttempts).apply()

        if (currentAttempts >= MAX_FAILED_ATTEMPTS) {
            val lockEndTime = System.currentTimeMillis() + LOCK_DURATION
            lockPrefs.edit().putLong(KEY_LOCK_END_TIME, lockEndTime).apply()

            // 调度锁定结束后的清理任务
            val cleanUpWork = OneTimeWorkRequestBuilder<LockCleanUpWorker>()
                .setInitialDelay(LOCK_DURATION, TimeUnit.MILLISECONDS)
                .build()
            WorkManager.getInstance(context).enqueue(cleanUpWork)
        }
    }

    /**
     * 重置锁定状态(成功后调用)
     */
    private fun resetLockState() {
        lockPrefs.edit()
            .putInt(KEY_FAILED_ATTEMPTS, 0)
            .remove(KEY_LOCK_END_TIME)
            .apply()
    }

    /**
     * 检查当前是否处于锁定状态
     */
    fun isFormLocked(): Boolean {
        val lockEndTime = lockPrefs.getLong(KEY_LOCK_END_TIME, 0)
        return System.currentTimeMillis() < lockEndTime
    }

    /**
     * 获取剩余锁定时长(毫秒)
     */
    fun getRemainingLockTime(): Long {
        val lockEndTime = lockPrefs.getLong(KEY_LOCK_END_TIME, 0)
        return max(0, lockEndTime - System.currentTimeMillis())
    }
}

2. WorkManager任务(锁定结束后清理状态)

class LockCleanUpWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {

    private val lockPrefs = context.getSharedPreferences("register_lock", Context.MODE_PRIVATE)
    private val KEY_LOCK_END_TIME = "lock_end_time"
    private val KEY_FAILED_ATTEMPTS = "failed_attempts"

    override suspend fun doWork(): Result {
        // 清理锁定记录
        lockPrefs.edit()
            .remove(KEY_LOCK_END_TIME)
            .putInt(KEY_FAILED_ATTEMPTS, 0)
            .apply()

        // 可选:发送通知告知用户表单已解锁
        sendUnlockNotification()

        return Result.success()
    }

    private fun sendUnlockNotification() {
        val notificationManager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        
        // 创建通知渠道(Android O及以上需要)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                "register_lock_channel",
                "注册锁定通知",
                NotificationManager.IMPORTANCE_DEFAULT
            )
            notificationManager.createNotificationChannel(channel)
        }

        val notification = NotificationCompat.Builder(applicationContext, "register_lock_channel")
            .setContentTitle("注册表单已解锁")
            .setContentText("您可以继续尝试注册了")
            .setSmallIcon(R.drawable.ic_notification) // 替换为你的应用图标
            .build()
        
        notificationManager.notify(1001, notification)
    }
}

关键优化点

  • 取消了错误的CountDownTimer用法,改用本地时间记录实现锁定逻辑,完全避免线程Looper问题。
  • WorkManager仅用来调度延迟清理任务,符合其设计初衷,不会持续占用系统资源。
  • 锁定状态持久化存储,APP重启后依然能正确判断锁定状态。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 04:03:14