Cómo convertir 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 SDK mínimo puede tener cualquier valor y no afectará el resultado.

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;
    }

}

Notarás que tu IDE te indica que no se definió el objeto @Nullable. Por lo tanto, importa androidx.annotation.Nullable si usas Android Studio o org.jetbrains.annotations.Nullable si usas IntelliJ.

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;
    }
}

El IDE puede convertir automáticamente y con bastante eficacia código Java a Kotlin, pero suele necesitar un poco de ayuda. Dejemos que el IDE realice un pase inicial a la conversión. Luego, revisaremos el código resultante para comprender cómo se convirtió de este modo y por qué.

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.

25e57db5b8e76557.png

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 conversión básica 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("Joe", "Doe")

Kotlin te permite etiquetar los argumentos cuando se llama a tus 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, debes llamar a la función que tiene 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")

Los valores predeterminados constituyen un concepto importante y que se suele usar en el código Kotlin. En este codelab, conviene especificar siempre el nombre y apellido en una declaración de objeto User, de modo que no se necesiten valores predeterminados.

Antes de continuar con el codelab, asegúrate de que la clase User sea una clase data. Ahora, convirtamos 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:

  • La lista de users es anulable, ya que no se creó una instancia del objeto en el momento de la declaración.
  • Se declaran las funciones de Kotlin, como getUsers(), con el modificador fun.
  • Ahora, el método getFormattedUserNames() es una propiedad llamada formattedUserNames.
  • La iteración sobre la lista de usuarios (que inicialmente era parte de getFormattedUserNames() tiene una sintaxis diferente de la de Java.
  • El campo static ahora forma parte de un bloque companion object.
  • Se agregó un bloque de init.

Antes de continuar, limpiemos un poco el código. Si observamos en el constructor, podemos ver que el convertidor convirtió a nuestra lista users en 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.
  • Quita el elemento ? de User? para el tipo de datos que se muestra de getUsers() a fin de que se muestre List<User>?.

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ó el bloque de objeto complementario y movió aquí el campo INSTANCE.

Cómo controlar singletons

Dado que solo necesitamos una instancia de la clase Repository, en Java usamos el patrón singleton. 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 reemplaza la definición de la clase con object Repository. También debes quitar el objeto complementario.

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 formattedUserNames = Repository.formattedUserNames

Ten en cuenta que si una propiedad no tiene un modificador de visibilidad, es pública de forma predeterminada, como en el caso de la propiedad formattedUserNames del objeto Repository.

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ó. En consecuencia, para todos los usos del objeto users, se debe usar el operador de aserción no nulo !!. (Verás users!! y user!! en todo el código convertido). El operador !! convierte cualquier variable en un tipo no nulo para que puedas acceder a sus propiedades o funciones de llamada. Sin embargo, se generará una excepción si el valor de la variable es realmente 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 (en el bloque init). Por lo tanto, podemos crear directamente una instancia del objeto users 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() y proporcionar el tipo de elemento de la lista. La función mutableListOf<User>() crea una lista vacía que puede contener objetos User. Dado que el compilador ahora puede inferir el tipo de datos de la variable, quita la declaración del tipo explícito de la propiedad users.

private val users = mutableListOf<User>()

También reemplazamos var por 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).

Dado que ya se inicializó la variable users, quita esta inicialización del bloque init:

users = ArrayList<Any?>()

El bloque init debería verse así:

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

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

Con estos cambios, la propiedad users no es nula y podemos quitar todos los casos innecesarios de operadores !!. Ten en cuenta que seguirás viendo errores de compilación en Android Studio, pero continúa con los próximos pasos de los codelabs para resolverlos.

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)
}

Además, en el valor userNames, si especificas que el tipo de ArrayList contiene Strings, puedes quitar el tipo explícito de la declaración porque se inferirá.

val userNames = ArrayList<String>(users.size)

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 data 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. Actualiza el bucle for como se muestra a continuación. Reemplaza todas las instancias de user.firstName por firstName y reemplaza user.lastName por lastName.

for ((firstName, lastName) in users) {
    var name: String
    name = if (lastName != null) {
        if (firstName != null) {
            firstName + " " + lastName
        } else {
            lastName
        }
    } else if (firstName != null) {
        firstName
    } else {
        "Unknown"
    }
    userNames.add(name)
}

