0. 단일 타이머 구현체

처음 코드를 작성했을 때는 타이머 로직 완성을 위해 단일 타이머로 개발을 했다.

구현체도 단일 로직에 맞추어 개발을 진행했다.

그렇다 보니, TimerRepository 인스턴스 수는 Singleton이므로 항상 1개였다.


1. 듀얼 타이머의 구현체…?

동일 구현체를 활용하여 각각 다른 두 개의 인스턴스를 만들어야했다.

구현체는 단일 타이머를 기준으로 만들었기에, 그대로 사용하면 아래와 같은 문제가 발생했다.

이러면 UI 입장에선 어떤 타이머가 업데이트한 건지 알 수 없다.


2. Singleton 어노테이션 제거

이를 통해 해결했다. 저걸 붙여놨으니 에러가 나지……

다만 기존 절대시간 방식이 마음에 들지 않아 delta 방식으로 코드를 변경했다.

/* 기존 방식*/

class TimerRepositoryImpl @Inject constructor() : TimerRepository {
    /**
     * 타이머 현재 경과 시간
     * 변수 명 뒤에 Millis를 붙여 이 변수가 어떤 단위인지 표기.
     **/
    private val _timeElapsedMillis = MutableStateFlow(0L)

    private var timerJob: Job? = null

    /**
     * 타이머의 최신 재개 시간
     **/
    private var startTimeMillis: Long = 0L

    /**
     * 일시 정지에 따른 타이머 누적 시간
     **/
    private var accumulatedTimeMillis: Long = 0L

    private val timerScope = CoroutineScope(Dispatchers.Default)

    override fun getTimerMillsUpdate(): Flow<Long> {
        return _timeElapsedMillis.asStateFlow()
    }

    /**
     * 타이머 시작
     */
    override fun startTimer() {
        if (timerJob?.isActive == true) return
        startTimeMillis = System.currentTimeMillis()

        timerJob = timerScope.launch {
            while (true) {
                /* Compose에서 delay가 없다면 UI는 event를 잔뜩 받고 성능이 저하됨. 따라서, 적절한 딜레이로 정확도와 성능 개선 */
                delay(100)

                val currentTimeMillis = System.currentTimeMillis()
                val elapsedTimeMillis = currentTimeMillis - startTimeMillis + accumulatedTimeMillis

                _timeElapsedMillis.value = elapsedTimeMillis
            }
        }
    }

    /**
     * 타이머 일시 정지
     */
    override fun pauseTimer() {
        timerJob?.cancel()
        timerJob = null

        /* 경과 시간을 누적 시간에 저장 */
        accumulatedTimeMillis = _timeElapsedMillis.value
    }

    /**
     * 타이머 종료
     */
    override fun stopTimer() {
        timerJob?.cancel()
        timerJob = null

        /* 전부 초기화 */
        startTimeMillis = 0L
        accumulatedTimeMillis = 0L
        _timeElapsedMillis.value = 0L
    }

}

변경된 delta 방식

class TimerRepositoryImpl @Inject constructor() : TimerRepository {

    private val _flow = MutableStateFlow(0L)
    override fun getTimerMillsUpdate(): Flow<Long> = _flow.asStateFlow()

    private var timerJob: Job? = null
    private var lastTimestamp = 0L
    private var accumulated = 0L

    private val scope = CoroutineScope(Dispatchers.Default)

    override fun startTimer() {
        if (timerJob != null) return

        lastTimestamp = System.currentTimeMillis()

        timerJob = scope.launch {
            while (true) {
                delay(100)

                val now = System.currentTimeMillis()
                val delta = now - lastTimestamp
                lastTimestamp = now

                accumulated += delta
                _flow.value = accumulated
            }
        }
    }

    override fun pauseTimer() {
        timerJob?.cancel()
        timerJob = null
    }

    override fun stopTimer() {
        timerJob?.cancel()
        timerJob = null

        accumulated = 0L
        lastTimestamp = 0L
        _flow.value = 0L
    }
}

로직을 더 간단하게 개선함으로써 잠재적인 버그 가능성을 좀 낮추었다.