1. 需求场景与核心功能拆解
在Android应用开发中,定时器功能是高频需求场景。不同于简单的倒计时显示,我们需要实现一个具备完整生命周期的定时器组件,具体要求包括:
- 精确到秒级的计时能力
- 运行过程中支持手动暂停/继续
- 计时结束时自动触发回调
- 线程安全的生命周期管理
这种组件常见于运动类APP的间歇训练计时、阅读类APP的专注模式倒计时、电商类APP的限时抢购等场景。以健身应用为例,用户可能需要设置30秒的组间休息时间,期间可能临时暂停查看动作要点,之后继续计时。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Kotlin实现方案选型
2.1 主流定时方案对比
| 方案 | 精度 | 线程模型 | 生命周期管理 | 适用场景 |
|---|---|---|---|---|
| Handler.postDelayed | 一般 | 主线程 | 手动取消 | 简单延时任务 |
| Timer/TimerTask | 较高 | 子线程 | 容易内存泄漏 | 已不推荐使用 |
| ScheduledThreadPool | 高 | 线程池管理 | 可控性好 | 复杂定时任务 |
| CountDownTimer | 较低 | 主线程 | 自动结束 | 简单倒计时 |
| Coroutine Delay | 一般 | 协程上下文 | 结构化并发 | 协程环境定时 |
2.2 最优解:协程+Flow方案
基于实际需求,我们选择协程配合StateFlow实现,优势在于:
- 天然支持结构化并发,自动取消避免内存泄漏
- Flow提供响应式状态更新
- 挂起函数完美支持暂停/继续逻辑
- 可轻松扩展到ViewModel中实现生命周期感知
3. 完整实现代码解析
3.1 核心数据结构设计
kotlin复制sealed class TimerState {
object Idle : TimerState()
data class Running(val remainingMs: Long) : TimerState()
data class Paused(val remainingMs: Long) : TimerState()
object Finished : TimerState()
}
class SmartTimer(
private val totalDuration: Long, // 总时长(毫秒)
private val interval: Long = 1000L, // 更新间隔(毫秒)
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
) {
private val _state = MutableStateFlow<TimerState>(TimerState.Idle)
val state: StateFlow<TimerState> = _state.asStateFlow()
private var timerJob: Job? = null
private var pausedRemaining = totalDuration
}
3.2 启动逻辑实现
kotlin复制fun start() {
if (_state.value is TimerState.Running) return
timerJob = scope.launch {
_state.value = TimerState.Running(pausedRemaining)
var remaining = pausedRemaining
while (remaining > 0 && isActive) {
delay(interval.coerceAtMost(remaining))
remaining -= interval
_state.value = TimerState.Running(remaining)
}
if (remaining <= 0) {
_state.value = TimerState.Finished
onFinish?.invoke()
}
}
}
3.3 暂停/继续控制
kotlin复制fun pause() {
if (_state.value !is TimerState.Running) return
timerJob?.cancel()
val runningState = _state.value as TimerState.Running
pausedRemaining = runningState.remainingMs
_state.value = TimerState.Paused(pausedRemaining)
}
fun resume() {
if (_state.value !is TimerState.Paused) return
start()
}
3.4 回调与资源清理
kotlin复制var onFinish: (() -> Unit)? = null
fun cancel() {
timerJob?.cancel()
_state.value = TimerState.Idle
pausedRemaining = totalDuration
}
override fun onCleared() {
cancel()
scope.cancel()
}
4. 实际应用中的关键细节
4.1 生命周期绑定最佳实践
建议在ViewModel中使用并绑定到界面生命周期:
kotlin复制class TimerViewModel : ViewModel() {
private val timer = SmartTimer(
totalDuration = 30_000L,
scope = viewModelScope
)
init {
timer.onFinish = {
// 处理计时完成逻辑
}
}
// 暴露给UI的接口...
}
4.2 界面状态更新策略
使用StateFlow配合Lifecycle实现高效更新:
kotlin复制@Composable
fun TimerScreen(viewModel: TimerViewModel) {
val timerState by viewModel.timerState.collectAsStateWithLifecycle()
when (val state = timerState) {
is TimerState.Running -> {
Text("剩余: ${state.remainingMs / 1000}s")
Button(onClick = { viewModel.pause() }) { Text("暂停") }
}
is TimerState.Paused -> {
Text("已暂停: ${state.remainingMs / 1000}s")
Button(onClick = { viewModel.resume() }) { Text("继续") }
}
TimerState.Finished -> Text("计时完成!")
TimerState.Idle -> Button(onClick = { viewModel.start() }) { Text("开始") }
}
}
4.3 精度补偿机制
长时间运行可能出现误差累积,建议添加补偿逻辑:
kotlin复制val startTime = System.currentTimeMillis()
var expectedRemaining = pausedRemaining
while (expectedRemaining > 0 && isActive) {
val elapsed = System.currentTimeMillis() - startTime
expectedRemaining = totalDuration - elapsed
val delayTime = expectedRemaining.coerceIn(0, interval)
if (delayTime > 0) delay(delayTime)
_state.value = TimerState.Running(expectedRemaining)
}
5. 性能优化与常见问题
5.1 内存泄漏防护
必须注意的三种泄漏场景:
- 未取消的协程任务
- 持有Activity/Fragment的闭包
- 静态回调引用
防护措施:
kotlin复制// 在ViewModel中自动清理
override fun onCleared() {
timer.cancel()
}
// 避免直接引用View
timer.onFinish = {
// 错误做法:直接调用view方法
// 正确做法:通过LiveData/StateFlow通知
_finishedEvent.value = Unit
}
5.2 后台计时准确性
应用进入后台时的策略选择:
| 策略 | 优点 | 缺点 |
|---|---|---|
| 持续精确计时 | 准确性高 | 耗电量大 |
| 使用AlarmManager | 省电 | 恢复时需要同步状态 |
| 估算补偿 | 平衡性能与准确性 | 复杂场景可能有误差 |
推荐实现方案:
kotlin复制// 在Application中监听生命周期
class TimerApp : Application() {
override fun onCreate() {
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
override fun onActivityPaused(activity: Activity) {
if (isAppInBackground()) {
// 记录暂停时的时间戳
timer.saveBackgroundTime()
}
}
override fun onActivityResumed(activity: Activity) {
if (wasAppInBackground()) {
// 恢复时计算时间差并补偿
timer.adjustForBackgroundTime()
}
}
})
}
}
5.3 多定时器管理
需要同时管理多个定时器时的架构建议:
- 使用Map保存多个定时器实例
kotlin复制val timers = mutableMapOf<String, SmartTimer>()
- 通过ID进行引用控制
kotlin复制fun getTimer(id: String): SmartTimer {
return timers.getOrPut(id) {
SmartTimer(...).apply { onFinish = { timers.remove(id) } }
}
}
- 统一生命周期管理
kotlin复制fun clearAll() {
timers.values.forEach { it.cancel() }
timers.clear()
}
6. 测试验证方案
6.1 单元测试要点
kotlin复制@Test
fun testTimerFlow() = runTest {
val timer = SmartTimer(totalDuration = 3000L, interval = 1000L)
val states = mutableListOf<TimerState>()
val collectJob = launch {
timer.state.collect { states.add(it) }
}
timer.start()
advanceTimeBy(1000)
timer.pause()
advanceTimeBy(2000)
timer.resume()
advanceTimeBy(3000)
assertThat(states).containsExactly(
TimerState.Idle,
TimerState.Running(3000),
TimerState.Running(2000),
TimerState.Paused(2000),
TimerState.Running(2000),
TimerState.Running(1000),
TimerState.Finished
)
collectJob.cancel()
}
6.2 界面测试策略
使用Espresso进行界面同步测试:
kotlin复制@Test
fun testTimerDisplay() {
val scenario = launchFragmentInContainer<TimerFragment>()
// 验证初始状态
onView(withText("开始")).check(matches(isDisplayed()))
// 启动计时
onView(withText("开始")).perform(click())
onView(withText("剩余: 30s")).check(matches(isDisplayed()))
// 暂停测试
onView(withText("暂停")).perform(click())
onView(withText("已暂停: 25s")).check(matches(isDisplayed()))
// 继续测试
onView(withText("继续")).perform(click())
onView(withText("剩余: 25s")).check(matches(isDisplayed()))
}
6.3 边界条件验证
需要特别测试的边界场景:
- 多次快速点击开始/暂停按钮
- 计时结束瞬间调用暂停
- 后台长时间停留后恢复
- 配置变更(旋转屏幕)时的状态保持
- 极端时间值(0ms或极大值)处理
kotlin复制@Test
fun testBoundaryCases() = runTest {
// 测试0时长
val zeroTimer = SmartTimer(0L)
zeroTimer.start()
assertThat(zeroTimer.state.value).isInstanceOf(TimerState.Finished::class.java)
// 测试多次暂停/继续
val timer = SmartTimer(5000L)
timer.start()
repeat(10) {
timer.pause()
timer.resume()
}
advanceTimeBy(5000)
assertThat(timer.state.value).isInstanceOf(TimerState.Finished::class.java)
}
7. 高级扩展方向
7.1 跨进程持久化计时
实现应用被杀后仍能恢复计时:
kotlin复制// 使用WorkManager持久化计时任务
val timerWork = OneTimeWorkRequestBuilder<TimerWorker>()
.setInitialDelay(duration, TimeUnit.MILLISECONDS)
.build()
WorkManager.getInstance(context).enqueue(timerWork)
class TimerWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
// 通知应用计时完成
sendBroadcast(Intent("TIMER_FINISHED"))
return Result.success()
}
}
7.2 多平台共享逻辑
使用Kotlin Multiplatform实现跨平台核心逻辑:
kotlin复制expect class PlatformTimer() {
fun getCurrentTime(): Long
}
class CommonTimer {
private val platformTimer = PlatformTimer()
fun start() {
val startTime = platformTimer.getCurrentTime()
// 共享计时逻辑...
}
}
// Android实现
actual class PlatformTimer actual constructor() {
actual fun getCurrentTime(): Long = System.currentTimeMillis()
}
7.3 可视化自定义配置
支持运行时动态调整的计时参数:
kotlin复制data class TimerConfig(
val duration: Long,
val interval: Long = 1000L,
val tickSound: Sound? = null,
val finishSound: Sound? = null,
val vibrationPattern: LongArray? = null
)
class ConfigurableTimer(config: TimerConfig) {
private var currentConfig = config
fun updateConfig(newConfig: TimerConfig) {
currentConfig = newConfig
// 应用新配置...
}
}
