Por qué el tiempo es un dolor (y cómo Kotlin lo cura)
\n\nSi alguna vez ha escrito una aplicación que atiende a usuarios de diferentes husos horarios, conoce este dolor. La solicitud llega con una marca de tiempo, en la base de datos está UTC, el usuario en Nueva York se queja de que la notificación llegó a las 3 de la madrugada, pero en la base de datos son las 8 a.m. ¿Le suena familiar?
\n\nTrabajar con zonas horarias es esa área de la programación donde la aparente simplicidad se convierte en decenas de errores. Pero Kotlin, que heredó las mejores prácticas de Java, nos proporciona un potente arsenal para combatir el caos. En este artículo, analizaremos cómo trabajar correctamente con zonas horarias en Kotlin, evitar errores típicos y escribir código que no se rompa durante el cambio al horario de verano.
\n\nBreve introducción: ¿qué es una zona horaria?
\n\nAntes de sumergirnos en el código, acordemos los términos. Una zona horaria (time zone) no es solo un desplazamiento respecto a UTC. También es un conjunto de reglas para el cambio al horario de verano/invierno (DST). Por ejemplo, Europe/Moscow no es solo +3 horas, sino también cambios históricos (en 2014, Rusia dejó de cambiar la hora).
En el mundo Java/Kotlin, dos conceptos clave se encargan de esto:
\n\n- \n
- ZoneId — identificador de zona horaria (por ejemplo, "America/New_York"). Contiene todo el historial de DST. \n
- ZoneOffset — solo el desplazamiento (por ejemplo, +05:00). Sin reglas de cambio. \n
La diferencia es crítica: si usa ZoneOffset en lugar de ZoneId, su código se romperá el día del cambio de hora. Recuérdelo.
Clases básicas de java.time en Kotlin
\n\nKotlin es totalmente compatible con el paquete java.time (Java 8+). Si programa para Android, no olvide agregar desugaring o use ThreeTenABP. Para Kotlin de servidor no hay problemas.
Instant — momento en el tiempo
\n\nInstant es un momento absoluto en la línea de tiempo. No está vinculado a un huso horario. Es precisamente lo que debería almacenar en la base de datos.
import java.time.Instant\n\nfun main() {\n val now = Instant.now()\n println(now) // 2025-03-15T10:30:00.123Z\n}\n\nObserve la letra Z al final: es la hora Zulu, es decir, UTC. Almacene todo en UTC — esa es la regla de oro.
ZonedDateTime — hora con zona
\n\nCuando necesite mostrar la hora al usuario, use ZonedDateTime. Almacena fecha, hora, desplazamiento e identificador de zona.
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\n¿Ve los corchetes? Eso es precisamente ZoneId, que permite al sistema manejar correctamente el DST.
OffsetDateTime — hora con desplazamiento
\n\nSi no necesita información de zona, solo el desplazamiento, use OffsetDateTime. Es más ligero y 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\nPero recuerde: si guarda un OffsetDateTime para Nueva York y luego EE. UU. cambia al horario de verano, su desplazamiento quedará obsoleto.
Conversión entre zonas horarias en Kotlin
\n\nLa tarea más común es tomar la hora de la base de datos (UTC) y mostrarla en el huso horario del usuario. Analicemos cómo hacerlo correctamente.
\n\nDe UTC a zona 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 =