Kotlin 스톱과 프로세스를 사용하여 스톱워치 제1부분 - 단일 스톱워치

현재, 나는 사용자가 여러 개의 초시계를 시작할 수 있도록 하는 Jetpack Compose Android app를 개발하고 있다.
이것은 첫 번째 부분이다. 나는 간단한 버전의 실현을 보여 주었다. 그것은 단지 초시계 하나만 처리한다.The second part에는 여러 개의 초시계의 전면적인 실시와 테스트가 포함될 것이다.

프레젠테이션


스톱워치 상태를 표시하기 위해 나는 두 개의 하위 유형을 포함하는 밀봉류를 사용했다.
sealed class StopwatchState {

    data class Paused(
        val elapsedTime: Long
    ) : StopwatchState()

    data class Running(
        val startTime: Long,
        val elapsedTime: Long
    ) : StopwatchState()
}
일시 중지 상태는 초시계 상태 변경 사이의 시간을 보존하는 데 사용된 시간을 포함합니다.
운행 상태는 초시계가 언제 시작되는지, 현재 운행 시간에 대한 정보를 포함한다.시작할 때 경과하는 시간은 0이지만, 초시계가 멈추고 시작될 때마다 이 값은 변경됩니다.
스톱워치가 멈추면 상태를 변경할 수 없기 때문에 정지 상태는 필수적이지 않습니다.일시 중지 또는 실행 상태에서 최종 시간을 계산할 수 있습니다.
시스템의 현재 타임스탬프는 TimestampProvider 인터페이스에서 사용할 수 있습니다.
interface TimestampProvider {

    fun getMilliseconds(): Long
}
이 클래스에서 실행과 일시정지 사이의 상태 변경 사항을 계산합니다.
class StopwatchStateCalculator(
    private val timestampProvider: TimestampProvider,
    private val elapsedTimeCalculator: ElapsedTimeCalculator,
) {

    fun calculateRunningState(oldState: StopwatchState): StopwatchState.Running =
        when (oldState) {
            is StopwatchState.Running -> oldState
            is StopwatchState.Paused -> {
                StopwatchState.Running(
                    startTime = timestampProvider.getMilliseconds(),
                    elapsedTime = oldState.elapsedTime
                )
            }
        }

    fun calculatePausedState(oldState: StopwatchState): StopwatchState.Paused =
        when (oldState) {
            is StopwatchState.Running -> {
                val elapsedTime = elapsedTimeCalculator.calculate(oldState)
                StopwatchState.Paused(elapsedTime = elapsedTime)
            }
            is StopwatchState.Paused -> oldState
        }
}
상태가 변경되지 않으면 매개변수가 반환됩니다.
일시 중지 -> 실행: 현재 시간 스탬프를 초시계 시작 시간으로 저장합니다.일시 정지 상태에서 초시계가'운행'해서는 안 되기 때문에 이전 상태에서 지나간 시간을 다시 사용한다.
실행 -> 일시 정지: 초시계의 시작 시간과 현재 경과 시간에 따라 경과 시간을 계산한다.다음 절에서는 이 계산에 대해 해석할 것이다

경과 시간을 계산하다


class ElapsedTimeCalculator(
    private val timestampProvider: TimestampProvider,
) {

    fun calculate(state: StopwatchState.Running): Long {
        val currentTimestamp = timestampProvider.getMilliseconds()
        val timePassedSinceStart = if (currentTimestamp > state.startTime) {
            currentTimestamp - state.startTime
        } else {
            0
        }
        return timePassedSinceStart + state.elapsedTime
    }
}
조건 검사 (current Timestamp >state.startTime) 는 항상 진실이어야 하지만, 영원히 알 수 없습니다.계산 운행 시간은 현재 시간 스탬프에서 초시계의 시작 시간을 빼고 결과를 기존 운행 시간에 추가하는 것을 포함한다.이 경과 시간은 초시계 상태 변경 사이의 시간을 저장하는 것을 책임진다.

