

También puedes escuchar este post en audio, ¡dale al play!
A través de este blog tendremos una visión general de:
- Definición de Coroutines
- Por qué elegir Coroutines sobre otros métodos
- Conceptos de Coroutines
- Flujos en Coroutines
Como desarrollador de software móvil, es bastante común realizar tareas asíncronas como hacer una llamada a la API y luego esperar el resultado del backend, obtener datos de la base de datos local o cualquier tarea de larga duración. En la mayoría de los lenguajes de programación, escribir código asíncrono es una especie de dolor de cabeza. En este blog, vamos a aprender a lidiar con el código que se ejecuta de forma asíncrona mediante el uso de Coroutines en Kotlin.

Ya tenemos algunas herramientas para manejar la programación asíncrona como Callbacks, RxJava, AsyncTask y Threads así que, ¿por qué usar Coroutines?
Básicamente empezamos a manejarlo usando el mecanismo de Callback, que ayuda a ejecutar una función después de que otra haya terminado. Y si hay una serie de lógica que hacer entonces nos enfrentaremos a un infierno de callbacks. Créeme, a medida que tu proyecto crece, eso podría llevarnos a una ambigüedad en la comprensión del código.
Otra alternativa es RxJava pero la curva de aprendizaje de RxJava es también demasiado. AsyncTask puede introducir fácilmente fugas de memoria en nuestra aplicación.
Aquí es donde entran en juego las Coroutines. Simplemente se dicen hilos ligeros. Las Coroutines nos proporcionan una forma fácil de hacer programación sincrónica y asincrónica.
De la documentación de Kotlin:
"Se puede pensar en una corutina como un hilo ligero. Al igual que los hilos, las coroutinas pueden ejecutarse en paralelo, esperarse unas a otras y comunicarse. La mayor diferencia es que las coroutinas son muy baratas, casi gratis: podemos crear miles de ellas, y pagar muy poco en términos de rendimiento. Los hilos reales, en cambio, son caros de iniciar y mantener. Mil hilos pueden ser un serio desafío para una máquina moderna".
En otras palabras:
- Cada Coroutine es una pequeña unidad de ejecución asignada a un hilo
- No hay limitación, podemos iniciar tantas Coroutines como queramos
- Podemos iniciar y detener Coroutines en cualquier momento
- Podemos tener Coroutines hijos dentro de cada Coroutine
- Cada Coroutine puede ser iniciada en un Scope específico
- Ámbito de la Coroutina
Crea, ejecuta y mantiene un seguimiento de todas sus coroutines. También proporciona eventos del ciclo de vida como el inicio y la pausa de una coroutina.
Este es el resultado cuando ejecutamos el código anterior:

- runBlocking { .. } : crea una Coroutine de forma bloqueante. Bloqueará el hilo principal o el hilo en el que se utilice. En el ejemplo anterior la impresión "La ejecución del programa continuará ahora" se ejecutará después de que el bloque runBlocking se complete.
- GlobalScope.lauch{ .. } : crea una nueva Coroutine, el ámbito será el ciclo de vida de la aplicación.
- coroutineScope { .. } : Crea un nuevo ámbito personalizado y no se completa hasta que todos los Coroutines hijos se completan;
- Si el padre se cancela, todos los hijos se cancelan.
- El padre siempre esperará a que se completen sus hijos.
No recomiendo el uso de GlobalScope porque el padre no va a esperar la finalización de sus hijos y una vez que el padre es cancelado, los otros trabajos van a seguir corriendo aparte. Es decir, ahora es responsabilidad del desarrollador llevar el control del tiempo de vida de las coroutines porque no hay sincronización con los trabajos hijos.
Funciones de suspensión
Las funciones de suspensión son como la columna vertebral de las Coroutinas. Así que es realmente importante entender completamente este concepto antes de avanzar.
Una función de suspensión es simplemente una función que puede ser pausada y reanudada en un momento posterior. Pueden ejecutar una operación de larga duración y esperar a que se complete sin bloquearse. La sintaxis de una función de suspensión es similar a la de una función regular, excepto por la adición de la palabra clave suspender.
Tenga en cuenta que las funciones de suspensión se sincronizan automáticamente con otras variables y funciones del hilo principal.

Este es el resultado cuando ejecutamos el código anterior:

El Contexto
Las coroutines siempre se ejecutan en algún contexto que está representado por un valor del tipo CoroutineContext.

El contexto de Coroutine es un conjunto de varios elementos. Los elementos principales son el
Job de la Coroutine, su dispatcher y también su Exception handler.
El Job
- Se crea con el constructor de coroutinas "launch". Ejecuta un bloque de código especificado y se completa al finalizar este bloque;
- Una vez creado, el trabajo se inicia automáticamente;
- Nos permite manipular el ciclo de vida de la coroutina;
- Tienen jerarquía, podemos tener trabajos padres e hijos;
- Un trabajo se cancela mediante la función cancel();
- Si un trabajo es cancelado, todos sus padres e hijos serán cancelados también;
- La ejecución de un trabajo no produce un valor de resultado. Deberíamos utilizar una interfaz Deferred para un trabajo que produzca un resultado.

Este es el resultado cuando ejecutamos el código anterior:

¿Cómo podemos recuperar el valor de un trabajo al final de la
ejecución?
Como hemos mencionado antes, usaremos Deferred que es un Job con un resultado. Esperará y bloqueará el hilo actual hasta que recuperemos el resultado.Básicamente se crea con el constructor async Coroutine y el resultado se puede recuperar con el método await(), que lanza una excepción si el Deferred ha fallado.
Aquí hay un ejemplo de su uso:

Este es el resultado cuando ejecutamos el código anterior:

El Dispatcher
En Kotlin, todas las Coroutines deben ejecutarse en un dispatcher incluso cuando se ejecutan en el hilo principal. Las coroutines pueden suspenderse a sí mismas, y el dispatcher es el que sabe cómo reanudarlas.Para especificar dónde deben ejecutarse las coroutines, Kotlin proporciona tres Dispatchers
que puedes utilizar:
- Dispatchers.Main : Hilo principal en Android, interactúa con la UI y realiza trabajos ligeros
➢ Llamar a funciones de la interfaz de usuario
➢ Actualizar LiveData
- Dispatchers.IO : Optimizado para la E/S de disco y red fuera del hilo principal
➢ Lectura/escritura de archivos;
➢ Conexión en red.
- Dispatchers.Default : Optimizado para el trabajo intensivo de la CPU fuera del hilo principal
➢ Análisis de JSON
➢ Utilidades
Pero espera, ¿qué pasa si iniciamos una Coroutine usando Dispatchers.Main y queremos
hacer otra acción usando Dispatchers.IO?
¿Cómo podemos cambiar el contexto de una Coroutine?
Esto se puede hacer fácilmente utilizando la función suspender conContexto(Despachador). Permite fácilmente cambiar el contexto, iniciar el ámbito de una Coroutine y cambiar entre despachadores.
Este es un ejemplo de cómo podemos usarlo para cambiar del dispatcher Default al dispatcher IO:

Manejador de excepciones
Manejar las excepciones de forma adecuada tiene un gran impacto en cómo los usuarios perciben nuestra aplicación. Si tu aplicación sigue fallando, el usuario se sentirá decepcionado y puede que no vuelva a utilizarla.Para este objetivo, tenemos dos opciones:
- Usar try/catch
- Usar CoroutineExceptionHandler
Así es como podemos definir un CoroutineExceptionHandler, cada vez que se atrapa una excepción, tiene información sobre el CoroutineContext donde ocurrió la excepción y la propia excepción.

Este es el resultado cuando ejecutamos el código anterior:

Cuando la Coroutine falla con una Excepción, propagará esta excepción a su
padre. Entonces el padre:
- Cancela el resto de sus hijos
- Se cancelará a sí misma
- Propagar la excepción a su padre hasta la raíz de la jerarquía y todas las Coroutines que ya han comenzado en el CoroutineScope serán canceladas también

Este enfoque no siempre es una buena idea. Por ejemplo tenemos la función init() que inicializa las interacciones del usuario con los componentes de la UI usando un CoroutineScope. Imagina que si una coroutina hija falla, el scope se cancelará automáticamente y la UI dejará de responder porque todo el scope se cancela.
Para solucionar esto podemos usar SupervisorJob que es una implementación diferente de un Job.

Podemos crear un CoroutineScope utilizando uno de los siguientes métodos:
- val uiScope = CoroutineScope(SupervisorJob())
- supervisorScope { .. }
Flujos en Coroutines
Otra cosa interesante de las Coroutines es la posibilidad de utilizar Flows. Se trata de un flujo de valores que se calculan de forma asíncrona desde una Coroutine.- Los Flows emiten valores con la función emit()
- Los Flows reciben los valores con la función collect()
- La función constructora de Flows es la función flow{..}
- Un flujo se cancela cuando la coroutina se cancela
- Los flujos son flujos fríos, en otras palabras, el código dentro de un constructor de flujo no se ejecuta hasta que el flujo se recoge

Este es el resultado cuando ejecutamos el código anterior:

- Una lista también puede convertirse en un flujo utilizando la función asFlow():

- Podemos crear un flujo directamente a partir de cualquier tipo de objetos utilizando la función flowOf():

Operadores de flujo
Flow tiene un montón de operadores interesantes para facilitarnos la codificación:
- Map: mapea un flujo a otro flujo

Aquí está el resultado impreso:

- Filtro: Filtra los valores de flujo con una condición específica
En este ejemplo tomaremos un flujo de Int y lo convertiremos en un flujo filtrado que sólo contenga valores pares:

Aquí está el resultado impreso:

- Transformar: Operador de transformación general que puede emitir cualquier valor en cualquier punto.

Aquí está el resultado impreso:

- Tomar: Utiliza sólo un número de valores, no tiene en cuenta el resto de los valores emitidos
En el siguiente ejemplo tenemos un flujo de Int de 1 a 10. Utilizando el operador take, podremos tomar sólo los dos primeros elementos.

Aquí está el resultado impreso:

- toList: convierte un flujo en una lista
- toSet: convierte un flujo en un conjunto que sólo tiene valores únicos (sin valores duplicados)
- flowOn: nos permite cambiar el contexto del flujo
a Dispatchers.IO:

Coroutines en una aplicación Android real
En el desarrollo de Android, básicamente estamos haciendo una petición de red y devolviendo el resultado al hilo principal, donde la aplicación puede entonces mostrar el resultado al usuario.
Usando la arquitectura limpia, el componente de la arquitectura ViewModel llama a la capa de casos de uso en el hilo principal para lanzar la petición de red desde la capa de repositorio. Nuestro objetivo es utilizar coroutines para mantener el hilo principal desbloqueado.
En el siguiente ejemplo veremos cómo podemos utilizar Coroutines en una Arquitectura Limpia con Retrofit:
- He creado una aplicación de demostración que busca artistas por su nombre. Una vez que se encuentra el artista se puede hacer clic en él para ver sus álbumes que se puede hacer clic en para mostrar todos los detalles sobre el mismo. No dude en ponerse en contacto conmigo si desea obtener el código fuente completo.



En esta sección sólo nos centraremos en obtener los detalles del disco.
- Antes de comenzar nuestro proyecto debe contener las siguientes dependencias para tener soporte de Coroutine:

- Result.kt es la envoltura de todas las llamadas a la red en la Aplicación. El resultado puede ser un valor, un error o un dato vacío:

- Capa de origen de datos: MusicApiService.kt
La función suspender para obtener los detalles de los álbumes se escribe brevemente así con la palabra clave suspender:

- Capa del repositorio : MusicRepositoryImpl.kt

BaseDataSource.getResult() es sólo una envoltura para cualquier llamada a la API en la aplicación.
Como podemos ver, creamos un flujo de NetworkResponse<Album>. Dentro de él llamamos a la función suspender dataSource para obtener los detalles del Álbum. Luego mapeamos el resultado recuperado a un objeto Album.
Finalmente emitimos esta respuesta a través de nuestro flujo.
- Capa de caso de uso : GetAlbumDetailsUseCase.kt

- Capa de ViewModel : GetAlbumDetailsViewModel.kt
El componente ViewModel tiene un conjunto de extensiones KTX que trabajan directamente con
coroutines. Se llama "lifecycle-viewmodel-ktx".

Se define un ViewModelScope para cada ViewModel. Lo bueno es que no necesitamos borrar las coroutines creadas dentro de nuestro ViewModel. ViewModelScope hace todo el trabajo por nosotros. Cualquier Coroutine lanzada en este ámbito se cancela automáticamente si se borra el ViewModel evitando cualquier consumo extra de recursos.
Este es el aspecto de nuestro ViewModel:

Comenzamos cancelando el trabajo actual e iniciamos uno nuevo utilizando Dispatcher.IO para las llamadas a la API. Si el nombre del artista y el nombre del álbum no están correctamente configurados, publicamos una respuesta vacía a través del liveData del álbum. En caso contrario, llamamos a nuestra capa useCase y recogemos los valores del flujo mediante la función collect. Por último, publicamos los detalles del álbum a través del liveData del álbum.
-
Ver capa : SearchAlbumDetailsFragment.kt

Una vez que el usuario llega a la pantalla de detalles del Álbum, el viewModel se activará utilizando esta función:

Entonces la vista actualizará la UI dependiendo del resultado del liveData.

Esto es todo, espero que hayas disfrutado de este blog. No dudes en ponerte en contacto conmigo si tienes alguna pregunta o para tener acceso al código fuente completo.
¡Feliz codificación!
.png?ext=.png)
Otros artículos destacados

¡Recibido!
Gracias por rellenar el formulario. Se han enviado los datos correctamente.
