Created
January 8, 2022 12:08
-
-
Save dracula151997/d76cab96f242cfb8084d02887bfd4bd4 to your computer and use it in GitHub Desktop.
Gist for implementing a StopWatch in your app
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.tutorial.runningapp.stopwatch | |
import javax.inject.Inject | |
class ClockTimestampProvider @Inject constructor() : TimestampProvider { | |
override fun getMilliseconds(): Long { | |
return System.currentTimeMillis() | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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