다음은 그림code의 외관을 보여 주는 테스트입니다. 타임 스탬프 제공 프로그램은 시뮬레이션이고 ElapsedTimeCalculator는 진정한 실현입니다.
@Test
fun `Article example test`() {
    val timestampProvider: TimestampProvider = mockk()
    val elapsedTimeCalculator = ElapsedTimeCalculator(timestampProvider)
    val stopwatchStateCalculator = StopwatchStateCalculator(
        timestampProvider = timestampProvider,
        elapsedTimeCalculator = elapsedTimeCalculator
    )

    every { timestampProvider.getMilliseconds() } returns 0
    val initialState = StopwatchState.Paused(0L)
    val firstStart = stopwatchStateCalculator.calculateRunningState(initialState)
    expectThat(firstStart.startTime).isEqualTo(0L)
    expectThat(firstStart.elapsedTime).isEqualTo(0L)

    every { timestampProvider.getMilliseconds() } returns 100
    val firstPause = stopwatchStateCalculator.calculatePausedState(firstStart)
    expectThat(firstPause.elapsedTime).isEqualTo(100L)

    every { timestampProvider.getMilliseconds() } returns 1000
    val secondStart = stopwatchStateCalculator.calculateRunningState(firstPause)
    expectThat(secondStart.startTime).isEqualTo(1000L)
    expectThat(secondStart.elapsedTime).isEqualTo(100L)

    every { timestampProvider.getMilliseconds() } returns 1500
    val secondPause = stopwatchStateCalculator.calculatePausedState(secondStart)
    expectThat(secondPause.elapsedTime).isEqualTo(600L)
}

스톱워치 포맷 시간


포맷 프로그램이 타임스탬프를 수신하고 UI에 대해 사람이 읽을 수 있는 시간을 생성합니다.
internal class TimestampMillisecondsFormatter() {

    companion object {
        const val DEFAULT_TIME = "00:00:000"
    }

    fun format(timestamp: Long): String {
        val millisecondsFormatted = (timestamp % 1000).pad(3)
        val seconds = timestamp / 1000
        val secondsFormatted = (seconds % 60).pad(2)
        val minutes = seconds / 60
        val minutesFormatted = (minutes % 60).pad(2)
        val hours = minutes / 60
        return if (hours > 0) {
            val hoursFormatted = (minutes / 60).pad(2)
            "$hoursFormatted:$minutesFormatted:$secondsFormatted"
        } else {
            "$minutesFormatted:$secondsFormatted:$millisecondsFormatted"
        }
    }

    private fun Long.pad(desiredLength: Int) = this.toString().padStart(desiredLength, '0')
}
초시계의 초기 시간은 밀리초를 표시하기 때문에 더욱 동적이다. 그러나 한 시간 후에는 밀리초를 무시하여 시간을 표시한다.그 밖에 이 숫자들은 하나 또는 두 개의 전도 0으로 채워져 있다.

스톱워치 상태 관리


상태는 자체 포함되며 동작은 포함되지 않으므로 상태 변경을 관리하려면 추가 클래스가 필요합니다.
internal class StopwatchStateHolder(
    private val stopwatchStateCalculator: StopwatchStateCalculator,
    private val elapsedTimeCalculator: ElapsedTimeCalculator,
    private val timestampMillisecondsFormatter: TimestampMillisecondsFormatter,
) {

    var currentState: StopwatchState = StopwatchState.Paused(0)
        private set

    fun start() {
        currentState = stopwatchStateCalculator.calculateRunningState(currentState)
    }

    fun pause() {
        currentState = stopwatchStateCalculator.calculatePausedState(currentState)
    }

    fun stop() {
        currentState = StopwatchState.Paused(0)
    }

    fun getStringTimeRepresentation(): String {
        val elapsedTime = when (val currentState = currentState) {
            is StopwatchState.Paused -> currentState.elapsedTime
            is StopwatchState.Running -> elapsedTimeCalculator.calculate(currentState)
        }
        return timestampMillisecondsFormatter.format(elapsedTime)
    }
}
이 유형은 스톱워치 상태를 관리하는 API를 제공합니다.편의를 위해 이 종류는 초시계의 포맷 운행 시간을 검색하는 방법을 공개했다.

스트림이 있는 Kotlin 스레드 사용


