Por que o tempo é uma dor (e como Kotlin a trata)
\n\nSe você já escreveu um aplicativo que atende usuários de diferentes fusos horários, conhece essa dor. A requisição chega com um timestamp, no banco está UTC, o usuário em Nova York reclama que a notificação chegou às 3 da manhã, mas no banco são 8 da manhã. Familiar?
\n\nTrabalhar com fusos horários é aquela área da programação onde a aparente simplicidade se transforma em dezenas de bugs. Mas Kotlin, herdando as melhores práticas do Java, nos dá um arsenal poderoso para combater o caos. Neste artigo, vamos abordar como trabalhar corretamente com fusos horários em Kotlin, evitar erros típicos e escrever código que não quebre na mudança para o horário de verão.
\n\nBreve introdução: o que é um fuso horário?
\n\nAntes de mergulhar no código, vamos alinhar os termos. Um fuso horário (time zone) não é apenas um deslocamento em relação ao UTC. É também um conjunto de regras de transição para horário de verão/inverno (DST). Por exemplo, Europe/Moscow não é apenas +3 horas, mas também mudanças históricas (em 2014, a Rússia parou de ajustar os relógios).
No mundo Java/Kotlin, dois conceitos-chave são responsáveis por isso:
\n\n- \n
- ZoneId — identificador do fuso horário (ex.: "America/New_York"). Contém todo o histórico de DST. \n
- ZoneOffset — apenas o deslocamento (ex.: +05:00). Sem regras de transição. \n
A diferença é crítica: se você usar ZoneOffset em vez de ZoneId, seu código quebrará no dia da mudança de horário. Lembre-se disso.
Classes básicas do java.time em Kotlin
\n\nKotlin suporta totalmente o pacote java.time (Java 8+). Se você está escrevendo para Android, não esqueça de adicionar desugaring ou use ThreeTenABP. Para Kotlin no servidor, não há problemas.
Instant — momento no tempo
\n\nInstant é um momento absoluto na linha do tempo. Não está vinculado a um fuso horário. É exatamente isso que você deve armazenar no banco de dados.
import java.time.Instant\n\nfun main() {\n val now = Instant.now()\n println(now) // 2025-03-15T10:30:00.123Z\n}\n\nObserve a letra Z no final — é o horário Zulu, ou seja, UTC. Armazene tudo em UTC — essa é a regra de ouro.
ZonedDateTime — hora com fuso horário
\n\nQuando você precisa mostrar a hora para o usuário, use ZonedDateTime. Ele armazena data, hora, deslocamento e o identificador do fuso.
import java.time.ZonedDateTime\nimport java.time.ZoneId\n\nfun main() {\n val moscowTime = ZonedDateTime.now(ZoneId.of("Europe/Moscow"))\n println(moscowTime) // 2025-03-15T13:30:00.123+03:00[Europe/Moscow]\n}\n\nViu os colchetes? Isso é o ZoneId, que permite ao sistema processar corretamente o DST.
OffsetDateTime — hora com deslocamento
\n\nSe você não precisa da informação do fuso, mas apenas do deslocamento, use OffsetDateTime. Ele é mais leve e rápido.
import java.time.OffsetDateTime\nimport java.time.ZoneOffset\n\nfun main() {\n val offsetTime = OffsetDateTime.now(ZoneOffset.ofHours(5))\n println(offsetTime) // 2025-03-15T15:30:00.123+05:00\n}\n\nMas lembre-se: se você salvar um OffsetDateTime para Nova York e depois os EUA entrarem no horário de verão, seu deslocamento ficará desatualizado.
Conversão entre fusos horários em Kotlin
\n\nA tarefa mais comum é pegar a hora do banco (UTC) e mostrá-la no fuso horário do usuário. Vamos ver como fazer isso corretamente.
\n\nDe UTC para o fuso local
\n\nimport java.time.Instant\nimport java.time.ZoneId\nimport java.time.ZonedDateTime\n\nfun convertUtcToLocal(utcInstant: Instant, targetZone: String): ZonedDateTime {\n return utcInstant.atZone(ZoneId.of(targetZone))\n}\n\nfun main() {\n val utcNow =