En este tutorial verás el uso de propiedades lazy en Kotlin, con el fin de posponer la lógica de los accesores de una propiedad.
Propiedades Lazy
Una propiedad lazy o perezosa, es aquella que su valor es computado por delegación, a través de la función lazy
. Esto hará que su accesor get()
otorgue el mismo valor luego de la primera ejecución.
La función lambda que recibe lazy
para la lógica de get()
, será materializado solo cuando sea necesitado, posponiendo la lógica de inicialización al momento en que crees una instancia de su clase contenedora.
Declara una propiedad lazy añadiéndole by lazy
al final de su tipo.
val propiedadLazy by lazy{
/* lógica de accesor
}
Por defecto, las propiedades lazy en Kotlin están seguras en un ambiente multihilo, ya que lazy()
mostrará el mismo valor a los hilos que intenten accederlo.
Ejemplo: Propiedad De Nivel Superior Lazy
Declaremos una propiedad de nivel superior que almacene el valor del tiempo actual.
val currentTime: Long by lazy {
System.currentTimeMillis()
}
fun main() {
println("Valor en llamada 1: $currentTime")
println ("Valor en llamada 2: $currentTime")
}
Salida:
Valor en llamada 1: 1611934597326
Valor en llamada 2: 1611934597326
Al delegar el contenido de la propiedad con la función lazy{}
, la primer llamada recordará el valor inicial, por lo que en la llamada 2 no habrá una segunda ejecución.
Y ya que solo tomará un valor al ejecutarse la lambda, debes usar val
para la declaración.
La Función lazy
Como ves, lazy
es una función que recibe como argumento un lambda y retorna una instancia de la interfaz Lazy<T>
.
fun <T> lazy(initializer: () -> T): Lazy<T>
La interfaz Lazy<T>
contiene un método llamado isInitalized()
, el cual actúa como centinela para no permitir las llamadas subsecuentes de la lambda pasada como parámetro:
public interface Lazy<out T> {
public val value: T
public fun isInitialized(): Boolean
}
Esta interfaz proporcionará el delegado para proveer el método getValue()
necesario por by
.
Ejemplo: Propiedad Miembro Lazy
Las propiedades miembro funcionan exactamente igual. Cuando usas el operador de acceso punto para obtener el valor de la propiedad, se ejecutará la lambda de inicialización y se establecerá su valor.
Por ejemplo, supongamos que deseamos crear un contendor de dependencias para manejar la inicialización de nuestros componentes.
En este caso: una base de datos con estadísticas de un usuario y el repositorio que la usa.
La idea es inicializar solo una vez a cada elemento, por lo que declararlas como propiedades lazy
nos vendría de lujo:
class CoreDatabase(val connection: String)
class StatsRepository(val db: CoreDatabase)
object Dependencies {
val dbController by lazy {
println("Inicializando a dbController")
CoreDatabase("mi_base_de_datos")
}
val statsRepository by lazy {
println("Inicializando a statsRepository")
StatsRepository(dbController)
}
}
Como ves, Dependencies
actúa como service locator y es una declaración de objeto.
En su interior tendremos a dbController
como el punto de acceso a la base de datos y a statsRepository
como el repositorio.
statsRepository
depende de la instancia de dbController
, por lo que este disparará la lambda asignado en el cuerpo de lazy
. Si accedes a statsRepository
:
fun main() {
Dependencies.statsRepository
}
Tendrás:
Inicializando a statsRepository
Inicializando a dbController
Y si solo haces la llamada a Dependencies.dbController
, solo se verá la inicialización de esta propiedad, ya que el repositorio esperará hasta que tú decidas acceder a su valor.