Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Kotlin Timer library provides robust and flexible CountdownTimer and Stopwatch functionalities implemented using Kotlin Coroutines Flow.

License

Notifications You must be signed in to change notification settings

jksalcedo/timer-kt

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Timer Library

Kotlin

The Timer library provides robust and flexible CountdownTimer and Stopwatch functionalities implemented using Kotlin Coroutines Flow. It's ideal for applications requiring precise time management, such as quiz apps, workout trackers, or game timers.

Features

Pure Kotlin Logic: The core timer logic is platform-agnostic, making it highly reusable in any JVM-based Kotlin project.

Reactive Updates: Leverages kotlinx.coroutines.flow to provide real-time updates for elapsedTime, remainingTime, and isRunning status.

CountdownTimer:

Start, pause, resume, stop, and restart functionalities.

Emits the remainingTime until zero.

Stopwatch:

Tracks total elapsedTime.

Supports lap() times (individual durations between lap presses).

Supports split() times (total elapsed time at specific marks).

Start, pause, stop, and reset functionalities.

High Precision: Uses kotlin.time.Duration and kotlin.time.TimeSource.Monotonic for accurate time tracking, even with millisecond precision.

API

Function / Class Description Parameters Returns
CountdownTimer A versatile countdown timer that can be started, paused, resumed, and stopped. Time updates are provided via a StateFlow. coroutineScope: CoroutineScope, tickInterval: Duration = 1.seconds —
- remainingTime: StateFlow<Duration> A StateFlow representing the time remaining on the countdown timer. Emits Duration.ZERO when inactive or finished. — StateFlow<Duration>
- isRunning: StateFlow<Boolean> A StateFlow indicating whether the countdown timer is currently running. — StateFlow<Boolean>
- start(duration: Duration) Starts the countdown timer from the specified duration. Restarts if already running. duration: Duration (must be positive) Unit
- pause() Pauses the countdown timer, preserving the remaining time. No effect if already paused. — Unit
- resume() Resumes the countdown timer from its current remainingTime. No effect if running or no time remains. — Unit
- stop() Stops the countdown timer and resets remainingTime to Duration.ZERO. No effect if not running. — Unit
- restart() Restarts the timer from its initial duration. No effect if no initial duration set. — Unit
Stopwatch A versatile stopwatch that can track elapsed time, individual lap deltas, and total time splits. coroutineScope: CoroutineScope, tickInterval: Duration = 100.milliseconds, timeSource: TimeSource = TimeSource.Monotonic —
- elapsedTime: StateFlow<Duration> A StateFlow representing the total elapsed time of the stopwatch. — StateFlow<Duration>
- isRunning: StateFlow<Boolean> A StateFlow indicating whether the stopwatch is currently running. — StateFlow<Boolean>
- lapTimes: StateFlow<List<Duration>> A list of individual lap durations (time between each lap press). — StateFlow<List<Duration>>
- splitTimes: StateFlow<List<Duration>> A list of total elapsed times recorded at each split press. — StateFlow<List<Duration>>
- start() Starts or resumes the stopwatch. No effect if already running. — Unit
- pause() Pauses the stopwatch. No effect if already paused. — Unit
- stop() Stops and resets the stopwatch completely. — Unit
- lap() Records a lap time (duration since the last lap). No effect if not running. — Unit
- split() Records a split time (total elapsed time at the moment). No effect if not running. — Unit
- reset() Resets the stopwatch to zero. Continues running if it was running. — Unit

Installation

Add the JitPack repository to your project's root build.gradle.kts (or build.gradle):

// settings.gradle.kts
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_NON_DECLARED_REPOSITORIES)
    repositories {
    google()
    mavenCentral()
    maven { url 'https://jitpack.io' } // Add this line
    }
}

Then, add the timer-kotlin dependency to your app's build.gradle.kts (or build.gradle):

// app/build.gradle.kts
dependencies {
    implementation("com.github.jksalcedo:timer-kotlin:1.0.0") // Or the latest version
    // Ensure you also have kotlinx-coroutines-android for ViewModelScope
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3")
    implementation("androidx.activity:activity-ktx:1.9.0") // For viewModels() delegate
}

Usage

Initialize CountdownTimer and Stopwatch instances within a CoroutineScope (e.g., viewModelScope for Android apps). Observe their StateFlows to update your UI.

  1. TimerViewModel.kt (Example ViewModel)

It's highly recommended to encapsulate timer logic within a ViewModel for lifecycle awareness and separation of concerns.

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.jksalcedo.timer.CountdownTimer
import com.jksalcedo.timer.Stopwatch
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

class TimerViewModel : ViewModel() {

    // --- Countdown Timer ---
    private val countdownTimer = CountdownTimer(viewModelScope)

    val countdownText = countdownTimer.remainingTime
        .map { duration -> formatDuration(duration) }
        .stateIn(
            viewModelScope,
            SharingStarted.WhileSubscribed(5000),
            formatDuration(Duration.ZERO)
        )

