This guide is the practical reference for using TimerButton in production apps. It keeps the README fast while giving enough detail for Compose, XML, callbacks, lifecycle, and testing.
dependencies {
implementation("com.goeslocal:timerbutton-compose:0.2.0") // Compose
implementation("com.goeslocal:timerbutton-view:0.2.0") // XML/View
// Or use the compatibility bundle when you want both APIs:
implementation("com.goeslocal:timerbutton:0.2.0")
}For local development inside this repository:
dependencies {
implementation(project(":timerbutton-compose"))
implementation(project(":timerbutton-view"))
}TimerButton has two public surfaces:
- Compose:
TimerButton(...)andrememberTimerButtonState(...) - XML/View:
TimerButtonView
They are published separately so Compose-only apps do not pull XML-only code, and XML-only apps do not pull Compose dependencies. The original timerbutton artifact remains as a compatibility bundle that depends on both split artifacts.
Progress is calculated from a monotonic clock:
elapsed = now - startedAt
remaining = duration - elapsed
progress = elapsed / duration
This keeps countdowns accurate even when frames are skipped.
Start on click is the default:
TimerButton(
text = "Retry",
durationMillis = 10_000L,
textFormatter = { state, label ->
if (state.isRunning) {
"Retry in ${(state.remainingMillis + 999) / 1000}s"
} else {
label
}
},
onTimerComplete = {
println("Retry is available")
},
)Use a state object when another event controls the timer:
val timerState = rememberTimerButtonState(durationMillis = 30_000L)
TimerButton(
state = timerState,
text = "Resend OTP",
config = TimerButtonConfig(
durationMillis = 30_000L,
clickStartsTimer = false,
),
textFormatter = { state, label ->
if (state.isRunning || state.isPaused) {
"Resend in ${(state.remainingMillis + 999) / 1000}s"
} else {
label
}
},
)
LaunchedEffect(otpSentSuccessfully) {
if (otpSentSuccessfully) timerState.start()
}TimerButtonState exposes:
| Member | Meaning |
|---|---|
progress |
Float from 0f to 1f. |
remainingMillis |
Remaining duration in milliseconds. |
elapsedMillis |
Elapsed duration in milliseconds. |
timerState |
Idle, Running, Paused, Completed, or Cancelled. |
isRunning, isPaused, isCompleted |
Convenience booleans. |
setDuration(...) |
Change the current duration. |
start() |
Start from zero. |
pause() |
Pause while running. |
resume() |
Resume while paused. |
cancel() |
Stop while keeping current progress. |
reset() |
Return to idle and zero progress. |
restart() |
Start again from zero. |
Operations that do not apply are no-ops. For example, pause() only changes state while running.
TimerButton(
text = "Download report",
durationMillis = 8_000L,
modifier = Modifier
.width(240.dp)
.height(56.dp),
config = TimerButtonConfig(
durationMillis = 8_000L,
progressDirection = TimerProgressDirection.LeftToRight,
progressMode = TimerProgressMode.Overlay,
),
colors = TimerButtonColors(
containerColor = Color(0xFF172033),
contentColor = Color.White,
progressColor = Color(0xFF7DA2FF),
disabledContainerColor = Color(0xFFE5E7EB),
disabledContentColor = Color(0xFF6B7280),
),
shape = RoundedCornerShape(18.dp),
border = BorderStroke(1.dp, Color(0xFF7DA2FF)),
progressAlpha = 0.42f,
)Add TimerButtonView to any XML layout:
<com.goeslocal.timerbutton.TimerButtonView
android:id="@+id/resendButton"
android:layout_width="220dp"
android:layout_height="56dp"
android:text="Resend OTP"
app:timerDuration="30000"
app:timerTextIdle="Resend OTP"
app:timerTextRunning="Resend in %ss"
app:timerTextCompleted="Resend now"
app:timerProgressDirection="leftToRight"
app:timerProgressMode="overlay" />Control it from Kotlin:
binding.resendButton.setTimerListener(
object : TimerButtonListener {
override fun onTimerComplete() {
println("Resend is available")
}
},
)
binding.resendButton.start()| Attribute | Default | Purpose |
|---|---|---|
timerDuration |
10000 |
Total duration in milliseconds. |
timerProgressColor |
teal | Progress fill or underline color. |
timerProgressAlpha |
0.32 |
Alpha applied to progress color. |
timerProgressDirection |
leftToRight |
Fill direction. |
timerProgressMode |
overlay |
Progress drawing mode. |
timerAutoStart |
false |
Start when attached. |
timerClickStartsTimer |
true |
Start/resume from clicks. |
timerAllowClickWhileRunning |
false |
Deliver clicks while running. |
timerTextIdle |
android:text |
Idle/cancelled text. |
timerTextRunning |
android:text |
Running/paused text. |
timerTextCompleted |
android:text |
Completed text. |
timerCornerRadius |
12dp |
Background and progress corner radius. |
timerStrokeColor |
transparent | Optional border color. |
timerStrokeWidth |
0dp |
Optional border width. |
timerButtonBackgroundColor |
purple | Enabled surface. |
timerButtonDisabledColor |
gray | Disabled surface. |
timerTextColor |
current text color | Label color. |
timerTextRunning may contain %s or %d; TimerButton replaces it with remaining seconds rounded up.
Progress directions:
LeftToRightRightToLeftTopToBottomBottomToTop
Progress modes:
Overlay: tinted layer over the button surface.Background: progress-colored surface behind content.Underline: 4dp progress line.
Callbacks:
| Callback | When it fires |
|---|---|
onClick |
A button click is delivered. |
onTimerStart |
A timer starts from idle/cancelled, or after restart. |
onTick |
An animation tick occurs while running. |
onTimerComplete |
A run completes, once per run. |
onTimerCancel |
A running or paused timer is cancelled. |
onTimerPause |
A running timer pauses. |
onTimerResume |
A paused timer resumes. |
onTimerReset |
State returns to idle. |
onTimerRestart |
State starts again from zero. |
onStateChange |
Public timer state changes. |
Compose timers run inside LaunchedEffect and stop when the composable leaves composition. State is saved with rememberSaveable, so normal Activity recreation restores timer progress.
TimerButtonView removes animation callbacks when detached. Call release() in onDestroyView() when listeners capture a Fragment binding or when you retain view references manually.
TimerButton is a UI component. For important rules such as OTP cooldowns, billing windows, auth lockouts, or server-enforced retry limits, store the authoritative timestamp in your ViewModel, repository, or backend. Use TimerButton to render the visible countdown and handle local interaction.
Run:
./gradlew checkFocused library tests:
./gradlew :timerbutton-core:testDebugUnitTestThe timer engine has fake-clock coverage for start, completion, one-shot completion delivery, cancel, pause/resume, reset, restart, clamping, restore after Activity recreation, and independent timer instances.