stopwatch State Holder는 stopwatch와 상호작용에 필요한 모든 기능을 제공하지만, stopwatch 업데이트는 주 라인에서 실행되어서는 안 됩니다.이렇게 하면 사용할 수 없는 UI를 만들 수 있는데, 그 중 유일하게 발생하는 것은 초시계의 변경이다.
스톱워치 업데이트를 백그라운드 스레드로 제거하려면 추가 추상을 만들어야 합니다.
internal class StopwatchListOrchestrator(
    private val stopwatchStateHolder: StopwatchStateHolder,
    private val scope: CoroutineScope,
) {

    private var job: Job? = null
    private val mutableTicker = MutableStateFlow("")
    val ticker: StateFlow<String> = mutableTicker

    fun start() {
        if (job == null) startJob()
        stopwatchStateHolder.start()
    }

    private fun startJob() {
        scope.launch {
            while (isActive) {
                mutableTicker.value = stopwatchStateHolder.getStringTimeRepresentation()
                delay(20)
            }
        }
    }

    fun pause() {
        stopwatchStateHolder.pause()
        stopJob()
    }

    fun stop() {
        stopwatchStateHolder.stop()
        stopJob()
        clearValue()
    }

    private fun stopJob() {
        scope.coroutineContext.cancelChildren()
        job = null
    }

    private fun clearValue() {
        mutableTicker.value = ""
    }
}
본문은 StopwatchListOrchestrator의 실현을 간소화시켰다는 것을 명심하세요.진정한 실현은 여러 개의 초시계를 동시에 처리할 수 있다.협동관찰경을 주입한 이유는 테스트와 관련이 있지만 다음 부분에서 더 깊이 있게 설명할 예정이다.
start 함수에서 협동 프로그램 작업을 만듭니다. 이 작업은 초시계 시간을 업데이트합니다.이것은while 순환으로 구성되어 있으며, 이 순환은 협동 작업이 여전히 활동 상태에 있는지 확인하는 조건을 가지고 있다.순환 중, 초시계 시간은 20밀리초 간격으로 업데이트됩니다.시간 업데이트는 상태 흐름을 통해 수행되며 상태 흐름은 최종적으로 UI로 수집됩니다.
스톱워치 시간이 업데이트되지 않기 때문에 스톱워치 기능과 스톱워치 기능이 함께 작동하지 않습니다.또한 stop 함수는 스톱워치를 리셋하고 스톱워치 시간의 기본값을 설정합니다. (빈 문자열은 스톱워치가 UI에 나타나지 않도록 합니다.)
협정 작업은 cancelChildren() 대신 cancel() 사용을 중지하였음을 주의하십시오.하위 프로세스의 범위를 취소하고 다른 협동 프로세스를 시작할 수 있도록 합니다."취소"방법을 사용하면 초시계가 일시 정지되거나 정지된 후에 작동하지 못하게 합니다.이렇게 하는 이유는 cancel() 전체 협동 프로그램 역할 영역을 없애고 새로운 coutine이 이 역할 영역에서 시작하는 것을 막기 때문이다.

응용 프로그램의 외관


요약


여기에 표시된 코드와 프로그램의 나머지 부분은 myGitHub에서 찾을 수 있습니다.
Kotlin 협동 프로그램은 백엔드 조작을 시작하고 정지하는 간단한 방법을 제공합니다. 전제는 모든 협동 프로그램의gotchas가 처리되었다는 것입니다. (예를 들어 cancelChildren을 사용하지 않고 cancel을 사용합니다.)
흐름은 비동기 흐름을 만들고 사용하기 위해 아름답고 간단한 API를 제공합니다. 이것은 성명식 UI의 유행과 잘 어울립니다.
이러한 개념을 결합하면 간단한 초시계를 실현하는 것이 얼마나 쉬운지 알 수 있다(타이머도 비슷한 방식으로 실현할 수 있다).누드 스레드를 사용하거나 RxJava 스트림을 일정 간격으로 만들 필요가 없습니다.
다음 글은 본고에서 묘사한 내용을 바탕으로 여러 개의 동시 초시계에 대한 지원을 추가할 것이다.

좋은 웹페이지 즐겨찾기