Manejo de excepciones en Kotlin Coroutines

Las corrutinas son una nueva forma de hacer programación asíncrona en Kotlin en Android. Al desarrollar una aplicación lista para producción, queremos asegurarnos de que todas las excepciones se manejen correctamente para que los usuarios tengan una experiencia agradable mientras usan nuestra aplicación. En este artículo, discutiremos cómo manejar adecuadamente las excepciones en un proyecto de Android cuando se usan corrutinas de Kotlin. Si quieres aprender a usar las corrutinas de Kotlin, puedes empezar aquí. Este artículo se dividirá en las siguientes secciones:

  1. ¿Cuáles son algunas de las excepciones?
  2. ¿Cómo tratamos las excepciones en general?
  3. ¿Cómo manejamos de manera eficiente las excepciones en Kotlin Coroutines?

¿Cuáles son algunas de las excepciones?

Las excepciones son eventos inesperados que ocurren mientras se ejecuta o ejecuta un programa. La ejecución se ve interrumpida por excepciones y no se ejecuta el flujo esperado de la aplicación. Por eso, para ejecutar el flujo adecuado de la aplicación, debemos manejar las excepciones en nuestro código.

¿Cómo tratamos las excepciones en general?

Un bloque try-catch es una forma general de manejar excepciones en Kotlin. En el bloque try, escribimos código que puede generar una excepción, y si se genera una excepción, la excepción se captura en el bloque catch. Ilustremos con un ejemplo:

Kotlin

try {
    val gfgAnswer = 5 / 0
    val addition = 2 + 5
    Log.d("GfGMainActivity", gfgAnswer.toString())
    Log.d("GfGMainActivity", addition.toString())
} catch (e: Exception) {
    Log.e("GfGMainActivity", e.toString())
}

En el código anterior, intentamos dividir 5 por 0 mientras sumamos dos números, 2,5. Luego, la solución debe imprimirse en Logcat. Cuando ejecutamos la aplicación, primero debemos obtener el valor en la variable de solución y luego asignar la suma a la variable adicional. Los valores se imprimirán más tarde en la declaración de registro. Las variables de solución y adición no se imprimen en este caso, pero la declaración de registro en el bloque catch tiene una excepción aritmética. La razón de esto es que ningún número se puede dividir por 0. Entonces, cuando obtuvimos la excepción, puede ver que no se dio ningún paso debajo de la primera línea y fue directamente al bloque catch. El código anterior demuestra cómo puede ocurrir una excepción y cómo podemos manejarla.

¿Cómo manejamos de manera eficiente las excepciones en Kotlin Coroutines?

Ahora veremos cómo podemos manejar las excepciones de manera efectiva en nuestro proyecto usando Kotlin Coroutines. Hay varios enfoques para tratar las excepciones.

método genérico

Uso de SupervisorScope y CoroutineExceptionHandler. Para ilustrar esto aún más, considere recuperar una lista de usuarios. Tendríamos una interfaz:

Kotlin

interface GfgService {
    @GET("courses")
    suspend fun getCourses(): List<GfgUser>
    @GET("more-courses")
    suspend fun getMoreCourses(): List<GfgUser>
    @GET("error")
    suspend fun getCoursesWithError(): List<GfgUser>
}

Aquí tenemos tres funciones de suspensión diferentes que podemos usar para obtener una lista de usuarios. Si observa detenidamente, notará que solo las dos primeras funciones, getUsers() y getMoreUsers(), devolverán una lista, mientras que la tercera función, getUserWithError(), generará una excepción.

gfg.HttpException: HTTP 404 Not Found

En aras de la claridad, diseñamos a propósito getUserWithError() para lanzar una excepción. Ahora, repasemos cómo manejar adecuadamente las excepciones en nuestro código usando Kotlin Coroutines.

1. Enfoque genérico

Considere el siguiente escenario: tenemos un ViewModel, TryCatchViewModel (presente en el proyecto) y quiero realizar mi llamada a la API en el ViewModel.

Kotlin

class TryCatchViewModel(
    private val gfgUser: GfgUser,
    private val gfgCoursedb: DatabaseHelper
) : ViewModel() {
  
    private val gfg = MutableLiveData<Resource<List<GfgCourseUser>>>()
  
    fun fetchGfg() {
        viewModelScope.launch {
            gfg.postValue(Resource.loading(null))
            try {
                val gfgFromApi = gfgUser.getGfg()
                gfg.postValue(Resource.success(gfgFromApi))
            } catch (e: Exception) {
                gfg.postValue(Resource.error("Something Went Wrong", null))
            }
        }
    }
  
    fun getGfg(): LiveData<Resource<List<GfgCourseUser>>> {
        return gfg
    }
}

Sin excepción, esto devolverá una lista de usuarios en mi actividad. Digamos que agregamos una excepción a nuestra función fetchUsers(). Cambiaremos el código de la siguiente manera:

Kotlin

fun gfgGfgPro() {
    viewModelScope.launch {
        gfgPro.postValue(Resource.loading(null))
        try {
            val moreGfgProFromApi = apiHelper.getGfgProWithError()
            val gfgProFromApi = apiHelper.getGfgPro()
              
            val allGfgProFromApi = mutableListOf<ApiUser>()
            allGfgProFromApi.addAll(gfgProFromApi)
            allGfgProFromApi.addAll(moreGfgProFromApi)
  
            gfgPro.postValue(Resource.success(allGfgProFromApi))
        } catch (e: Exception) {
            gfgPro.postValue(Resource.error("Aw Snap!", null))
        }
    }
}

Sin excepción, esto devolverá una lista de usuarios en mi actividad. Digamos que agregamos una excepción a nuestra función fetchUsers(). Cambiaremos el código de la siguiente manera:

Kotlin

fun fetchGfgPro() {
    viewModelScope.launch {
        gfgPro.postValue(Resource.loading(null))
        try {
            val moreGfgProFromApi = try {
                apiHelper.getGfgProWithError()
            } catch (e: Exception) {
                emptyList<ApiGfgGfgUser>()
            }
            val gfgProFromApi = try {
                apiHelper.getGfgPro()
            } catch (e: Exception) {
                emptyList<ApiGfgGfgUser>()
            }
  
            val allGfgProFromApi = mutableListOf<ApiGfgGfgUser>()
            allGfgProFromApi.addAll(gfgProFromApi)
            allGfgProFromApi.addAll(moreGfgProFromApi)
  
            gfgPro.postValue(Resource.success(allGfgProFromApi))
        } catch (e: Exception) {
            gfgPro.postValue(Resource.error("Aw Snap", null))
        }
    }
}

En este caso, hemos agregado una excepción individual a ambas llamadas a la API, de modo que si ocurre una, se asigna una lista vacía a la variable y la ejecución continúa.

Este es un ejemplo de ejecución de tareas de manera secuencial.

2. Hacer uso de CoroutineExceptionHandler

En el ejemplo anterior, puede ver que encerramos nuestro código dentro de una excepción try-catch. Sin embargo, cuando trabajamos con corrutinas, podemos manejar las excepciones mediante el uso de un controlador de excepciones de corrutina global llamado CoroutineExceptionHandler.

Kotlin

class ExceptionHandlerViewModel(
    private val gfgServerAPI: GfgServerAPI,
    private val gfgHelperDB: DatabaseHelper
) : ViewModel() {
  
    private val gfgUsers = MutableLiveData<Resource<List<ApiGfgUser>>>()
  
    private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
        gfgUsers.postValue(Resource.error("Aw Snap!", null))
    }
    fun fetchGfgUsers() {
        viewModelScope.launch(exceptionHandler) {
            gfgUsers.postValue(Resource.loading(null))
            val gfgUsersFromApi = gfgServerAPI.getGfgUsers()
            gfgUsers.postValue(Resource.success(gfgUsersFromApi))
        }
    }
    fun getGfgUsers(): LiveData<Resource<List<ApiGfgUser>>> {
        return gfgUsers
    }
}

GeekTip: En este caso, agregamos getUsersWithError(), que generará una excepción y pasará el identificador al controlador.

Tenga en cuenta que aquí no hemos usado un bloque try-catch, y la excepción será manejada por CoroutineExceptionHandler, que actúa como un controlador de excepciones global para coroutine.

3. Empleo de SupervisorScope

No queremos que la ejecución de nuestra tarea termine debido a una excepción. Pero, hasta ahora, hemos visto que cada vez que encontramos una excepción, nuestra ejecución falla y la tarea finaliza. Ya hemos visto cómo mantener la tarea en ejecución secuencial; en esta sección, veremos cómo mantener la ejecución de la tarea en ejecución paralela. Entonces, antes de comenzar con supervisorScope, primero comprendamos el problema con la ejecución en paralelo. Supongamos que realizamos una ejecución paralela, como:

Kotlin

private fun fetchGfgUsers() {
    viewModelScope.launch {
        gfgUsers.postValue(Resource.loading(null))
        try {
                val gfgUsersWithErrorFromGfgServerDeferred = async { gfgServerHelper.getGfgUsersWithError() }
                val moreGfgUsersFromGfgServerDeferred = async { gfgServerHelper.getMoreGfgUsers() }
                val gfgUsersWithErrorFromGfgServer = gfgUsersWithErrorFromGfgServerDeferred.await()
                val moreGfgUsersFromGfgServer = moreGfgUsersFromGfgServerDeferred.await()
                val allGfgUsersFromGfgServer = mutableListOf<GfgServerGfgUser>()
                allGfgUsersFromGfgServer.addAll(gfgUsersWithErrorFromGfgServer)
                allGfgUsersFromGfgServer.addAll(moreGfgUsersFromGfgServer)
  
                gfgUsers.postValue(Resource.success(allGfgUsersFromGfgServer))
  
        } catch (e: Exception) {
            gfgUsers.postValue(Resource.error(Aw Snap!", null))
        }
    }
}

Cuando se llama a getUsersWithError() en este caso, se lanzará una excepción, lo que provocará el bloqueo de nuestra aplicación de Android y la finalización de la ejecución de nuestra tarea. En esta ejecución, estamos realizando una ejecución paralela dentro de coroutineScope, que se encuentra dentro del bloque try. Recibiríamos una excepción de getUsersWithError(), y una vez que ocurra la excepción, la ejecución se detendría y la ejecución continuaría con el bloque catch.

GeekTip: cuando ocurre una excepción, la ejecución de la tarea finalizará.

Como resultado, podemos usar supervisorScope en nuestra tarea para superar la falla de ejecución. En supervisorScope, tenemos dos trabajos que se ejecutan simultáneamente, uno de los cuales generará una excepción, pero la tarea aún se completará. En este caso, estamos usando async, que devuelve un Diferido que entregará el resultado más tarde. Entonces, cuando usamos await() para obtener el resultado, usamos el bloque try-catch como una expresión, de modo que si ocurre una excepción, se devuelve una lista vacía, pero la ejecución se completa y obtenemos una lista de usuarios.

Conclusión

  1. Si bien no usamos async, podemos usar try-catch o CoroutineExceptionHandler para lograr lo que queramos en función de nuestros casos de uso.
  2. Cuando usamos async, tenemos dos opciones además de try-catch: coroutineScope y supervisorScope.
  3. Cuando use async, use supervisorScope con try-catch individual para cada tarea además del try-catch de nivel superior si desea continuar con otras tareas si una o más de ellas fallan.
  4. Use coroutineScope con el nivel superior try-catch with async cuando NO desee continuar con otras tareas si alguna de ellas ha fallado.

Publicación traducida automáticamente

Artículo escrito por therebootedcoder y traducido por Barcelona Geeks. The original can be accessed here. Licence: CCBY-SA

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *