Cómo refactorizar a Kotlin

En este codelab, aprenderás a convertir tu código de Java a Kotlin. También aprenderás cuáles son las convenciones del lenguaje Kotlin y cómo asegurarte de respetarlas al escribir el código.

Este codelab está pensado para desarrolladores que usan Java y piensan en migrar su proyecto a Kotlin. Comenzaremos con algunas clases de Java que convertirás a Kotlin usando el IDE. Luego, analizaremos el código convertido y veremos cómo podemos mejorarlo. Para ello, lo haremos más idiomático y evitaremos dificultades comunes.

Qué aprenderás

Aprenderás a convertir Java a Kotlin. Además, conocerás los siguientes conceptos y funciones del lenguaje Kotlin:

  • Cómo procesar la nulabilidad
  • Cómo implementar singletons
  • Clases de datos
  • Cómo controlar strings
  • Operador elvis
  • Cómo realizar la desestructuración
  • Propiedades y propiedades de copia de seguridad
  • Argumentos predeterminados y parámetros con nombre
  • Cómo trabajar con colecciones
  • Funciones de extensión
  • Funciones y parámetros de nivel superior
  • Palabras clave let, apply, with y run

Suposiciones

Ya deberías contar con conocimientos de Java.

Requisitos

Crea un proyecto nuevo

Si usas IntelliJ IDEA, crea un nuevo proyecto de Java con Kotlin/JVM.

Si usas Android Studio, crea un proyecto nuevo sin actividad.

El código

Crearemos un objeto User modelo y una clase singleton Repository que funciona con objetos User y expone listas de usuarios y nombres de usuario con formato.

Crea un archivo nuevo llamado User.java en app/java/<nombredelpaquete> y pega el siguiente código:

public class User {

    @Nullable
    private String firstName;
    @Nullable
    private String lastName;

    public User(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

}

Según el tipo de proyecto, importa androidx.annotation.Nullable si usas un proyecto de Android o org.jetbrains.annotations.Nullable.

Crea un archivo nuevo llamado Repository.java y pega el siguiente código:

import java.util.ArrayList;
import java.util.List;

public class Repository {

    private static Repository INSTANCE = null;

    private List<User> users = null;

    public static Repository getInstance() {
        if (INSTANCE == null) {
            synchronized (Repository.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Repository();
                }
            }
        }
        return INSTANCE;
    }

    // keeping the constructor private to enforce the usage of getInstance
    private Repository() {

        User user1 = new User("Jane", "");
        User user2 = new User("John", null);
        User user3 = new User("Anne", "Doe");

        users = new ArrayList();
        users.add(user1);
        users.add(user2);
        users.add(user3);
    }

    public List<User> getUsers() {
        return users;
    }

    public List<String> getFormattedUserNames() {
        List<String> userNames = new ArrayList<>(users.size());
        for (User user : users) {
            String name;

            if (user.getLastName() != null) {
                if (user.getFirstName() != null) {
                    name = user.getFirstName() + " " + user.getLastName();
                } else {
                    name = user.getLastName();
                }
            } else if (user.getFirstName() != null) {
                name = user.getFirstName();
            } else {
                name = "Unknown";
            }
            userNames.add(name);
        }
        return userNames;
    }
}

Nuestro IDE puede refactorizar de forma automática el código Java en Kotlin, pero a veces necesita un poco de ayuda. Primero, haremos esto y, luego, revisaremos el código refactorizado para comprender cómo y por qué se refactorizó de esta manera.

Ve al archivo User.java y conviértelo a Kotlin: Barra de menú -> Code -> Convert Java File to Kotlin File.

Si el IDE solicita una corrección después de la conversión, presiona Yes.

Deberías ver el siguiente código de Kotlin:

class User(var firstName: String?, var lastName: String?)

Ten en cuenta que se cambió el nombre User.java por User.kt. Los archivos de Kotlin tienen la extensión .kt.