    val isCountdownRunning = countdownTimer.isRunning.asStateFlow()

    fun startCountdown(duration: Duration) { countdownTimer.start(duration) }
    fun pauseCountdown() { countdownTimer.pause() }
    fun resumeCountdown() { countdownTimer.resume() }
    fun stopCountdown() { countdownTimer.stop() }
    fun restartCountdown() { countdownTimer.restart() }

    // --- Stopwatch ---
    private val stopwatch = Stopwatch(viewModelScope)

    val stopwatchText = stopwatch.elapsedTime
        .map { duration -> formatDuration(duration, includeMilliseconds = true) }
        .stateIn(
            viewModelScope,
            SharingStarted.WhileSubscribed(5000),
            formatDuration(Duration.ZERO, includeMilliseconds = true)
        )

    val isStopwatchRunning = stopwatch.isRunning.asStateFlow()
    val lapTimes = stopwatch.lapTimes.asStateFlow()
    val splitTimes = stopwatch.splitTimes.asStateFlow()

    fun startStopwatch() { stopwatch.start() }
    fun pauseStopwatch() { stopwatch.pause() }
    fun stopStopwatch() { stopwatch.stop() }
    fun lapStopwatch() { stopwatch.lap() }
    fun splitStopwatch() { stopwatch.split() }
    fun resetStopwatch() { stopwatch.reset() }

    // --- Utility formatting function ---
    private fun formatDuration(duration: Duration, includeMilliseconds: Boolean = false): String {
        val totalSeconds = duration.inWholeSeconds
        val minutes = totalSeconds / 60
        val seconds = totalSeconds % 60
        val milliseconds = (duration - totalSeconds.seconds).inWholeMilliseconds

        return if (includeMilliseconds) {
            String.format("%02d:%02d.%03d", minutes, seconds, milliseconds)
        } else {
            String.format("%02d:%02d", minutes, seconds)
        }
    }
}
  1. MainActivity.kt (Example UI Integration)

Observe the StateFlows from your ViewModel to update UI elements.

import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds

class MainActivity : AppCompatActivity() {

    private val timerViewModel: TimerViewModel by viewModels()

    // UI elements (declare as lateinit var and initialize in onCreate)
    private lateinit var countdownTextView: TextView
    private lateinit var startCountdownButton: Button
    private lateinit var var pauseCountdownButton: Button
    private lateinit var var resumeCountdownButton: Button
    private lateinit var var stopCountdownButton: Button
    private lateinit var var restartCountdownButton: Button

    private lateinit var stopwatchTextView: TextView
    private lateinit var startStopwatchButton: Button
    private lateinit var pauseStopwatchButton: Button
    private lateinit var lapStopwatchButton: Button
    private lateinit var splitStopwatchButton: Button
    private lateinit var stopStopwatchButton: Button
    private lateinit var resetStopwatchButton: Button
    private lateinit var lapTimesTextView: TextView
    private lateinit var splitTimesTextView: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Initialize UI elements
        countdownTextView = findViewById(R.id.countdownTextView)
        startCountdownButton = findViewById(R.id.startCountdownButton)
        pauseCountdownButton = findViewById(R.id.pauseCountdownButton)
        resumeCountdownButton = findViewById(R.id.resumeCountdownButton)
        stopCountdownButton = findViewById(R.id.stopCountdownButton)
        restartCountdownButton = findViewById(R.id.restartCountdownButton)

        stopwatchTextView = findViewById(R.id.stopwatchTextView)
        startStopwatchButton = findViewById(R.id.startStopwatchButton)
        pauseStopwatchButton = findViewById(R.id.pauseStopwatchButton)
        lapStopwatchButton = findViewById(R.id.lapStopwatchButton)
        splitStopwatchButton = findViewById(R.id.splitStopwatchButton)
        stopStopwatchButton = findViewById(R.id.stopStopwatchButton)
        resetStopwatchButton = findViewById(R.id.resetStopwatchButton)
        lapTimesTextView = findViewById(R.id.lapTimesTextView)
        splitTimesTextView = findViewById(R.id.splitTimesTextView)

        // Set up click listeners for countdown timer buttons
        startCountdownButton.setOnClickListener { timerViewModel.startCountdown(60.seconds) }
        pauseCountdownButton.setOnClickListener { timerViewModel.pauseCountdown() }
        resumeCountdownButton.setOnClickListener { timerViewModel.resumeCountdown() }
        stopCountdownButton.setOnClickListener { timerViewModel.stopCountdown() }
        restartCountdownButton.setOnClickListener { timerViewModel.restartCountdown() }

        // Set up click listeners for stopwatch buttons
        startStopwatchButton.setOnClickListener { timerViewModel.startStopwatch() }
        pauseStopwatchButton.setOnClickListener { timerViewModel.pauseStopwatch() }
        lapStopwatchButton.setOnClickListener { timerViewModel.lapStopwatch() }
        splitStopwatchButton.setOnClickListener { timerViewModel.splitStopwatch() }
        stopStopwatchButton.setOnClickListener { timerViewModel.stopStopwatch() }
        resetStopwatchButton.setOnClickListener { timerViewModel.resetStopwatch() }

        // Observe the ViewModel's StateFlows and update the UI
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                launch { timerViewModel.countdownText.collect { text -> countdownTextView.text = text } }
                launch { timerViewModel.stopwatchText.collect { text -> stopwatchTextView.text = text } }
                launch {
                    timerViewModel.lapTimes.collect { lapTimes ->
                        val formattedLaps = lapTimes.joinToString(separator = "\n") { duration ->
                            val totalSeconds = duration.inWholeSeconds
                            val minutes = totalSeconds / 60
                            val seconds = totalSeconds % 60
                            val milliseconds = (duration - totalSeconds.seconds).inWholeMilliseconds
                            String.format("%02d:%02d.%03d", minutes, seconds, milliseconds)
                        }
                        lapTimesTextView.text = if (lapTimes.isEmpty()) "Lap Times: " else "Lap Times:\n$formattedLaps"
                    }
                }
                launch {
                    timerViewModel.splitTimes.collect { splitTimes ->
                        val formattedSplits = splitTimes.joinToString(separator = "\n") { duration ->
                            val totalSeconds = duration.inWholeSeconds
                            val minutes = totalSeconds / 60
                            val seconds = totalSeconds % 60
                            val milliseconds = (duration - totalSeconds.seconds).inWholeMilliseconds
                            String.format("%02d:%02d.%03d", minutes, seconds, milliseconds)
                        }
                        splitTimesTextView.text = if (splitTimes.isEmpty()) "Split Times: " else "Split Times:\n$formattedSplits"
                    }
                }
            }
        }
    }
}
  1. activity_main.xml (Example Layout)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
android:gravity="center_horizontal"
tools:context=".MainActivity">

    <!-- Countdown Timer Section -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Countdown Timer"
        android:textSize="24sp"
        android:textStyle="bold"
        android:layout_marginTop="24dp"
        android:layout_marginBottom="8dp"/>

    <TextView
        android:id="@+id/countdownTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="00:00"
        android:textSize="48sp"
        android:textStyle="bold"
        android:textColor="@android:color/black"
        tools:text="05:30"/>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="16dp">

        <Button
            android:id="@+id/startCountdownButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Start (60s)"
            android:layout_marginEnd="8dp"/>

        <Button
            android:id="@+id/pauseCountdownButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Pause"
            android:layout_marginEnd="8dp"/>

        <Button
            android:id="@+id/resumeCountdownButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Resume"
            android:layout_marginEnd="8dp"/>

        <Button
            android:id="@+id/stopCountdownButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Stop" />

    </LinearLayout>

    <Button
        android:id="@+id/restartCountdownButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Restart Last Countdown"
        android:layout_marginTop="8dp"/>

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="@android:color/darker_gray"
        android:layout_marginTop="32dp"
        android:layout_marginBottom="32dp"/>

    <!-- Stopwatch Section -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Stopwatch"
        android:textSize="24sp"
        android:textStyle="bold"
        android:layout_marginBottom="8dp"/>

    <TextView
        android:id="@+id/stopwatchTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="00:00.000"
        android:textSize="48sp"
        android:textStyle="bold"
        android:textColor="@android:color/black"
        tools:text="01:23.456"/>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="16dp">

        <Button
            android:id="@+id/startStopwatchButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Start"
            android:layout_marginEnd="8dp"/>

        <Button
            android:id="@+id/pauseStopwatchButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Pause"
            android:layout_marginEnd="8dp"/>

        <Button
            android:id="@+id/lapStopwatchButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Lap"
            android:layout_marginEnd="8dp"/>

        <Button
            android:id="@+id/splitStopwatchButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Split"
            android:layout_marginEnd="8dp"/>

        <Button
            android:id="@+id/stopStopwatchButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Stop" />

    </LinearLayout>

    <Button
        android:id="@+id/resetStopwatchButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Reset"
        android:layout_marginTop="8dp"/>

    <TextView
        android:id="@+id/lapTimesTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Lap Times: "
        android:layout_marginTop="16dp"
        android:textSize="16sp"
        android:gravity="center_horizontal"/>

    <TextView
        android:id="@+id/splitTimesTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Split Times: "
        android:layout_marginTop="8dp"
        android:textSize="16sp"
        android:gravity="center_horizontal"/>

</LinearLayout>

Contributing

Contributions are welcome! To contribute:

  1. Fork the repository.
  2. Create a new branch for your feature or bugfix.
  3. Make your changes and add tests if needed.
  4. Open a pull request with a clear description of your changes.

Please follow the existing code style and conventions.

About

Kotlin Timer library provides robust and flexible CountdownTimer and Stopwatch functionalities implemented using Kotlin Coroutines Flow.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages