Usa patrones comunes de Kotlin con Android

En este tema, nos centraremos en algunos de los aspectos más útiles del lenguaje Kotlin cuando se desarrolla contenido para Android.

Trabaja con fragmentos

En las siguientes secciones, se usan ejemplos de Fragment para destacar algunas de las mejores funciones de Kotlin.

Herencia

Puedes declarar una clase en Kotlin con la palabra clave class. En el siguiente ejemplo, LoginFragment es una subclase de Fragment. Puedes indicar la herencia si utilizas el operador : entre la subclase y su elemento superior:

class LoginFragment : Fragment()

En esta declaración de clase, LoginFragment es responsable de llamar al constructor de su superclase, Fragment.

Dentro de LoginFragment, puedes anular una serie de devoluciones de llamada de ciclo de vida para responder a los cambios de estado en tu Fragment. Para anular una función, utiliza la palabra clave override, como se muestra en el siguiente ejemplo:

override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
): View? {
    return inflater.inflate(R.layout.login_fragment, container, false)
}

Para hacer referencia a una función en la clase superior, utiliza la palabra clave super, como se muestra a continuación:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
}

Nulabilidad e inicialización

En los ejemplos anteriores, algunos de los parámetros de los métodos anulados tienen tipos con el signo de interrogación ?. Esto indica que los argumentos que se pasaron para esos parámetros pueden ser nulos. Asegúrate de procesar la nulabilidad de forma segura.

En Kotlin, debes inicializar las propiedades de un objeto cuando lo declaras. Esto implica que, cuando obtienes una instancia de una clase, puedes hacer referencia de inmediato a cualquiera de sus propiedades accesibles. Sin embargo, no es posible aumentar los objetos View de un Fragment hasta que se llama a Fragment#onCreateView, por lo que necesitas una forma de diferir la inicialización de propiedades para una View.

lateinit te permite diferir la inicialización de propiedades. Cuando uses lateinit, deberías inicializar tu propiedad lo antes posible.

El siguiente ejemplo demuestra el uso de lateinit para asignar objetos View en onViewCreated:

class LoginFragment : Fragment() {

    private lateinit var usernameEditText: EditText
    private lateinit var passwordEditText: EditText
    private lateinit var loginButton: Button
    private lateinit var statusTextView: TextView

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        usernameEditText = view.findViewById(R.id.username_edit_text)
        passwordEditText = view.findViewById(R.id.password_edit_text)
        loginButton = view.findViewById(R.id.login_button)
        statusTextView = view.findViewById(R.id.status_text_view)
    }

    ...
}

Conversión de SAM

Para escuchar eventos de clics en Android, implementa la interfaz OnClickListener. Los objetos Button contienen una función setOnClickListener() que incluye una implementación de OnClickListener.

OnClickListener tiene un método abstracto único, onClick(), que debes implementar. Dado que setOnClickListener() siempre toma a OnClickListener como argumento, y debido a que OnClickListener siempre tiene el mismo método abstracto único, esta implementación se puede representar mediante una función anónima en Kotlin. Este proceso se conoce como conversión de método abstracto único o conversión de SAM.

La conversión de SAM puede hacer que tu código sea mucho más limpio. En el siguiente ejemplo, se muestra cómo usar la conversión de SAM a fin de implementar un OnClickListener para un Button:

loginButton.setOnClickListener {
    val authSuccessful: Boolean = viewModel.authenticate(
            usernameEditText.text.toString(),
            passwordEditText.text.toString()
    )
    if (authSuccessful) {
        // Navigate to next screen
    } else {
        statusTextView.text = requireContext().getString(R.string.auth_failed)
    }
}

El código dentro de la función anónima que se pasó a setOnClickListener() se ejecuta cuando un usuario hace clic en loginButton.

Objetos complementarios

Los objetos complementarios ofrecen un mecanismo para definir variables o funciones que están vinculadas de forma conceptual a un tipo, pero no a un objeto en particular. Los objetos complementarios son similares al uso de la palabra clave static de Java para variables y métodos.

En el siguiente ejemplo, TAG es una constante de String. No necesitas una instancia única de String para cada instancia de LoginFragment, por lo que debes definirla en un objeto complementario:

class LoginFragment : Fragment() {

    ...

    companion object {
        private const val TAG = "LoginFragment"
    }
}

Podrías definir TAG en el nivel superior del archivo, pero el archivo también podría tener una gran cantidad de variables, funciones y clases que también están definidas en el nivel superior. Los objetos complementarios ayudan a conectar variables, funciones y la definición de clase sin hacer referencia a ninguna instancia particular de esa clase.

Delegación de propiedades

Cuando inicializas propiedades, puedes repetir algunos de los patrones más comunes de Android, como el acceso a un ViewModel dentro de un Fragment. Para evitar el exceso de código duplicado, puedes usar la sintaxis delegación de propiedades de Kotlin.

private val viewModel: LoginViewModel by viewModels()

La delegación de propiedades proporciona una implementación común que puedes reutilizar en toda tu app. Android KTX te brinda algunos delegados de propiedades. Por ejemplo, viewModels recupera un ViewModel cuyo alcance es el actual Fragment.

La delegación de propiedades utiliza la reflexión, lo que agrega un poco de sobrecarga de rendimiento. La compensación es una sintaxis concisa que ahorra tiempo de desarrollo.

Nulabilidad

Kotlin proporciona reglas estrictas de nulabilidad que mantienen la seguridad de tipo en toda tu app. En Kotlin, las referencias a objetos no pueden contener valores nulos de forma predeterminada. A fin de asignar un valor nulo a una variable, debes declarar un tipo de variable anulable. Para ello, agrega ? al final del tipo de base.

Como ejemplo, la siguiente es una expresión ilegal en Kotlin. name es del tipo String y no es anulable:

val name: String = null

Para permitir un valor nulo, debes usar un tipo de String anulable, String?, como se muestra a continuación:

val name: String? = null

Interoperabilidad

Las reglas estrictas de Kotlin hacen que tu código sea más seguro y conciso. Estas reglas reducen las posibilidades de tener un NullPointerException que podría causar una falla en tu app. Asimismo, reducen la cantidad de verificaciones nulas que debes realizar en tu código.

A menudo, también debes llamar a un código que no sea de Kotlin cuando escribes una app para Android, ya que la mayoría de las API de Android están escritas en el lenguaje de programación Java.

La nulabilidad es un área clave, ya que Java y Kotlin difieren en el comportamiento. Java es menos estricto con la sintaxis de nulabilidad.

Como ejemplo, la clase Account tiene algunas propiedades, incluida una propiedad String llamada name. Java no tiene reglas de Kotlin sobre la nulabilidad, sino que se basa en las anotaciones de nulabilidad opcionales para declarar de forma explícita si puedes asignar un valor nulo.

Debido a que el framework de Android está escrito principalmente en Java, es posible que te encuentres con este escenario si llamas a las API sin anotaciones de nulabilidad.

Tipos de plataformas

Si usas Kotlin para hacer referencia a un miembro name sin anotación definido en una clase Account de Java, el compilador no sabe si String se mapea a una String o a una String? en Kotlin. Esta ambigüedad se representa mediante un tipo de plataforma: String!.

String! no tiene un significado especial para el compilador de Kotlin. String! puede representar una String o una String?, y el compilador te permite asignar un valor de cualquier tipo. Ten en cuenta que corres el riesgo de arrojar una NullPointerException si representas el tipo como una String y asignas un valor nulo.

Para solucionar este problema, debes usar anotaciones de nulabilidad cada vez que escribas código en Java. Estas anotaciones ayudan a los desarrolladores de Java y Kotlin.

Por ejemplo, esta es la clase Account, tal como se define en Java:

public class Account implements Parcelable {
    public final String name;
    public final String type;
    private final @Nullable String accessId;

    ...
}

Una de las variables de miembro, accessId, está anotada con @Nullable, lo que indica que puede contener un valor nulo. Por lo tanto, Kotlin consideraría a accessId como una String?.

Para indicar que una variable nunca puede ser nula, usa la anotación @NonNull:

public class Account implements Parcelable {
    public final @NonNull String name;
    ...
}

En este caso, name se considera una String no anulable en Kotlin.

Las anotaciones de nulabilidad se incluyen en todas las API de Android nuevas y en muchas API de Android existentes. Muchas bibliotecas de Java agregaron anotaciones de nulabilidad para asistir mejor a los desarrolladores de Kotlin y Java.

Cómo procesar la nulabilidad

Si no estás seguro sobre un tipo de Java, debes considerar que puede ser anulable. Por ejemplo, el miembro name de la clase Account no está anotado, por lo que debes suponer que es un String anulable.

Si deseas cortar name para que su valor no incluya espacios en blanco iniciales o finales, puedes usar la función trim de Kotlin. Puedes cortar una String? de varias formas. Una de ellas es utilizar el operador de aserción no nulo, !!, como se muestra en el siguiente ejemplo:

val account = Account("name", "type")
val accountName = account.name!!.trim()

El operador !! considera que todo lo que está a su izquierda no es nulo, por lo que, en este caso, se considera que name es una String no nula. Si el resultado de la expresión a la izquierda es nulo, tu app arroja una NullPointerException. Este operador es rápido y fácil, pero debe usarse con moderación, ya que puede volver a introducir instancias de NullPointerException en tu código.