Expresión if

Los nombres de la lista de usuarios aún no están en el formato que necesitamos. Dado que las propiedades lastName y firstName pueden ser null, debemos controlar la nulidad cuando compilamos la lista de nombres de usuario con formato. Nos conviene mostrar "Unknown" si falta alguno de los nombres. Dado que no se modificará la variable name después de que se establezca una vez, podemos usar val en lugar de var. Primero, realiza este cambio.

val name: String

Observa el código que configura la variable del nombre. Es posible que nunca hayas visto una variable configurada como igual y el bloque de código if y else. Esto se permite porque en Kotlin if, when, for y while son expresiones, ya que muestran un valor. Se asignará la última línea de la sentencia if a name. La única función de este bloque es inicializar el valor name.

Según esta lógica, si el valor lastName es nulo, se configura name como firstName o "Unknown".

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

Operador elvis

Se puede escribir este código 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 firstName si no es nulo. Si el valor de firstName es nulo, la expresión muestra el valor en el lado derecho, "Unknown":

name = if (lastName != null) {
    ...
} else {
    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 usando el símbolo $ antes de la variable. También puedes poner una expresión dentro de una declaración de una string si la colocas entre { } y le antepones el símbolo $. Por ejemplo: ${user.firstName}.

Actualmente, el código usa la concatenación de strings para combinar las propiedades firstName y lastName en el nombre de usuario.

if (firstName != null) {
    firstName + " " + lastName
}

En su lugar, reemplaza la concatenación de strings por lo siguiente:

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

El uso de plantillas de strings puede simplificar tu código.

El IDE muestra advertencias si existe una forma más idiomática de escribir el código. Verás un subrayado desigual en el código. Cuando coloques el cursor sobre este, verás una sugerencia sobre cómo refactorizarlo.

Ahora deberías ver una advertencia que indica que se puede unir la declaración name con la asignación. Vamos a aplicarla. Debido a que se puede deducir el tipo de la variable name, podemos quitar la declaración del tipo String explícito. Ahora nuestro formattedUserNames se ve así:

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
    }

Podemos hacer un ajuste adicional. Nuestra lógica de IU muestra "Unknown" en caso de que falten nombres y apellidos, de modo que no admitimos objetos nulos. Por lo tanto, en el tipo de datos de formattedUserNames, reemplaza List<String?> por List<String>.

val formattedUserNames: List<String>

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
            }
        }

Ten en cuenta que usamos el operador elvis para mostrar "Unknown" si el valor de user.lastName es nulo, ya que user.lastName es de tipo String? y se requiere una String para el objeto name.

...
else {
    user.lastName ?: "Unknown"
}
...

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 si es necesario desde los métodos get y set.

Sin embargo, tal vez busquemos algunas funciones adicionales que no proporciona el campo de copia de seguridad automática.

Veamos un ejemplo.

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. Destaca el nombre de la variable y haz clic con el botón derecho en Refactor > Rename para cambiar el nombre de la variable. Luego, agrega una propiedad inmutable que muestre una lista de usuarios. La llamaremos users:

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

En este punto, puedes borrar el método getUsers().

Con el cambio anterior, 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 propiedad List ya está definida en Collections.kt de la biblioteca estándar de Kotlin.

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. No obstante, Kotlin también permite crear funciones de extensión, que extienden un tipo determinado, pero se declaran fuera del 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 de la clase User 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 función 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 User a la lista _users. Estas llamadas pueden realizarse de forma más idiomática con la ayuda de las funciones de alcance de Kotlin.

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 ofrece 5 funciones de alcance: let, apply, with, run y also. Estas funciones hacen que el código sea más fácil de leer y más conciso. Todas las funciones de alcance tienen un receptor (this), pueden tener un argumento (it) y pueden mostrar un valor.

Esta hoja de referencia te ayudará a recordar cuándo debes usar cada función:

6b9283d411fb6e7b.png

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 convertir tu código de Java a Kotlin. Esta conversión es independiente de tu plataforma de desarrollo y ayuda a garantizar que el código que escribas esté en un lenguaje Kotlin 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

data 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: