Resposta rápida: use kotlinx-datetime para modelar tempo de forma tipada em Kotlin (Android, backend e KMP). Guarde eventos absolutos como Instant (UTC), derive calendário com TimeZone e toLocalDateTime(), e use LocalDate / LocalDateTime / LocalTime só quando o valor for “civil” (aniversário, horário de funcionamento, data de vencimento sem instante único). Prefira Clock.System.now() injetável nos testes em vez de Clock.System espalhado. Não confunda java.util.Date, Calendar e strings dd/MM/yyyy com um modelo de domínio.
Datas quebram produtos silenciosamente: um agendamento “às 09:00” vira 12:00 para quem está em outro fuso, um relatório diário corta o dia errado no horário de verão, e um teste flaky passa só no laptop do autor. O ecossistema Kotlin já aponta kotlinx-datetime em tutoriais de Kotlin Multiplatform, glossário de expect/actual e guias KMP — mas faltava um guia prático dono do tópico. Este artigo fecha esse buraco com decisões, código e checklist de produção.
Por que kotlinx-datetime e não java.time puro?
No JVM você pode usar java.time (Instant, ZonedDateTime, LocalDate). Em KMP, iOS e common code, java.time não está disponível da mesma forma. kotlinx-datetime oferece um modelo compartilhado:
| Tipo | Significado | Quando usar |
|---|---|---|
Instant | ponto no tempo (UTC) | eventos, logs, createdAt, expiração de token |
LocalDate | ano-mês-dia civil | aniversário, data de fatura, feriado |
LocalTime | hora civil sem data | horário de abertura da loja |
LocalDateTime | data + hora civil sem fuso | “reunião em 17/08 às 14:00” nessa região |
TimeZone | regras de fuso / offset | converter Instant ↔ calendário |
DateTimePeriod / DatePeriod | duração calendárica | “daqui a 1 mês”, “+2 dias úteis” (com cuidado) |
Clock | fonte de “agora” | produção e testes |
A biblioteca não substitui serialização JSON sozinha: combine com kotlinx.serialization e contratos explícitos de ISO-8601. Em backends Spring/Ktor no JVM, muitos times ainda usam java.time na borda HTTP e convertem para o domínio — o importante é uma representação canônica por conceito.
Dependência e imports
No Gradle (Version Catalog recomendado):
// libs.versions.toml
[versions]
kotlinx-datetime = "0.6.2" // confira a estável atual no repositório oficial
[libraries]
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
dependencies {
implementation(libs.kotlinx.datetime)
}
Em commonMain de um projeto KMP:
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.kotlinx.datetime)
}
}
}
Imports típicos:
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
Não fixe versões “mágicas” em snippets de blog: alinhe ao catálogo do monorepo e atualize por PR, como no guia de Gradle com Kotlin.
Instant: o tipo padrão para eventos
Quase todo evento de sistema deve nascer como Instant:
import kotlin.time.Duration.Companion.minutes
val agora: Instant = Clock.System.now()
val expiraEm: Instant = agora + 15.minutes
Para intervalos absolutos, prefira kotlin.time.Duration com Instant (operadores da biblioteca) em vez de somar dias civis “no escuro”. Quando o negócio fala em “daqui a 30 dias de calendário”, use DatePeriod / DateTimePeriod depois de escolher o fuso.
Exemplo de domínio limpo:
data class Assinatura(
val id: String,
val criadaEm: Instant,
val renovavelAte: Instant,
)
fun Assinatura.estaAtiva(agora: Instant = Clock.System.now()): Boolean =
agora < renovavelAte
Injete Clock (ou () -> Instant) em ViewModels, services e use cases. Isso evita testes que dependem do relógio da máquina e facilita simular “amanhã” em cenários de WorkManager e offline-first.
LocalDate e calendário civil
Use LocalDate quando o fuso não faz parte do significado:
val aniversario = LocalDate(1990, 5, 17)
val hojeEmSp = Clock.System.now()
.toLocalDateTime(TimeZone.of("America/Sao_Paulo"))
.date
fun fazAniversarioHoje(aniversario: LocalDate, hoje: LocalDate): Boolean =
aniversario.month == hoje.month && aniversario.dayOfMonth == hoje.dayOfMonth
Erros clássicos:
- Guardar aniversário como
Instantà meia-noite UTC — o dia “vira” conforme o fuso do cliente. - Comparar strings
17/05/1990sem parse tipado. - Usar
LocalDateTimequando só a data importa.
Para faturas e fechamento de cartão no Brasil, combine LocalDate com a regra de negócio (próximo dia útil, etc.) em código explícito — não esconda isso em formatação.
Fuso horário: America/Sao_Paulo e amigos
Conversão correta:
val zonaSp = TimeZone.of("America/Sao_Paulo")
val instante = Instant.parse("2026-08-17T15:00:00Z")
val civilEmSp: LocalDateTime = instante.toLocalDateTime(zonaSp)
val deVolta: Instant = civilEmSp.toInstant(zonaSp)
Regras práticas para times brasileiros:
- Armazene Instant (UTC) no banco e na API. Exiba no fuso do usuário ou no fuso do negócio (
America/Sao_Paulo) na UI. - Não use
TimeZone.currentSystemDefault()no servidor para regras de negócio: o pod pode estar em UTC. Fixe o fuso do produto. - IDs IANA (
America/Sao_Paulo,America/Manaus) são mais seguros que offsets fixos (-03:00) quando há histórico de horário de verão ou mudanças legais. - Em apps mobile, o fuso do aparelho importa para UX; o contrato da API deve documentar se o campo é Instant ISO-8601 (
…Z) ou data civil.
Se você monta APIs Ktor, alinhe serialização e contratos com o que já faz em cliente resiliente e testes com testApplication: um campo createdAt ambíguo é bug de produto, não “detalhe de front”.
Parsing, formatação e ISO-8601
Prefira ISO-8601 na borda:
val instant = Instant.parse("2026-08-17T18:30:00Z")
val data = LocalDate.parse("2026-08-17")
Para exibir em PT-BR na UI Android, muitos times formatam a partir de java.time no androidMain ou usam formatadores da plataforma após converter. Em common code, mantenha o valor tipado e deixe a formatação na camada de apresentação. Evite guardar String formatada no banco.
Checklist de contrato HTTP:
- Campos absolutos:
2026-08-17T18:30:00Z(Instant). - Campos civis:
2026-08-17(LocalDate) — documente o significado. - Nunca misture
18/08/2026 15:30sem fuso em JSON de integração. - Rejeite payloads ambíguos cedo (400) com mensagem clara.
Persistência: Room, SQLDelight e SQL
Em Android com Room, tipos customizados convertem Instant ↔ Long (epoch millis) ou String ISO. Em KMP com SQLDelight ou Room KMP, defina adapters explícitos:
// ideia: Instant ↔ Long (epoch milliseconds)
fun Instant.asEpochMillis(): Long = toEpochMilliseconds()
fun Long.toKotlinInstant(): Instant = Instant.fromEpochMilliseconds(this)
Cuidados:
- Migrations de coluna
TEXT“dd/MM/yyyy” para Instant exigem script e validação — veja o espírito do guia de migrations Room. - Índices em epoch facilitam ranges (
WHERE created_at BETWEEN ? AND ?). - Relatórios “por dia” no fuso de São Paulo devem converter Instant →
LocalDatena query ou na aplicação com o fuso correto; agrupar só por UTC meia-noite distorce o dia comercial brasileiro.
Arquiteturas offline-first sincronizam melhor quando o relógio canônico é Instant e o conflito é resolvido com updatedAt absoluto.
Testes determinísticos com Clock
import kotlin.time.Duration.Companion.days
class FakeClock(private var agora: Instant) : Clock {
override fun now(): Instant = agora
fun avancar(duration: kotlin.time.Duration) {
agora = agora + duration
}
}
@Test
fun renovacaoExpira() {
val clock = FakeClock(Instant.parse("2026-08-17T12:00:00Z"))
val assinatura = Assinatura(
id = "1",
criadaEm = clock.now(),
renovavelAte = clock.now() + 1.days,
)
clock.avancar(2.days)
assertFalse(assinatura.estaAtiva(clock.now()))
}
Combine com a pirâmide de testes do site (Turbine/Flow, Testcontainers, Kotest): propriedades de calendário (“todo Instant tem LocalDate em SP”) são ótimas candidatas a property-based testing.
KMP: common code sem expect/actual para datas
Para a maioria dos casos, kotlinx-datetime elimina expect/actual de parsing e aritmética. Reserve expect/actual para integração com APIs nativas de calendário/UI. O glossário de expect/actual já recomenda avaliar bibliotecas multiplataforma antes de duplicar implementação — datas são o exemplo canônico.
Quem busca carreira KMP encontra o encaixe em desenvolvedor Kotlin Multiplatform e no guia KMP mobile: domínio compartilhado com Instant/LocalDate é um dos ganhos mais tangíveis em entrevistas.
Erros comuns (e como evitar)
- Somar 24 horas para “amanhã” em vez de
DatePeriod(days = 1)no fuso certo — quebra em mudanças de offset. - Persistir
LocalDateTimesem fuso para eventos online — dois usuários veem horários incoerentes. - Usar
Date()/Calendarlegado em código novo Android — migre na borda e isole. - Confiar no fuso do servidor em Kubernetes — configure
America/Sao_Paulosó onde for regra de negócio; logs em UTC. - Comparar strings formatadas em testes de UI — compare
LocalDate/Instant. - Esquecer monotonicidade vs wall clock — para timeouts use
TimeSource/ medições; para negócio useClockde wall time documentado. - Serializar Instant como epoch em um serviço e ISO em outro sem gateway — padronize o contrato.
Quando NÃO usar kotlinx-datetime
- Código JVM-only legado já saturado de
java.timecom adapters estáveis: introduza kotlinx-datetime só se houver plano KMP ou modelo de domínio compartilhado. - Cálculos astronômicos ou calendários litúrgicos especializados: use bibliotecas de domínio.
- Cron expressions de infraestrutura (K8s, GitHub Actions): mantenha no formato da ferramenta.
Checklist de adoção em 2026
- Inventariar campos de data no domínio: classificar Instant vs LocalDate vs LocalDateTime.
- Adicionar dependência no catálogo; proibir
java.util.Dateem código novo (Detekt/ktlint se possível). - Definir fuso canônico do produto (
America/Sao_Paulopara regras BR) e UTC no armazenamento. - Injetar
Clocknas camadas de aplicação. - Documentar o contrato JSON (ISO-8601).
- Cobrir conversões críticas com testes de fuso (SP, Manaus, UTC).
- Revisar jobs agendados (WorkManager, cron Ktor/Spring) para não misturar civil e absoluto.
Perguntas frequentes
kotlinx-datetime substitui java.time no Android?
No androidMain/jvmMain você pode continuar usando java.time na UI ou em bibliotecas JVM. Em commonMain e em estratégias KMP, kotlinx-datetime é a escolha natural. Muitos apps convertem na fronteira do módulo shared.
Como representar “todo dia às 09:00 em São Paulo”?
Modele a regra civil (LocalTime(9, 0) + TimeZone.of("America/Sao_Paulo")) e materialize o próximo Instant quando for agendar a execução. Não grave só um Instant único se a regra for recorrente.
Posso usar kotlinx-datetime com Exposed ou Spring Data?
Sim, com converters. No ecossistema Exposed (Exposed 1.0 / R2DBC) mapeie colunas timestamp/timestamptz para Instant. Em Spring, java.time ainda é o default — converta na borda do domínio.
Qual a diferença entre Duration e DatePeriod?
Duration (kotlin.time) é tempo físico (segundos/nanos). DatePeriod / DateTimePeriod é calendário (dias/meses/anos) e depende do contexto civil. “Mais 30 dias” de fatura ≠ “mais 30 × 24 horas”.
kotlinx-datetime serve para iOS via KMP?
Sim. É uma das bibliotecas citadas em Kotlin para iOS junto com Ktor, SQLDelight e serialization. Valide formatação e calendário nativo na camada iOS quando a UX exigir NSCalendar.
Conclusão e próximos passos
kotlinx-datetime deixa explícito o que muitos apps escondem em String e Long mágicos: se o valor é um instante absoluto ou uma data civil, e em qual fuso a regra de negócio vive. Para times brasileiros, a combinação Instant no armazenamento + America/Sao_Paulo na regra civil + Clock injetável elimina a maior parte dos bugs de “funcionou no meu emulador”.
Continue no cluster: tutorial KMP, estrutura de projetos KMP, coroutines, serialization e vagas Kotlin se o próximo passo for aplicar isso em produção. Se a dúvida for carreira multiplataforma, o guia de desenvolvedor KMP fecha o caminho do shared domain até o portfólio.