1. Зачем нам «текущее время» и почему это не строка
Когда вы впервые хотите добавить в программу время, обычно мысль такая: «ну я выведу текст “12:34” и всё». Но как только появляются вопросы «что было раньше», «сколько прошло», «отсортировать по времени», «показать последние 10 событий», строка перестаёт быть другом и становится тем самым коллегой, который “просто подержит провод”, а потом исчезает вместе с проводом.
В этой лекции мы будем мыслить так: время — это данные, которые можно сравнивать, сортировать и вычитать, а не украшение в консоли. Для таких задач kotlinx.datetime даёт нам Instant (момент на шкале времени), а стандартная библиотека Kotlin — kotlin.time.Duration (длительность между моментами).
Представьте линию времени как прямую:
flowchart LR A["Instant A"] -->|прошло| D["Duration"] --> B["Instant B"]
Instant — это “точка”, Duration — это “расстояние между точками”.
2. Clock.System.now(): получаем текущий Instant
Есть очень человеческое желание: “мне нужно текущее время” → “значит, я просто везде вставлю Clock.System.now()”. Это примерно как в кулинарии: “нужно соли” → “значит, я буду солить на каждом шаге”. В итоге вы либо пересолите, либо получите непредсказуемый вкус.
Правильная привычка такая: если “текущее время” нужно как опорная точка, то мы берём его один раз и кладём в val. Это делает поведение стабильнее и код — понятнее: видно, какое именно “сейчас” вы имели в виду.
Самый простой сценарий: «зафиксировать сейчас»
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
fun main() {
val now: Instant = Clock.System.now()
println(now) // пример: 2026-01-14T18:22:10.123Z
}
Вывод обычно будет в ISO-представлении. На этом этапе нам важно не то, как оно выглядит, а то, что это тип Instant, который можно сравнивать и вычитать.
Почему now() два раза — это два разных значения
import kotlinx.datetime.Clock
fun main() {
val a = Clock.System.now()
val b = Clock.System.now()
println(a == b) // скорее всего: false
println(b > a) // скорее всего: true
}
Между двумя вызовами прошла хотя бы микроскопическая доля времени. Иногда настолько маленькая, что вы начинаете сомневаться в реальности, но процессор — не сомневается.
Хороший стиль: «берём now один раз и передаём дальше»
Допустим, мы делаем запись события. Лучше сделать так:
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
fun createTimestamp(): Instant {
return Clock.System.now()
}
fun main() {
val createdAt = createTimestamp()
println(createdAt) // пример: 2026-01-14T18:22:10.456Z
}
Да, это кажется “слишком просто”, но как только код разрастётся, вы будете благодарны себе за то, что время берётся в одном месте, а не размазано по всей программе как джем по клавиатуре.
3. Сравнение Instant: раньше/позже и проверки
Сравнения — это момент, где начинаются настоящие преимущества Instant. Когда у вас есть тип “момент времени”, сравнения >, <, >=, <= становятся осмысленными: это сравнение точек на временной шкале, а не сравнение букв в строке.
В этом разделе мы научимся делать проверки “раньше/позже”, писать читаемые условия и аккуратно относиться к идее “время может идти назад” (например, из-за корректировки системных часов). Последнее мы глубоко разбирать не будем, но знать, что такое бывает, полезно — хотя бы чтобы потом не удивляться странным отрицательным длительностям.
Проверка: «событие произошло после старта программы»
import kotlinx.datetime.Clock
fun main() {
val startedAt = Clock.System.now()
// ... тут мог быть какой-то код ...
val eventAt = Clock.System.now()
if (eventAt >= startedAt) {
println("Событие произошло после старта") // скорее всего так и будет
}
}
Тут важный смысловой момент: мы сравниваем моменты, а не локальное время на часах.
Проверка таймаута: «прошло больше N секунд?»
Пока мы не вводим никаких сложных таймеров, но сам паттерн очень частый: вы хотите понять, что прошло “достаточно времени”, и можно что-то делать (или наоборот — пора остановиться).
import kotlinx.datetime.Clock
import kotlin.time.Duration.Companion.seconds
fun main() {
val startedAt = Clock.System.now()
val now = Clock.System.now()
val elapsed = now - startedAt
println(elapsed > 2.seconds) // чаще всего: false
}
Здесь появляется Duration. Пока достаточно понимать: разница двух Instant — это длительность.
Нюанс: сравниваем одно и то же «сейчас»
Иногда новички пишут условие в таком стиле (и да, я тоже так делал, когда был молод и компилировался быстрее):
import kotlinx.datetime.Clock
import kotlin.time.Duration.Companion.seconds
fun main() {
val startedAt = Clock.System.now()
// Плохая идея: now берётся каждый раз заново
if (Clock.System.now() - startedAt > 2.seconds) {
println("Прошло больше 2 секунд")
}
}
Формально это работает. Практически это ухудшает читаемость: вы не видите, где “фиксируется” момент для проверки. Лучше делать val now = ... и использовать его дальше.
4. Разница двух Instant как Duration
Когда люди впервые слышат слово Duration, они часто думают “о, это как дата, только поменьше”. Нет. Длительность — это не “дата маленького размера”, а промежуток времени: 5 секунд, 120 миллисекунд, 3 минуты. Длительность удобно использовать для измерений “сколько заняло”, “сколько прошло”, “на сколько опоздали”.
В этом разделе мы научимся получать Duration как разницу двух Instant, доставать из неё числа (секунды/миллисекунды) и не путать длительность с календарной арифметикой. Календарь — это отдельный зверь, и мы его сегодня не трогаем.
Базовый пример: end - start
import kotlinx.datetime.Clock
import kotlin.time.Duration
fun main() {
val start = Clock.System.now()
// допустим, тут был какой-то код
val end = Clock.System.now()
val elapsed: Duration = end - start
println(elapsed) // пример: 12ms
}
Важно: elapsed — не число. Это объект-длительность, который можно переводить в нужные единицы.
Получаем миллисекунды, секунды и наносекунды
import kotlinx.datetime.Clock
fun main() {
val start = Clock.System.now()
val end = Clock.System.now()
val elapsed = end - start
println(elapsed.inWholeMilliseconds) // пример: 0
println(elapsed.inWholeNanoseconds) // пример: 50000 (числа условные)
}
Да, “0 миллисекунд” — нормальный результат: между двумя вызовами могло пройти меньше миллисекунды.
Отрицательная длительность: это не баг, это жизнь
Иногда (редко, но бывает) можно получить ситуацию, где “конец раньше начала” — например, если системные часы сдвинулись. На уровне базового курса достаточно помнить: Duration может быть отрицательной, и если вам важны гарантии, нужно проектировать логику аккуратно (но это уже тема архитектуры измерений времени, а не сегодняшняя механика).
import kotlin.time.Duration.Companion.seconds
fun main() {
val d = (-5).seconds
println(d) // -5s
}
5. События со временем: сортировка и мини‑пример
Как только у вас появляется список событий с метками времени, почти неизбежно хочется: “покажи последние”, “отсортируй от нового к старому”, “найди самое раннее”, “найди самое позднее”. И вот тут Instant внезапно превращается в идеального кандидата для сортировки, потому что он сравним: Instant имеет естественный порядок “раньше → позже”.
В этом разделе мы применим привычные функции сортировки списков, но уже к моментам времени. Мы будем использовать sorted(), sortedDescending(), sortedBy { ... } и друзей. Эти функции в стандартной библиотеке строят новый список и не мутируют исходный, что делает код предсказуемее.
Сортируем список Instant
import kotlinx.datetime.Instant
fun main() {
val moments = listOf(
Instant.parse("2026-01-06T10:00:01Z"),
Instant.parse("2026-01-06T10:00:00Z"),
Instant.parse("2026-01-06T09:59:59Z")
)
val sorted = moments.sorted()
println(sorted)
}
Мы здесь используем Instant.parse(...) чисто как способ быстро получить тестовые моменты. В реальном приложении чаще будут живые Clock.System.now().
Сортировка «сначала новые»
import kotlinx.datetime.Instant
fun main() {
val moments = listOf(
Instant.parse("2026-01-06T10:00:01Z"),
Instant.parse("2026-01-06T10:00:00Z")
)
println(moments.sortedDescending()) // сначала более поздний момент
}
Смысл: если Instant задаёт момент, то “больше” означает “позже”.
Сортируем объекты по полю createdAt
Теперь сделаем шаг ближе к приложению. Допустим, у нас есть записи (например, расходы), и у каждой есть время создания.
import kotlinx.datetime.Instant
data class Expense(
val title: String,
val amount: Int,
val createdAt: Instant
)
Тогда сортировка будет такой:
import kotlinx.datetime.Instant
data class Expense(val title: String, val amount: Int, val createdAt: Instant)
fun main() {
val xs = listOf(
Expense("Кофе", 250, Instant.parse("2026-01-06T10:00:00Z")),
Expense("Пицца", 900, Instant.parse("2026-01-06T09:00:00Z"))
)
val sorted = xs.sortedBy { it.createdAt }
println(sorted.map { it.title }) // [Пицца, Кофе]
}
Функции вроде sortedBy() — это стандартный стиль: мы сортируем список по ключу.
Мини-практический пример: добавляем время в трекер расходов
Сейчас будет важный момент “зачем это всё”. В учебных примерах легко получить ощущение, что Instant — это просто новый тип, который печатается красивой строкой. Но настоящая сила начинается, когда вы добавляете время в состояние приложения и начинаете строить логику: сортировки, «последний расход был N минут назад», «покажи последние 5».
В этом мини-примере мы возьмём простую модель “расход” и добавим к ней поле createdAt: Instant, которое заполняется через Clock.System.now(). Это не требует новых архитектурных трюков: просто ещё одно поле, но сразу открывает много возможностей.
Функция создания расхода «прямо сейчас»
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
data class Expense(val title: String, val amount: Int, val createdAt: Instant)
fun createExpenseNow(title: String, amount: Int): Expense {
val now = Clock.System.now()
return Expense(title = title, amount = amount, createdAt = now)
}
Обратите внимание на стиль: now берём один раз, чтобы “момент создания” был ровно один, а не “немного меняющийся пока конструктор думал”.
Добавляем расход в список и печатаем
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
data class Expense(val title: String, val amount: Int, val createdAt: Instant)
fun main() {
val expenses = mutableListOf<Expense>()
expenses.add(Expense("Кофе", 250, Clock.System.now()))
expenses.add(Expense("Метро", 60, Clock.System.now()))
println(expenses.size) // 2
}
Мы используем MutableList, потому что добавляем элементы. Сам факт, что коллекция мутируемая, не требует var: менять можно содержимое даже у val, потому что val защищает ссылку, а не внутренности объекта.
«Последние расходы»: сортировка по времени и take(n)
Мы не углубляемся в “топ-N” как отдельную тему, но сам приём очень естественный: сначала сортируем по времени, потом берём несколько первых.
import kotlinx.datetime.Instant
data class Expense(val title: String, val amount: Int, val createdAt: Instant)
fun latest(expenses: List<Expense>, n: Int): List<Expense> {
return expenses
.sortedByDescending { it.createdAt }
.take(n)
}
Сортировка здесь возвращает новый список и не меняет исходный, что снижает количество “неожиданных сюрпризов” при чтении кода.
«Сколько времени прошло с последнего расхода?»
Вот здесь Duration показывает себя максимально практично.
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlin.time.Duration
data class Expense(val title: String, val amount: Int, val createdAt: Instant)
fun timeSinceLast(expenses: List<Expense>): Duration? {
val last = expenses.maxByOrNull { it.createdAt } ?: return null
return Clock.System.now() - last.createdAt
}
Контракт функции простой: если расходов нет — возвращаем null, иначе возвращаем длительность от последнего события до текущего момента.
И пример использования:
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
data class Expense(val title: String, val amount: Int, val createdAt: Instant)
fun main() {
val xs = listOf(
Expense("Кофе", 250, Clock.System.now())
)
val d = Clock.System.now() - xs[0].createdAt
println(d.inWholeSeconds) // скорее всего: 0
}
6. Типичные ошибки
Ошибка №1: вызывать Clock.System.now() много раз и думать, что это “одно и то же сейчас”.
Если вы берёте now() в нескольких местах подряд, вы получаете несколько разных моментов времени. Иногда разница микроскопическая, но логически это уже “разные часы”. Хороший стиль — взять val now = Clock.System.now() один раз в рамках операции и использовать его дальше.
Ошибка №2: путать Duration и “календарные” сдвиги.
Duration — это “сколько прошло”, а не “прибавить один календарный день”. Даже если длительность равна 24 часам, это ещё не означает “следующая дата” в смысле календаря, потому что календарные правила живут отдельно от длительностей. В задачах измерений держитесь Instant + Duration, а календарные штуки не пытайтесь имитировать длительностями.
Ошибка №3: сортировать события по строковому времени вместо Instant.
Как только вы храните время как строку и делаете sorted() по строкам, вы начинаете надеяться на формат и дисциплину данных. Это слишком хрупко. С Instant сортировка работает по смыслу и использует естественный порядок времени.
Ошибка №4: “случайно мутировать” исходный список при сортировке.
В Kotlin есть сортировки, которые возвращают новый список (sorted*), и есть сортировки “на месте” для MutableList. На старте проще и безопаснее держаться стиля “получили новый список”, потому что так меньше неожиданных эффектов: исходные данные остаются как были, а отсортированная версия — отдельный результат.
Ошибка №5: пытаться измерять время через String/split/“ручной парсинг”.
Если вы ловите себя на мысли “я сейчас распарсю часы и минуты из строки и вычту”, остановитесь. Это почти гарантированно приведёт к ошибкам на границах (минуты переходят через 60, дни — через месяц и так далее). Для измерений используйте разницу Instant, которая сразу даёт Duration.
Ошибка №6: считать, что Duration — это число, и терять единицы измерения.
Duration хранит смысл “длительность” и позволяет безопасно переводить её в нужные единицы (inWholeMilliseconds, inWholeSeconds). Если сразу преобразовать в Long и забыть, это секунды или миллисекунды, то через неделю вы получите баг “в 1000 раз медленнее/быстрее” — классика жанра, почти как забытый break в when в языках, где он вообще бывает.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