CountDownTimer → Flow
老写法(Java)
java
new CountDownTimer(30000, 1000) {
@Override
public void onTick(long millisUntilFinished) {
long seconds = millisUntilFinished / 1000;
tvCountdown.setText("剩余 " + seconds + " 秒");
}
@Override
public void onFinish() {
tvCountdown.setText("完成");
btnAction.setEnabled(true);
}
}.start();
问题在哪里
CountDownTimer 不感知生命周期,Activity 退出后 tick 还在跑,回调会操作已销毁的 View 导致空指针。onTick 的精度也不保证精确的 1000ms,可能出现几毫秒到几十毫秒的漂移。
新写法(Kotlin + Flow)
kotlin
// ViewModel 中
val countdownFlow: Flow<Int> = flow {
for (i in 30 downTo 0) {
emit(i)
delay(1000)
}
}
// Activity / Fragment 中
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.countdownFlow.collect { seconds ->
when (seconds) {
0 -> {
tvCountdown.text = "完成"
btnAction.isEnabled = true
}
else -> tvCountdown.text = "剩余 $seconds 秒"
}
}
}
}
或者直接在 ViewModel 里封装好状态:
kotlin
class SmsViewModel : ViewModel() {
private val _countdown = MutableStateFlow(60)
val countdown: StateFlow<Int> = _countdown
private var job: Job? = null
fun startCountdown() {
job?.cancel()
job = viewModelScope.launch {
for (i in 60 downTo 0) {
_countdown.value = i
delay(1000)
}
}
}
override fun onCleared() {
job?.cancel()
super.onCleared()
}
}
一句话注意
viewLifecycleOwner.lifecycleScope 配合 repeatOnLifecycle 确保 Fragment 视图销毁后自动停止收集,ViewModel 里可以继续计时,但不会去更新已经销毁的 UI,完美避开 CountDownTimer 的泄漏问题。
如果倒计时对精度要求高(比如计时器),注意 delay 也有微小漂移,应记录起始时间戳,每次计算剩余时间:
kotlin
flow {
val start = System.currentTimeMillis()
val total = 30_000L
while (true) {
val elapsed = System.currentTimeMillis() - start
val remaining = ((total - elapsed) / 1000).toInt()
if (remaining <= 0) { emit(0); break }
emit(remaining)
delay(1000)
}
}
Java Android 老项目迁移系列,持续更新中。