En nuestra clase User de Java, teníamos dos propiedades: firstName y lastName. Cada uno tenía un método get y un método set, por lo que su valor era mutable. La palabra clave de Kotlin para las variables mutables es var, de modo que el conversor usa var para cada una de estas propiedades. Si nuestras propiedades Java solo tuvieran métodos get, serían inmutables y se declararían como variables val. val es similar a la palabra clave final de Java.

Una de las diferencias clave entre Kotlin y Java es que el primero especifica de manera explícita si una variable puede aceptar un valor nulo. Para ello, agrega un elemento"?"a la declaración de tipo.

Dado que marcaste las variables firstName y lastName como anulables, el convertidor automático marcó automáticamente las propiedades como anulables con String?. Si anotas tus miembros de Java como no nulos (mediante org.jetbrains.annotations.NotNull o androidx.annotation.NonNull), el conversor también reconocerá esta acción y los campos tampoco serán nulos en Kotlin.

La refactorización ya está lista. pero podemos escribir el código de forma más idiomática. Veamos cómo hacerlo.

Clase de datos

La clase User solo contiene datos. Kotlin tiene una palabra clave para las clases con esta función: data. Si marcas esta clase como clase data, el compilador creará automáticamente métodos get y set. También derivará las funciones equals(), hashCode() y toString().

Agreguemos la palabra clave data a nuestra clase User:

data class User(var firstName: String, var lastName: String)

Kotlin, al igual que Java, puede tener un constructor principal y uno o más constructores secundarios. El del ejemplo anterior es el constructor principal de la clase User. Si vas a convertir una clase Java con varios constructores, el conversor también creará automáticamente varios constructores en Kotlin. Se definen con la palabra clave constructor.

Si queremos crear una instancia de esta clase, podemos hacerlo de la siguiente manera:

val user1 = User("Jane", "Doe")

Igualdad

Kotlin tiene dos tipos de igualdad:

  • La igualdad estructural usa el operador == y llama al método equals() para determinar si dos instancias son iguales.
  • La igualdad referencial usa el operador === y comprueba si dos referencias apuntan al mismo objeto.

Las propiedades definidas en el constructor principal de la clase de datos se usarán para las comprobaciones de igualdad estructural.

val user1 = User("Jane", "Doe")
val user2 = User("Jane", "Doe")
val structurallyEqual = user1 == user2 // true
val referentiallyEqual = user1 === user2 // false

En Kotlin, podemos asignar valores predeterminados a los argumentos de las llamadas a funciones. Se usa el valor predeterminado cuando se omite el argumento. En Kotlin, los constructores también son funciones, por lo que podemos usar los argumentos predeterminados para especificar que el valor predeterminado de lastName es null. Para ello, solo debemos asignar null a lastName.

data class User(var firstName: String?, var lastName: String? = null)

// usage
val jane = User("Jane") // same as User("Jane", null)
val joe = User("John", "Doe")

Se puede nombrar los parámetros de función cuando se llama a las funciones:

val john = User(firstName = "John", lastName = "Doe") 

En otro caso de uso, supongamos que firstName tiene null como valor predeterminado y lastName no. En este caso, debido a que el parámetro predeterminado precedería a un parámetro sin valor predeterminado, tendrías que llamar a la función con argumentos con nombre:

data class User(var firstName: String? = null, var lastName: String?)

// usage
val jane = User(lastName = "Doe") // same as User(null, "Doe")
val john = User("John", "Doe")

Antes de continuar, asegúrate de que la clase User sea data. Convierta la clase Repository a Kotlin. El resultado de la conversión automática debería ser el siguiente:

import java.util.*

class Repository private constructor() {
   private var users: MutableList<User?>? = null
   fun getUsers(): List<User?>? {
       return users
   }

   val formattedUserNames: List<String?>
       get() {
           val userNames: MutableList<String?> =
               ArrayList(users!!.size)
           for (user in users) {
               var name: String
               name = if (user!!.lastName != null) {
                   if (user!!.firstName != null) {
                       user!!.firstName + " " + user!!.lastName
                   } else {
                       user!!.lastName
                   }
               } else if (user!!.firstName != null) {
                   user!!.firstName
               } else {
                   "Unknown"
               }
               userNames.add(name)
           }
           return userNames
       }

   companion object {
       private var INSTANCE: Repository? = null
       val instance: Repository?
           get() {
               if (INSTANCE == null) {
                   synchronized(Repository::class.java) {
                       if (INSTANCE == null) {
                           INSTANCE =
                               Repository()
                       }
                   }
               }
               return INSTANCE
           }
   }

   // keeping the constructor private to enforce the usage of getInstance
   init {
       val user1 = User("Jane", "")
       val user2 = User("John", null)
       val user3 = User("Anne", "Doe")
       users = ArrayList<Any?>()
       users.add(user1)
       users.add(user2)
       users.add(user3)
   }
}

Veamos qué hizo el conversor automático:

  • Se agregó un bloque de init (Repository.kt#L50)
  • El campo static ahora forma parte de un bloque companion object (Repository.kt#L33).
  • La lista de users es anulable, ya que no se creó una instancia del objeto en el momento de la declaración (Repository.kt#L7).
  • El método getFormattedUserNames() ahora es una propiedad llamada formattedUserNames (Repository.kt#L11)
  • La iteración sobre la lista de usuarios (que inicialmente era parte de getFormattedUserNames() tiene una sintaxis diferente de la de Java (Repository.kt#L15).

Antes de continuar, limpiemos un poco el código. Podemos ver que el conversor hizo que nuestra lista users sea una lista mutable que contiene objetos anulables. Si bien la lista puede ser nula, supongamos que no puede contener usuarios nulos. Entonces, hagamos lo siguiente:

  • Quita el elemento ? de User? dentro de la declaración de tipo users.
  • getUsers debe mostrar List<User>?

El conversor automático también divide sin necesidad en 2 líneas las declaraciones de variables de las variables de usuarios y de aquellas definidas en el bloque init. Coloquemos cada declaración de variable en una línea. Así se vería nuestro código:

class Repository private constructor() {
    private var users: MutableList<User>? = null

    fun getUsers(): List<User>? {
        return users
    }

    val formattedUserNames: List<String?>
        get() {
            val userNames: MutableList<String?> =
                ArrayList(users!!.size)
            for (user in users) {
                var name: String
                name = if (user!!.lastName != null) {
                    if (user!!.firstName != null) {
                        user!!.firstName + " " + user!!.lastName
                    } else {
                        user!!.lastName
                    }
                } else if (user!!.firstName != null) {
                    user!!.firstName
                } else {
                    "Unknown"
                }
                userNames.add(name)
            }
            return userNames
        }

    companion object {
        private var INSTANCE: Repository? = null
        val instance: Repository?
            get() {
                if (INSTANCE == null) {
                    synchronized(Repository::class.java) {
                        if (INSTANCE == null) {
                            INSTANCE =
                                Repository()
                        }
                    }
                }
                return INSTANCE
            }
    }

    // keeping the constructor private to enforce the usage of getInstance
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

Bloque Init

En Kotlin, el constructor principal no puede contener código, por lo que se coloca el código de inicialización en bloques init. La funcionalidad es la misma.

class Repository private constructor() {
    ...
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

Una gran parte del código init controla las propiedades de inicialización, lo que también se puede hacer en la declaración de la propiedad. Por ejemplo, en la versión de Kotlin de nuestra clase Repository, vemos que se inicializó la propiedad de usuarios en la declaración.

private var users: MutableList<User>? = null

Propiedades y métodos static de Kotlin

En Java, usamos la palabra clave static en los campos o funciones a fin de indicar que pertenecen a una clase, pero no a una instancia de la clase. Por eso, creamos el campo estático INSTANCE en nuestra clase Repository. El equivalente de Kotlin de esto es el bloque companion object. Aquí, también se deberían declarar los campos estáticos y las funciones estáticas. El conversor creó y movió el campo INSTANCE aquí.

Cómo controlar singletons

Como solo necesitamos una instancia de la clase Repository, usamos el patrón singleton en Java. Con Kotlin, puedes aplicar este patrón en el nivel del compilador si reemplazas la palabra clave class por object.

Quita el constructor privado y el objeto complementario, y reemplaza la definición de clase por object Repository.

object Repository {

    private var users: MutableList<User>? = null

    fun getUsers(): List<User>? {
       return users
    }

    val formattedUserNames: List<String>
        get() {
            val userNames: MutableList<String> =
                ArrayList(users!!.size)
        for (user in users) {
            var name: String
            name = if (user!!.lastName != null) {
                if (user!!.firstName != null) {
                    user!!.firstName + " " + user!!.lastName
                } else {
                    user!!.lastName
                }
            } else if (user!!.firstName != null) {
                user!!.firstName
            } else {
                "Unknown"
            }
            userNames.add(name)
       }
       return userNames
   }

    // keeping the constructor private to enforce the usage of getInstance
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

Cuando uses la clase object, solo debes llamar a funciones y propiedades directamente en el objeto, de la siguiente manera:

val users = Repository.users

Cómo realizar la desestructuración

Kotlin permite estructurar un objeto en una serie de variables, mediante una sintaxis llamada declaración de desestructuración. Creamos una serie de variables y las usamos de forma independiente.

Por ejemplo, las clases de datos admiten la desestructuración, de modo que podemos desestructurar el objeto User del bucle for en (firstName, lastName). Esto nos permite trabajar directamente con los valores firstName y lastName. Actualicemos el bucle for de la siguiente manera:

 
for ((firstName, lastName) in users!!) {
       val name: String?

       if (lastName != null) {
          if (firstName != null) {
                name = "$firstName $lastName"
          } else {
                name = lastName
          }
       } else if (firstName != null) {
            name = firstName
       } else {
            name = "Unknown"
       }
       userNames.add(name)
}

Cuando convirtió la clase Repository a Kotlin, el conversor automático hizo que la lista de usuarios sea anulable, ya que no se inicializó en un objeto cuando se declaró. Para todos los usos del objeto users, se usa el operador de aserción no nulo !!. Convierte cualquier variable en un tipo no nulo y arroja una excepción si el valor es nulo. Si usas !!, corres el riesgo de que se generen excepciones durante el tiempo de ejecución.

En cambio, es preferible controlar la nulidad usando uno de estos métodos:

  • Realiza una verificación de nulabilidad (if (users != null) {...}).
  • Usa el operador elvis ?: (que se analiza más adelante en el codelab).
  • Usa algunas de las funciones estándar de Kotlin (que se analizan más adelante en el codelab).

En nuestro caso, sabemos que la lista de usuarios no necesita ser nula, ya que se inicializa justo después de la construcción del objeto, por lo que podemos crear una instancia de este directamente cuando lo declaramos.

Cuando se crean instancias de tipos de colección, Kotlin proporciona varias funciones auxiliares para que el código sea más legible y flexible. Aquí usamos una MutableList para users:

private var users: MutableList<User>? = null

Para simplificar, podemos usar la función mutableListOf(), proporcionar el tipo de elemento de la lista, quitar la llamada del constructor ArrayList del bloque init y quitar la declaración del tipo explícito de la propiedad users.

private val users = mutableListOf<User>()

También cambiamos el valor de var en val porque los usuarios contendrán una referencia inmutable a la lista de usuarios. Ten en cuenta que la referencia es inmutable, pero la lista misma es mutable (puedes agregar o quitar elementos).

Con estos cambios, la propiedad users no es nula y podemos quitar todos los casos innecesarios de operadores !!.

val userNames: MutableList<String?> = ArrayList(users.size)
for ((firstName, lastName) in users) {
    ...
}

Además, como la variable de usuario ya está inicializada, debemos quitar la inicialización del bloque init:

init {
    val user1 = User("Jane", "")
    val user2 = User("John", null)
    val user3 = User("Anne", "Doe")

    users.add(user1)
    users.add(user2)
    users.add(user3)
}

Dado que las propiedades lastName y firstName pueden ser null, debemos controlar la nulidad cuando compilamos la lista de nombres de usuario con formato. Como queremos mostrar "Unknown" si falta alguno de los nombres, podemos hacer que el nombre no sea nulo eliminando ? de la declaración de tipo.

val name: String

Si el lastName es nulo, name es firstName o "Unknown":

if (lastName != null) {
    if (firstName != null) {
        name = "$firstName $lastName"
    } else {
        name = lastName
    }
} else if (firstName != null) {
    name = firstName
} else {
    name = "Unknown"
}

Se puede escribir de forma más idiomática usando el operador elvis ?:. Este operador muestra la expresión en el lado izquierdo si no es nula, o en el lado derecho si el lado izquierdo es nulo.

Por lo tanto, en el siguiente código, se muestra user.firstName si no es nulo. Si el valor de user.firstName es nulo, la expresión muestra el valor en el lado derecho, "Unknown":

if (lastName != null) {
    ...
} else {
    name = firstName ?: "Unknown"
}

Con Kotlin, es fácil trabajar con objetos String mediante plantillas de strings. Las plantillas de strings te permiten hacer referencia a variables dentro de las declaraciones de strings.

El conversor automático actualizó la concatenación del nombre y el apellido para hacer referencia al nombre de la variable directamente en la string mediante el símbolo $ y coloca la expresión entre { }.

// Java
name = user.getFirstName() + " " + user.getLastName();

// Kotlin
name = "${user.firstName} ${user.lastName}"

En el código, reemplaza la concatenación de strings por lo siguiente:

name = "$firstName $lastName"

En Kotlin if, when, for y while son expresiones, ya que muestran un valor. Tu IDE incluso muestra una advertencia que indica que se debe quitar la asignación del if:

Sigamos la sugerencia del IDE e quitemos la asignación para ambas sentencias de if. Se asignará la última línea de la sentencia if. De esta forma, es más claro que el único propósito de este bloqueo es inicializar el valor del nombre:

name = if (lastName != null) {
    if (firstName != null) {
        "$firstName $lastName"
    } else {
        lastName
    }
} else {
   firstName ?: "Unknown"
}

A continuación, recibirás una advertencia que indica que se puede unir la declaración name con la tarea. También apliquemos esto. Debido a que se puede deducir el tipo de la variable de nombre, podemos quitar la declaración del tipo explícito. Ahora nuestro formattedUserNames se ve así:

val formattedUserNames: List<String?>
   get() {
       val userNames: MutableList<String?> = ArrayList(users.size)
       for ((firstName, lastName) in users) {
           val name = if (lastName != null) {
               if (firstName != null) {
                   "$firstName $lastName"
               } else {
                   lastName
               }
           } else {
               firstName ?: "Unknown"
           }
           userNames.add(name)
       }
       return userNames
   }

Analicemos con más detalle el método get formattedUserNames y veamos cómo podemos hacerlo más idiomático. Por el momento, el código hace lo siguiente:

  • Crea una lista nueva de strings.
  • Recorre la lista de usuarios.
  • Construye el nombre con formato para cada usuario, según su nombre y apellido.
  • Muestra la lista recién creada.
val formattedUserNames: List<String>
        get() {
            val userNames = ArrayList<String>(users.size)
            for ((firstName, lastName) in users) {
                val name = if (lastName != null) {
                    if (firstName != null) {
                        "$firstName $lastName"
                    } else {
                        lastName
                    }
                } else {
                    firstName ?: "Unknown"
                }

                userNames.add(name)
            }
            return userNames
        }

Kotlin ofrece una amplia lista de transformaciones de colecciones que hacen que el desarrollo sea más rápido y seguro expandiendo las capacidades de la API de Java Collections. Una de ellas es la función map. Esta función muestra una lista nueva que contiene los resultados de la aplicación de la función de transformación determinada a cada elemento de la lista original. Por lo tanto, en lugar de crear una lista nueva y recorrer la lista de usuarios de forma manual, podemos usar la función map y mover la lógica que teníamos en el bucle for dentro del cuerpo del objeto map. De forma predeterminada, el nombre del elemento de lista actual que se usa en map es it, pero, para que sea más legible, puedes reemplazar it por tu propio nombre de variable. En nuestro caso, la llamaremos user:

    
val formattedUserNames: List<String>
        get() {
            return users.map { user ->
                val name = if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
                name
            }
        }

Para simplificar aún más el código, podemos quitar la variable name por completo:

    
val formattedUserNames: List<String>
        get() {
            return users.map { user ->
                if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
            }
        }

Observamos que el conversor automático reemplazó la función getFormattedUserNames() por una propiedad llamada formattedUserNames, que tiene un método get personalizado. En un nivel profundo, Kotlin genera un método getFormattedUserNames() que muestra una List.

En Java, expondríamos las propiedades de nuestra clase mediante funciones de los métodos get y set. Kotlin nos permite marcar una diferencia más clara entre las propiedades de una clase, expresadas con campos, y la funcionalidades (acciones que una clase puede hacer), expresadas con funciones. En nuestro caso, la clase Repository es muy simple y no realiza ninguna acción, por lo que solo tiene campos.

La lógica que se activó en la función getFormattedUserNames() de Java ahora se activa cuando se llama al método get de la propiedad formattedUserNames de Kotlin.

Si bien no tenemos un campo explícito correspondiente a la propiedad formattedUserNames, Kotlin nos proporciona un campo de copia de seguridad automática llamado field al que podemos acceder desde métodos get y métodos set personalizados.

Sin embargo, tal vez busquemos algunas funciones adicionales que no proporciona el campo de copia de seguridad automática. Veamos un ejemplo a continuación.

Dentro de la clase Repository, tenemos una lista mutable de usuarios que se expone en la función getUsers(), que se generó en función de nuestro código Java:

fun getUsers(): List<User>? {
    return users
}

El problema es que, cuando se muestra la propiedad users, cualquier usuario de la clase Repository puede modificar nuestra lista de usuarios, lo que no es una buena idea. Para solucionar este problema, usaremos una propiedad de copia de seguridad.

En primer lugar, cambia el nombre de users por _users. Ahora, agregue una propiedad inmutable que muestre una lista de usuarios. Llamémoslo users:

private val _users = mutableListOf<User>()
val users: List<User>
      get() = _users

Con este cambio, la propiedad privada _users se convierte en la propiedad de copia de seguridad de la propiedad pública users. Fuera de la clase Repository, no se puede modificar la lista _users, ya que los usuarios de esta pueden acceder a ella solo a través de users.

Código completo:

object Repository {

    private val _users = mutableListOf<User>()
    val users: List<User>
        get() = _users

    val formattedUserNames: List<String>
        get() {
            return _users.map { user ->
                if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
            }
        }

    init {

        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")

        _users.add(user1)
        _users.add(user2)
        _users.add(user3)
    }
}

En este momento, la clase Repository sabe cómo calcular el nombre de usuario con formato para un objeto User. Sin embargo, si queremos reutilizar la misma lógica de formato en otras clases, debemos copiarla y pegarla o moverla a la clase User.

Kotlin ofrece la capacidad de declarar funciones y propiedades fuera de cualquier clase, objeto o interfaz. Por ejemplo, la función mutableListOf() que usamos para crear una nueva instancia de una List se define directamente en Collections.kt desde la biblioteca estándar.

En Java, cada vez que necesitas funciones de utilidad, lo más probable es que debas crear una clase Util y declarar esa función como función estática. En Kotlin, puedes declarar funciones de nivel superior sin tener una clase. Sin embargo, Kotlin también permite crear funciones de extensión. que extienden un tipo determinado, pero se declaran fuera del tipo. Por lo tanto, tienen afinidad con ese tipo.

Se puede restringir la visibilidad de las funciones y propiedades de extensión usando modificadores de visibilidad. Estos restringen el uso solo a las clases que necesitan las extensiones y no alteran el espacio de nombres.

Para la clase User, podemos agregar una función de extensión que calcule el nombre con formato, o bien podemos mantener el nombre con formato en una propiedad de extensión. Se puede agregar fuera de la clase Repository, en el mismo archivo:

// extension function
fun User.getFormattedName(): String {
    return if (lastName != null) {
        if (firstName != null) {
            "$firstName $lastName"
        } else {
            lastName ?: "Unknown"
        }
    } else {
        firstName ?: "Unknown"
    }
}

// extension property
val User.userFormattedName: String
    get() {
        return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

// usage:
val user = User(...)
val name = user.getFormattedName()
val formattedName = user.userFormattedName

Luego, podemos usar las funciones y propiedades de extensión como si fueran parte de la clase User.

Debido a que el nombre con formato es una propiedad del usuario y no una función de la clase Repository, usaremos la propiedad de extensión. Nuestro archivo Repository se ve así:

val User.formattedName: String
    get() {
        return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

object Repository {

    private val _users = mutableListOf<User>()
    val users: List<User>
      get() = _users

    val formattedUserNames: List<String>
        get() {
            return _users.map { user -> user.formattedName }
        }

    init {

        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")

        _users.add(user1)
        _users.add(user2)
        _users.add(user3)
    }
}

La biblioteca estándar de Kotlin usa funciones de extensión para extender la funcionalidad de varias API de Java. Se implementan muchas de las funciones de Iterable y Collection como funciones de extensión. Por ejemplo, la función map que usamos en un paso anterior es una función de extensión de la clase Iterable.

En el código de la clase Repository, agregamos varios objetos de usuario a la lista _users. Estas llamadas se pueden realizar de forma más idiomática con la ayuda de las funciones de alcance.

Para ejecutar código solo en el contexto de un objeto específico, sin necesidad de acceder al objeto en función de su nombre, Kotlin creó 5 funciones de alcance: let, apply, with, run y also. Breves y potentes, todas estas funciones tienen un receptor (this), pueden tener un argumento (it) y pueden mostrar un valor. Usted decide cuál usar según lo que desee lograr.

Esta es una práctica hoja de referencia que te ayudará a recordar esta información:

Como estamos configurando nuestro objeto _users en la clase Repository, podemos hacer que el código sea más idiomático usando la función apply:

init {
    val user1 = User("Jane", "")
    val user2 = User("John", null)
    val user3 = User("Anne", "Doe")
   
    _users.apply {
       // this == _users
       add(user1)
       add(user2)
       add(user3)
    }
 }

En este codelab, abarcamos los conceptos básicos que necesitas para refactorizar tu código de Java a Kotlin. Esta refactorización es independiente de tu plataforma de desarrollo y ayuda a garantizar que el código que escribas sea idiomático.

Este tipo de lenguaje hace que escribir código sea corto y atractivo. Con todas las funciones que proporciona Kotlin, tienes muchas maneras de hacer que tu código sea más seguro, conciso y fácil de leer. Por ejemplo, podemos optimizar la clase Repository creando instancias de la lista _users con usuarios directamente en la declaración, lo que permite eliminar el bloque init:

private val users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))

Abarcamos una gran variedad de temas: desde el control de nulabilidades, singletons, strings y colecciones, hasta temas como funciones de extensión, funciones de nivel superior, propiedades y funciones de alcance. Comenzamos con dos clases de Java y terminamos con dos clases de Kotlin que ahora se ven así:

User.kt

class User(var firstName: String?, var lastName: String?)

Repository.kt

val User.formattedName: String
    get() {
       return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

object Repository {

    private val _users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))
    val users: List<User>
        get() = _users

    val formattedUserNames: List<String>
        get() = _users.map { user -> user.formattedName }
}

Este es un resumen de las funciones de Java y su correspondencia en Kotlin:

Java

Kotlin

Objeto final

Objeto val

equals()

==

==

===

Clase que solo contiene datos

Clase data

Inicialización en el constructor

Inicialización en el bloque init

Campos y funciones static

Campos y funciones declarados en un companion object

Clase singleton

object

Para obtener más información sobre Kotlin y cómo usarlo en tu plataforma, consulta los siguientes recursos: