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, sino cuando se inicializa su clase anidada.
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.