Una opción más segura es utilizar el operador de llamada segura, ?., como se muestra a continuación:

val account = Account("name", "type")
val accountName = account.name?.trim()

Con el operador de llamada segura, si name no es nulo, entonces el resultado de name?.trim() es un valor de nombre sin espacios en blanco iniciales o finales. Si name es nulo, entonces el resultado de name?.trim() es null. Esto significa que tu app nunca podrá arrojar una NullPointerException cuando ejecute esta declaración.

Si bien el operador de llamada segura evita una posible NullPointerException, pasa un valor nulo a la siguiente declaración. En su lugar, puedes procesar casos nulos de inmediato si utilizas un operador Elvis (?:), como se muestra en el siguiente ejemplo:

val account = Account("name", "type")
val accountName = account.name?.trim() ?: "Default name"

Si el resultado de la expresión en el lado izquierdo del operador Elvis es nulo, entonces el valor en el lado derecho se asigna a accountName. Esta técnica es útil para proporcionar un valor predeterminado que, de otro modo, sería nulo.

También puedes usar el operador Elvis para regresar antes de una función, como se muestra en el siguiente ejemplo:

fun validateAccount(account: Account?) {
    val accountName = account?.name?.trim() ?: "Default name"

    // account cannot be null beyond this point
    account ?: return

    ...
}

Cambios en las API de Android

La compatibilidad de las API de Android con Kotlin es cada vez mayor. Varias de las API más comunes de Android, incluidas AppCompatActivity y Fragment, contienen anotaciones de nulabilidad, y ciertas llamadas como Fragment#getContext tienen más alternativas compatibles con Kotlin.

Por ejemplo, acceder al Context de un Fragment casi siempre es no nulo, ya que la mayoría de las llamadas que realizas en un Fragment ocurren mientras el Fragment está unido a una Activity (una subclase de Context). Dicho esto, Fragment#getContext no siempre muestra un valor no nulo, ya que hay casos en los que un Fragment no está unido a una Activity. Por lo tanto, el tipo de datos que se muestra de Fragment#getContext es anulable.

Dado que el Context que muestra Fragment#getContext es anulable (y se anota como @Nullable), debes tratarlo como un Context? en tu código Kotlin. Esto significa que se debe aplicar uno de los operadores mencionados previamente para abordar la nulabilidad antes de acceder a sus propiedades y funciones. Para algunos de estos escenarios, Android contiene las API alternativas que brindan esta conveniencia. Por ejemplo, Fragment#requireContext muestra un Context no nulo y arroja una IllegalStateException si se lo llama cuando un Context es nulo. De esta manera, puedes tratar el Context resultante como no nulo sin la necesidad de operadores de llamada segura o soluciones alternativas.

Inicialización de propiedades

Las propiedades de Kotlin no se inicializan de forma predeterminada. Se deben inicializar cuando se inicializa su clase contenedora.

Puedes inicializar propiedades de diferentes maneras. En el siguiente ejemplo, se muestra el proceso para inicializar una variable index con la asignación de un valor en la declaración de la clase:

class LoginFragment : Fragment() {
    val index: Int = 12
}

Esta inicialización también se puede definir en un bloque de inicializador:

class LoginFragment : Fragment() {
    val index: Int

    init {
        index = 12
    }
}

En los ejemplos anteriores, se inicializa index cuando se construye un LoginFragment.

Sin embargo, es posible que tengas algunas propiedades que no se puedan inicializar durante la construcción del objeto. Por ejemplo, es posible que desees hacer referencia a una View desde un Fragment, lo que significa que primero se debe aumentar el diseño. El aumento no ocurre cuando se construye un Fragment. En cambio, aumenta cuando se llama a Fragment#onCreateView.

Una forma de abordar este escenario es declarar la vista como anulable e inicializarla lo antes posible, como se muestra en el siguiente ejemplo:

class LoginFragment : Fragment() {
    private var statusTextView: TextView? = null

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)

            statusTextView = view.findViewById(R.id.status_text_view)
            statusTextView?.setText(R.string.auth_failed)
    }
}

Si bien funciona de la manera esperada, ahora debes administrar la nulabilidad de View cada vez que hagas referencia a ella. Una mejor solución es utilizar lateinit para la inicialización de View, como se muestra a continuación:

class LoginFragment : Fragment() {
    private lateinit var statusTextView: TextView

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)

            statusTextView = view.findViewById(R.id.status_text_view)
            statusTextView.setText(R.string.auth_failed)
    }
}

La palabra clave lateinit te permite evitar la inicialización de una propiedad cuando se construye un objeto. Si se hace referencia a tu propiedad antes de inicializarse, Kotlin arroja una UninitializedPropertyAccessException, así que asegúrate de inicializar tu propiedad lo antes posible.