Skip to content

Instantly share code, notes, and snippets.

@dracula151997
Created January 8, 2022 12:08
Show Gist options
  • Save dracula151997/d76cab96f242cfb8084d02887bfd4bd4 to your computer and use it in GitHub Desktop.
Save dracula151997/d76cab96f242cfb8084d02887bfd4bd4 to your computer and use it in GitHub Desktop.
Gist for implementing a StopWatch in your app
package com.tutorial.runningapp.stopwatch
import javax.inject.Inject
class ClockTimestampProvider @Inject constructor() : TimestampProvider {
override fun getMilliseconds(): Long {
return System.currentTimeMillis()
}
}
package com.tutorial.runningapp.stopwatch
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
class StopwatchListOrchestrator @Inject constructor(
private val stopWatchStateHolder: StopWatchStateHolder,
private val scope: CoroutineScope
) {
private var job: Job? = null
private val _ticker = MutableStateFlow("")
val ticker: StateFlow<String> = _ticker
private val _tickerInSeconds = MutableStateFlow("")
val tickerInSeconds: StateFlow<String> = _tickerInSeconds
fun start() {
if (job == null) startJob()
stopWatchStateHolder.start()
}
fun pause() {
stopWatchStateHolder.pause()
stopJob()
}
fun stop() {
stopWatchStateHolder.stop()
stopJob()
clearValue()
}
private fun clearValue() {
_ticker.value = TimestampMillisecondsFormatter.DEFAULT_FORMAT
}
private fun stopJob() {
scope.coroutineContext.cancelChildren()
job = null
}
private fun startJob() {
scope.launch {
while (isActive) {
_ticker.value = stopWatchStateHolder.timeRepresented
delay(20)
}
}
}
}
package com.tutorial.runningapp.stopwatch
sealed class StopWatchState {
/**
* The paused state contains an elapsed time used in order to preserve the time between
* stopwatch state changes
*/
data class Paused(
val elapsedTime: Long
) : StopWatchState()
/**
* The Running state contains information about the stopwatch was started and what
* the current elapsed time is
* At the beginning, the elapsed time will be 0 and every the stopwatch is paused and started this value changes
*/
data class Running(
val startTime: Long,
val elapsedTime: Long
) : StopWatchState()
}
package com.tutorial.runningapp.stopwatch
import javax.inject.Inject
/**
* To calculate the state changes between Running and Paused
*/
class StopWatchStateCalculator @Inject constructor(
private val timestampProvider: TimestampProvider,
private val elapsedTimeCalculator: ElapsedTimeCalculator
) {
/**
* If the state remains unchanged, the old state is just returned
*
* Paused -> Running
* In this state, Saves the current timestamp as the stopwatch start time
* The elapsed time is reused from the old state because the stopwatch should not be running in the pause state
*
* Running -> Paused
* In the elapsed time based on the stopwatch start time and the current elapsed time
*/
fun calculateRunningState(oldState: StopWatchState): StopWatchState.Running =
when (oldState) {
is StopWatchState.Paused -> StopWatchState.Running(
startTime = timestampProvider.getMilliseconds(),
elapsedTime = oldState.elapsedTime
)
is StopWatchState.Running -> oldState
}
fun calculatePausedState(oldState: StopWatchState): StopWatchState.Paused = when (oldState) {
is StopWatchState.Paused -> oldState
is StopWatchState.Running -> {
val elapsedTime = elapsedTimeCalculator.calculate(oldState)
StopWatchState.Paused(elapsedTime = elapsedTime)
}
}
}
/**
* To calculate the elapsed time
*/
class ElapsedTimeCalculator @Inject constructor(
private val timestampProvider: TimestampProvider
) {
/**
* The condition check (currentTimestamp - state.startTime) should always be true, but you never know
* elapsedTime = (current timestamp - the stopwatch start time) + the existing elapsed time
*/
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
}
}
package com.tutorial.runningapp.stopwatch
import javax.inject.Inject
class StopWatchStateHolder @Inject constructor(
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)
}
val timeRepresented: String
get() {
val elapsedTime = when (val currentState = currentState) {
is StopWatchState.Paused -> currentState.elapsedTime
is StopWatchState.Running -> elapsedTimeCalculator.calculate(currentState)
}
return timestampMillisecondsFormatter.format(elapsedTime)
}
}
package com.tutorial.runningapp.stopwatch
import javax.inject.Inject
class TimestampMillisecondsFormatter @Inject constructor() {
companion object {
const val DEFAULT_FORMAT = "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 hourFormatted = (minutes / 60).pad(2)
"$hourFormatted:$minutesFormatted:$secondsFormatted"
} else "$minutesFormatted:$secondsFormatted:$millisecondsFormatted"
}
private fun Long.pad(desiredLength: Int) = this.toString().padStart(desiredLength, '0')
}
package com.tutorial.runningapp.stopwatch
/**
* To provide the current timestamp of the system
*/
interface TimestampProvider {
fun getMilliseconds() : Long
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment