Iniciando con Corrutinas de Kotlin en Android

Nicolas Gonzalez
12 min readJan 2, 2023

Una breve introducción sobre las corrutinas que te servirá para aumentar la productividad y escribir código mas limpio y conciso en tus aplicaciones.

  • ¿Que son las Corrutinas?
  • ¿Cuál es la función de una Corrutina?
  • ¿Que es el launch?
  • Tipos de CoroutineScope
  • ¿De que se encarga el Dispatchers?
  • Manejo de excepciones usando Corrutinas

¿Cual es la diferencia entre Asíncrono y Síncrono?

El término “asíncrono” se refiere a algo que sucede de manera independiente a otro proceso y no necesariamente en el mismo orden. Por ejemplo, si tienes una aplicación que realiza una tarea asíncrona, esa tarea puede realizarse mientras la aplicación sigue funcionando y haciendo otras cosas. Esto es útil cuando quieres evitar bloquear el hilo de la interfaz de usuario (UI) mientras realizas una tarea que puede llevar mucho tiempo, como descargar datos de Internet.

Por otro lado, el término “síncrono” se refiere a algo que sucede en un orden específico y una tarea síncrona debe completarse antes de que otra tarea pueda comenzar. Por ejemplo, si tienes una aplicación que realiza una tarea síncrona, la aplicación no puede hacer otra cosa hasta que se complete esa tarea. Esto puede ser útil cuando quieres asegurarte de que una tarea se complete antes de que otra tarea comience, pero puede ser inconveniente si la tarea toma mucho tiempo y bloquea la interfaz de usuario (UI).

Un ejemplo aplicado en Android podría ser el de una tarea “asíncrona” para descargar datos de Internet mientras se muestra una pantalla de carga a los usuarios. En lugar de bloquear la interfaz de usuario (UI) mientras se realiza la descarga, se puede mostrar una pantalla de carga y realizar la descarga de manera asíncrona en segundo plano. De esta manera, los usuarios pueden seguir interactuando con la aplicación mientras se realiza la descarga y se puede actualizar la interfaz de usuario (UI) una vez que se completa la descarga.

Un ejemplo de una tarea síncrona en una aplicación Android podría ser leer un archivo de almacenamiento interno y procesar los datos contenidos en él. En este caso, es importante asegurarse de que se lea y procese el archivo completo antes de realizar cualquier otra tarea, por lo que se debe usar una tarea síncrona.

¿Que son las corutinas?

Las corrutinas son una forma de escribir código asíncrono de manera síncrona. Esto significa que puedes escribir código que se ejecuta de manera asíncrona de manera similar a como escribirías código síncrono, pero sin tener que lidiar con callbacks anidados y sin bloquear el hilo de interfaz de usuario (UI). Esto hace que sea más fácil escribir aplicaciones que realizan tareas asíncronas de manera más clara y legible.

Las corrutinas están disponibles a partir de Android 3.0 (Honeycomb) y se pueden usar con la biblioteca de soporte de AndroidX.

¿Cuál es la función de una Corrutina?

La función principal de las corrutinas en Android es facilitar la legibilidad al escribir código asíncrono de forma más clara. Las corrutinas permiten escribir código asíncrono de manera similar a como se escribe código síncrono, lo que hace que sea más fácil de leer y entender.

Además, las corrutinas permiten realizar tareas asíncronas de manera más eficiente y evitan el uso de callbacks anidados, lo que puede hacer que el código sea más fácil de mantener. También tienen menos impacto en el rendimiento que otras soluciones asíncronas, como los hilos, por lo que son una opción atractiva para realizar tareas asíncronas en Android.

En resumen, las corrutinas son una herramienta útil para escribir código asíncrono de manera más clara y legible, y para hacer que el código sea más fácil de mantener y tenga menor impacto en el rendimiento.

Para declarar una corrutina en Android, primero debes incluir la biblioteca de soporte de AndroidX en tu proyecto y agregar la dependencia en tu archivo build.gradle.

dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}

Luego, en tu clase, debes importar la clase CoroutineScope y crear una instancia de esta clase.

Para crear una corrutina, debes usar el método launch o async de CoroutineScope. El método launch se utiliza para crear una corrutina que no devuelve un resultado, mientras que el método async se utiliza para crear una corrutina que devuelve un resultado. Ambas funciones toman un bloque de código lambda como argumento que contiene la lógica de la corrutina.

Ejemplo de una corutina que no devuelve un resultado:

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

class MyViewModel : ViewModel() {
fun doWork() {
viewModelScope.launch {
// Realiza el trabajo aquí
}
}
}

Ejemplo de cómo se podría declarar una corrutina que devuelve un resultado en Android:

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.async

class MyViewModel : ViewModel() {
fun getData(): Deferred<Data> {
return viewModelScope.async {
// Obtiene los datos aquí y devuelve el resultado
}
}
}

En ambos ejemplos, se crea una instancia de CoroutineScope utilizando el extension property viewModelScope y se usa el método launch o async para crear la corrutina. El bloque de código lambda que se pasa como argumento contiene la lógica de la corrutina y se ejecutará de manera asíncrona cuando se llame a la corrutina.

¿Que es el launch?

launch es un método de la clase CoroutineScope que se utiliza para crear una corrutina que no devuelve un resultado. Se utiliza para realizar tareas asíncronas que no necesitan devolver un valor al llamador, como enviar una solicitud de red o actualizar la interfaz de usuario (UI).

El método launch toma un bloque de código lambda como argumento que contiene la lógica de la corrutina y crea una nueva corrutina que se ejecutará de manera asíncrona. El método launch devuelve un objeto Job que puedes usar para cancelar la corrutina si es necesario.

Ejemplo usando el método launch para crear una corrutina que actualiza la interfaz de usuario (UI) en una aplicación Android:

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

class MyViewModel : ViewModel() {
fun updateUI() {
viewModelScope.launch {
// Obtiene los datos aquí
withContext(Dispatchers.Main) {
// Actualiza la interfaz de usuario (UI) con los datos aquí
}
}
}
}

En este ejemplo, se crea una instancia de CoroutineScope utilizando el extension property viewModelScope y se usa el método launch para crear una corrutina que se ejecutará de manera asíncrona. La corrutina obtiene los datos necesarios y luego actualiza la interfaz de usuario (UI) con los datos utilizando el método withContext(Dispatchers.Main), que ejecuta el código en el hilo de la interfaz de usuario (UI).

Tipos de CoroutineScope

Existen varios tipos de CoroutineScope que puedes usar para crear corrutinas en Android. Algunos de los más comunes son:

  • GlobalScope: Es una instancia de CoroutineScope que se ejecuta en un hilo global y no está vinculada a ningún componente de Android, como una Activity o un Fragment. Se puede usar para realizar tareas que no estén vinculadas a la interfaz de usuario (UI) o para iniciar una corrutina que sea independiente de la vida útil de cualquier componente de Android.
  • viewModelScope: Es un extension property de ViewModel que se puede usar para crear corrutinas que se cancelen cuando el ViewModel se destruye. Es útil para realizar tareas asíncronas que se relacionen con la vista, pero que no dependan de la vida útil de una Activity o Fragment en particular.
  • lifecycleScope: Es un extension property de LifecycleOwner (como Activity o Fragment) que se puede usar para crear corrutinas que se cancelen cuando el LifecycleOwner entra en un estado de destrucción. Es útil para realizar tareas asíncronas que dependan de la vida útil de un componente de Android en particular.

Ejemplo de cómo se podrían usar diferentes tipos de CoroutineScope para crear corrutinas en una aplicación Android:

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

class MyViewModel : ViewModel() {
fun doWork() {
// Crea una corrutina en viewModelScope
viewModelScope.launch {
// Realiza el trabajo aquí
}
}
}
import androidx.lifecycle.ViewModel
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch

class MyViewModel : ViewModel() {
fun doWork() {
// Crea una corrutina en GlobalScope
GlobalScope.launch {
// Realiza el trabajo aquí
}
}
}

class MyFragment : Fragment, LifecycleOwner {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

// Crea una corrutina en lifecycleScope
lifecycleScope.launch {
// Realiza el trabajo aquí
}
}

¿De que se encarga el Dispatchers?

El objeto Dispatchers es una clase provista por Kotlin que proporciona acceso a diferentes hilos de ejecución y se utiliza a menudo con corrutinas para especificar en qué hilo se debe ejecutar una tarea.

Por ejemplo:

  • Dispatchers.Main éste método se utiliza para ejecutar una tarea en el hilo de la interfaz de usuario (UI) y es útil para actualizar la interfaz de usuario (UI) desde una corrutina.
  • Dispatchers.IO se utiliza para ejecutar tareas de entrada/salida (I/O) como solicitudes de red y el método Dispatchers.Default se utiliza para ejecutar tareas en un hilo de trabajo por defecto.

Ejemplo de cómo se podría usar el objeto Dispatchers para especificar en qué hilo se debe ejecutar una tarea en una corrutina en una aplicación Android:

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class MyViewModel : ViewModel() {
fun doWork() {
viewModelScope.launch {
// Realiza el trabajo en un hilo de trabajo por defecto
val result = withContext(Dispatchers.Default) {
// Realiza el trabajo aquí
}
// Actualiza la interfaz de usuario (UI) con el resultado en el hilo de la interfaz de usuario (UI)
withContext(Dispatchers.Main) {
// Actualiza la interfaz de usuario (UI) aquí
}
}
}
}

En este ejemplo, la corrutina se inicia en el hilo de trabajo por defecto y luego se usa el método withContext(Dispatchers.Default) para ejecutar una tarea en el hilo de trabajo por defecto. Luego, se usa el método withContext(Dispatchers.Main) para actualizar la interfaz de usuario (UI) con el resultado en el hilo de la interfaz de usuario (UI).

Manejo de excepciones usando Corrutinas

Las corrutinas te permiten manejar excepciones de manera similar a como lo harías en código síncrono. Puedes usar la instrucción try-catch para atrapar excepciones y la instrucción throw para lanzar excepciones.

Por ejemplo, aquí hay un ejemplo de cómo se podría manejar una excepción en una corrutina en una aplicación Android:

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

class MyViewModel : ViewModel() {
fun doWork() {
viewModelScope.launch {
try {
// Realiza el trabajo aquí
} catch (e: Exception) {
// Maneja la excepción aquí
}
}
}
}

En este ejemplo, se crea una corrutina y se usa la instrucción try-catch para atrapar cualquier excepción que pueda ocurrir durante la ejecución de la corrutina. Si se produce una excepción, se maneja en el bloque catch.

También puedes lanzar excepciones desde una corrutina usando la instrucción throw. Por ejemplo:

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

class MyViewModel : ViewModel() {
fun doWork() {
viewModelScope.launch {
// Realiza el trabajo aquí
if (errorOccurred) {
throw Exception("Error occurred")
}
}
}
}

En este ejemplo, se lanza una excepción si se produce un error durante la ejecución de la corrutina. La excepción puede ser atrapada y manejada en otro lugar del código, como se muestra en el primer ejemplo.

Es importante tener en cuenta que las excepciones lanzadas desde una corrutina no se propagarán automáticamente al hilo en el que se creó la corrutina. En su lugar, debes atrapar la excepción en la corrutina y manejarla allí o usar el método async y el método await para propagar la excepción al hilo que llamó a la corrutina.

Manejo de excepciones con async y await

Otra forma de manejar excepciones con corrutinas es usar el método async y el método await para propagar la excepción al hilo que llamó a la corrutina.

El método async es similar al método launch, pero devuelve un objeto Deferred que puedes usar para obtener el resultado de la corrutina una vez que se complete. El método await se usa para esperar a que se complete la corrutina y obtener su resultado. Si la corrutina lanza una excepción, el método await propagará la excepción al hilo que llamó a la corrutina.

Aquí hay un ejemplo de cómo se podría manejar una excepción usando el método async y el método await en una aplicación Android:

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll

class MyViewModel : ViewModel() {
fun doWork(): List<Result> {
val deferredResults = listOf(
viewModelScope.async {
// Realiza el trabajo y devuelve el resultado aquí
if (errorOccurred) {
throw Exception("Error occurred")
}
return@async result
}
)
return try {
deferredResults.awaitAll()
} catch (e: Exception) {
// Maneja la excepción aquí
emptyList()
}
}
}

En este ejemplo, se crea una lista de objetos Deferred utilizando el método async. Cada objeto Deferred representa una corrutina que se ejecutará de manera asíncrona. Si alguna de las corrutinas lanza una excepción, el método awaitAll propagará la excepción al hilo que llamó a la corrutina. La excepción se atrapa en el bloque catch y se maneja allí.

Si usas el método awaitAll para esperar a que se completen varias corrutinas y una de ellas lanza una excepción, el método awaitAll propagará la excepción y todas las corrutinas restantes se cancelarán.

Por ejemplo, en el siguiente código:

val deferredResults = listOf(
viewModelScope.async {
// Realiza el trabajo y devuelve el resultado aquí
if (errorOccurred) {
throw Exception("Error occurred")
}
return@async result1
},
viewModelScope.async {
// Realiza el trabajo y devuelve el resultado aquí
return@async result2
}
)

try {
deferredResults.awaitAll()
} catch (e: Exception) {
// Maneja la excepción aquí
}

Si la primera corrutina lanza una excepción, el método awaitAll propagará la excepción y cancelará la segunda corrutina. Si no se produce ninguna excepción, el método awaitAll devuelve una lista con los resultados de las corrutinas.

También puedes usar el método async y el método await para ejecutar varias corrutinas de manera independiente y manejar cada excepción de manera individual:

val deferredResult1 = viewModelScope.async {
// Realiza el trabajo y devuelve el resultado aquí
if (errorOccurred) {
throw Exception("Error occurred")
}
return@async result1
}

val deferredResult2 = viewModelScope.async {
// Realiza el trabajo y devuelve el resultado aquí
return@async result2
}

try {
val result1 = deferredResult1.await()
val result2 = deferredResult2.await()
} catch (e: Exception) {
// Maneja la excepción aquí
}

En este ejemplo, cada corrutina se ejecuta de manera independiente y se maneja cada excepción de manera individual en el bloque catch.

Una forma más elegante de manejar excepciones de corrutinas en Kotlin es usar el método runCatching o el método try de la clase Deferred.

El método runCatching es una función top-level que ejecuta un bloque de código y devuelve un objeto Result que contiene el resultado de la operación o la excepción que se lanzó. Puedes usar el método isSuccess para comprobar si la operación se realizó correctamente y el método exceptionOrNull para obtener la excepción lanzada, si la hay.

Aquí hay un ejemplo de cómo se podría usar el método runCatching para manejar excepciones en una corrutina en una aplicación Android:

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

class MyViewModel : ViewModel() {
fun doWork() {
viewModelScope.launch {
val result = runCatching {
// Realiza el trabajo aquí
if (errorOccurred) {
throw Exception("Error occurred")
}
return@runCatching result
}
if (result.isSuccess) {
// El trabajo se realizó correctamente
} else {
val exception = result.exceptionOrNull()
// Maneja la excepción aquí
}
}
}
}

En este ejemplo, se crea una corrutina y se usa el método runCatching para ejecutar un bloque de código. Si la operación se realizó correctamente, el método runCatching devuelve un objeto Result con el resultado de la operación. Si se lanzó una excepción, el método runCatching devuelve un objeto Result con la excepción. Puedes usar el método isSuccess para comprobar si la operación se realizó correctamente y el método exceptionOrNull para obtener la excepción lanzada, si la hay.

Otra forma de manejar excepciones de corrutinas es usar el método try de la clase Deferred.

La forma de manejar excepciones de corrutinas usando el método try de la clase Deferred es similar a la forma de manejar excepciones usando el método runCatching. El método try es una función de extensión que devuelve un objeto Result que contiene el resultado de la corrutina o la excepción que se lanzó.

Ejemplo de cómo se podría usar el método try para manejar excepciones en una corrutina en una aplicación Android:

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll

class MyViewModel : ViewModel() {
fun doWork(): List<Result> {
val deferredResults = listOf(
viewModelScope.async {
// Realiza el trabajo y devuelve el resultado aquí
if (errorOccurred) {
throw Exception("Error occurred")
}
return@async result
}
)
return deferredResults.map { it.try() }
}
}

En este ejemplo, se crea una lista de objetos Deferred utilizando el método async. Luego se usa el método try para obtener una lista de objetos Result que contienen el resultado de las corrutinas o las excepciones que se lanzaron. Puedes usar el método isSuccess para comprobar si la corrutina se completó correctamente y el método exceptionOrNull para obtener la excepción lanzada, si la hay.

Es importante tener en cuenta que el método try solo se puede usar con objetos Deferred y no con corrutinas lanzadas con el método launch. Si quieres manejar excepciones de corrutinas lanzadas con el método launch, debes usar el método runCatching o la instrucción try-catch.

Espero éste post te haya servido para dar inicio al uso de la corrutinas y al como manejar excepciones de distintas formas! :D.

--

--

Nicolas Gonzalez
Nicolas Gonzalez

Written by Nicolas Gonzalez

Android & Flutter Developer at Globant, cat&dog, guitar and PIZZA LOVER!

No responses yet