Skip to content

Kotlin Time (Duration) & Flow

Modifier.pointerInput(Unit) {
detectTapGestures(onDoubleTap = {
togglePlay()
}, onPress = {
mediaPlayer?.let {
try {
withTimeout(500) {
tryAwaitRelease()
}
} catch (e: TimeoutCancellationException) {
it.playbackParams = PlaybackParams().apply {
speed = 2f
}
tryAwaitRelease()
it.playbackParams = PlaybackParams().apply {
speed = 1f
}
updateState()
} }
})
}

❌ Avoid — Manual math + CountDownTimer

Section titled “❌ Avoid — Manual math + CountDownTimer”
val day = 60 * 60 * 24
val hour = 60 * 60
val days = countdown / day
val hours = (countdown % day) / hour
val minutes = ((countdown % day) % hour) / 60
val seconds = ((countdown % day) % hour) % 60
val duration = (targetTime - now).seconds
duration.toComponents { days, hours, minutes, seconds, _ ->
// each unit is already broken down for you
}

// instead of LocalDateTime.now().atZone(...).toEpochSecond()
val now = Instant.now().epochSecond

Duration.between — diff between two times

Section titled “Duration.between — diff between two times”
val target = LocalDateTime.parse("2026-05-26 23:59:59", pattern)
val duration = Duration.between(LocalDateTime.now(), target)
duration.toKotlinDuration().toComponents { days, hours, minutes, seconds, _ -> }
val days = LocalDate.now().until(targetDate, ChronoUnit.DAYS)
val hours = LocalDateTime.now().until(targetDateTime, ChronoUnit.HOURS)

Blocks the thread, dangerous on the main thread.

runBlocking {
while (true) { delay(1000) }
}

Non-blocking, respects cancellation.

CoroutineScope(Dispatchers.Main).launch {
while (isActive) {
// ...
delay(1000)
}
}

✅ Better — viewModelScope / lifecycleScope

Section titled “✅ Better — viewModelScope / lifecycleScope”

Auto-cancels when the lifecycle ends, no manual cleanup needed.

viewModelScope.launch {
while (isActive) {
// ...
delay(1000)
}
}

Separates ticking logic from UI, easy to test and reuse.

flow {
while (true) {
emit(Duration.between(LocalDateTime.now(), target).toKotlinDuration())
delay(1000)
}
}
.takeWhile { it.isPositive() } // auto-stops at zero
.collect { duration ->
duration.toComponents { days, hours, minutes, seconds, _ ->
// update UI
}
}
OperatorPurpose
emit()push a value downstream
collect {}consume values
takeWhile {}auto-complete when condition is false
map {}transform each value

val pattern = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
val target = LocalDateTime.parse("2026-05-26 23:59:59", pattern)
val views = RemoteViews(context.packageName, R.layout.countdown_widget)
flow {
while (true) {
emit(Duration.between(LocalDateTime.now(), target).toKotlinDuration())
delay(1000)
}
}
.takeWhile { it.isPositive() }
.collect { duration ->
duration.toComponents { days, hours, minutes, seconds, _ ->
mapOf(
R.id.days to days,
R.id.hours to hours,
R.id.minutes to minutes,
R.id.seconds to seconds,
).forEach { (id, value) ->
views.setTextViewText(id, value.toString())
}
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}

NeedUse
Current epoch secondsInstant.now().epochSecond
Diff between two timesDuration.between(a, b)
Break duration into units.toKotlinDuration().toComponents {}
Tick every secondflow { while(true) { emit(...); delay(1000) } }
Auto-stop at zero.takeWhile { it.isPositive() }
Safe coroutine scopeviewModelScope or lifecycleScope