# Gestión de Errores
# ¿Qué es un error?
¿Qué es un error en una aplicación? Un error es un resultado no deseado por el usuario pero contemplado por el programador. Suele estar relacionado con la lógica de la aplicación.
Por ejemplo, el usuario espera recibir un listado de Superhéroes de una API pero el servidor está caído y devuelve un error 404, el programador ha implementado código para capturar ese error y mostrar una interfaz agradable al usuario.
# ¿Qué es una excepción?
Una excepción es un error no esperado ni por el usuario ni por el programador. Son errores no contemplados.
Por ejemplo, el usuario espera recibir un listado de Superhéroes de una API, el resultado esperado es un listado vacío
o un listado con SuperHéroes. Backend devuelve un null
, algo no contemplado. La aplicación lanza una excepción
porque no se ha desarrollado código para contemplar esa situación.
# Cómo gestionar los errores
Hay muchas formas de gestionar los errores y depende de la decisión del equipo o del proyecto. En PMDM los errores se van a gestionar con Either.
Either es un concepto de programación funcional y es un operador que permite tratar dos posibles soluciones:
Either<Left, Right>
.
En el caso de la gestión de errores, las dos posibles respuestas a tratar son: Either<Error, Success>
. Esto quiere
decir que si el resultado obtenido es un error se devuelve en el parámetro izquierdo y si el resultado obtenido es
correcto se devuelve en el parámetro derecho.
Existen muchas implementaciones por internet sobre este operador, en PMDM se va a usar esta implementación: Either.kt.
Ejemplo de una operación con un resultado correcto:
Código sin gestión de errores:
override fun getAppearance(heroId: Int): Appearance? {
val jsonAppearance = sharedPreferences.getString(heroId.toString(), null)
return jsonAppearance?.let {
gson.fromJson(it, Appearance::class.java)
}
}
2
3
4
5
6
Código con Either: Si no se encuentra el appearance es que existe un error.
override fun getAppearance(heroId: Int): Either<ErrorApp, Appearance> {
val jsonAppearance = sharedPreferences.getString(heroId.toString(), null)
return if (jsonAppearance == null) {
Either.Left(ErrorApp.DataError())
} else {
val appearance: Appearance = gson.fromJson(jsonAppearance, Appearance::class.java)
Either.Right(appearance)
}
}
2
3
4
5
6
7
8
9
# Operadores para crear una respuesta con Either
- Either.Left(E: ErrorApp)
- Either.Right(S: Object)
- ErrorApp.DataErrorApp().left(): left es una función de extensión que sustituye el Either.Left(...)
- Object.right(): right es una función de extensión que sustituye el Either.Right(...)
# Operadores para la gestión de una respuesta con Either
- responseEither.get() devuelve el valor de success
override fun getAppearance(superHeroId: Int): Either<ErrorApp, Appearance> {
val response = localDataSource.getAppearance(superHeroId)
val appearance = response.get()
return appearance.right()
}
2
3
4
5
- responseEither.isLeft() o responseEither.isRight() pregunta por cada una de las respuestas obenidas.
override fun getAppearance(superHeroId: Int): Either<ErrorApp, Appearance> {
val response = localDataSource.getAppearance(superHeroId)
return if (response.isLeft()){
// response es ErrorApp
} else{
// response es Appearance
}
}
2
3
4
5
6
7
8
- responseEither.map{ ... } permite operar con la respuesta de la derecha y devuelve un Either.Right.
override fun getAppearance(superHeroId: Int): Either<ErrorApp, Appearance> {
val response = localDataSource.getAppearance(superHeroId)
return response.map {
//bloque de success
appearance
}
}
2
3
4
5
6
7
- responseEither.mapLef{ ... } permite operar con la repuesta de la izquierda y tiene que devolver un Either.Left.
override fun getAppearance(superHeroId: Int): Either<ErrorApp, Appearance> {
val response = localDataSource.getAppearance(superHeroId)
return response.mapLeft {
//bloque de error, lo que se devuelva aquí tiene que ser un ErrorApp.
}
}
2
3
4
5
6
- reponseEither.fold{ {...Left...}, {...Right...} } trata las dos respuestas en bloques. Fold devuelve los objetos ErrorApp y Success según el bloque a tratar.
override fun getAppearance(superHeroId: Int): Either<ErrorApp, Appearance> {
val response = localDataSource.getAppearance(superHeroId)
response.fold({ errorApp ->
//trato ErrorApp
errorApp
}, { appearance ->
appearance
})
}
2
3
4
5
6
7
8
9
- reponseEither.fold{ {...Left...}, {...Right...} } trata las dos respuestas en bloques. Fold devuelve los objetos ErrorApp y Success según el bloque a tratar.
override fun getAppearance(superHeroId: Int): Either<ErrorApp, Appearance> {
val response = localDataSource.getAppearance(superHeroId)
response.fold({ errorApp ->
//trato ErrorApp
errorApp
}, { appearance ->
appearance
})
}
2
3
4
5
6
7
8
9
- responseEither.bimap{ {...Left...}, {...Right...} } trata las dos respuestas en bloques. Bimap devuelve los objetos envueltos en un Either con ErrorApp o Success según el bloque a tratar.
override fun getAppearance(superHeroId: Int): Either<ErrorApp, Appearance> {
val response = localDataSource.getAppearance(superHeroId)
val appearance = response.get()
return response.bimap({
it -> devuelve Either.Left(it) donde it es el ErrorAoo
}, {
it --> deuelve Either.Right(it) donde it es el objeto Appearance
})
}
2
3
4
5
6
7
8
9
- responseEither.flatMap: permite devolver otro Either diferente al obtenido en el flatMap.
override fun getAppearance(superHeroId: Int): Either<ErrorApp, Appearance> {
val response: Either<ErrorApp, Biography> = Biography("realName", "alig").right()
return response.flatMap {
localDataSource.getAppearance(1) //devuelve Either<ErrorApp, Appearance>
}
}
2
3
4
5
6
- responseEither.getOrNull: permite obtener el valor de success o null en caso de ser por la izquierda.
override fun getAppearance(superHeroId: Int): Either<ErrorApp, Appearance> {
if (localDataSource.getAppearance(superHeroId).getOrNull() == null) {
}
}
2
3
4
5
- reponseEither. getOrElse: permite obtener el valor de success o ejecuta un bloque de código en caso de ser por la izquierda. La función devuelve los objetos planos, se necesita wrappear a Either.
override fun getAppearance(superHeroId: Int): Either<ErrorApp, Appearance> {
return localDataSource.getAppearance(superHeroId).getOrElse {
val appearance = remoteDataSource.getAppearance(superHeroId)!!
localDataSource.save(superHeroId, appearance)
appearance
}.right()
}
2
3
4
5
6
7
# Cómo gestionar las excepciones
Las excepciones no deben ser ocultadas. Si se oculta una excepción, el programador nunca sabrá que se esán produciendo. Para las excepciones se recomienda usar un log de excepciones con un sistema de captura para informar a los programadores y diseñar una pantalla amigable en el caso de que se lanza una excepción.
No hace falta capturarla, cuando salte en el ViewModel se avisa a la vista que muestra la pantalla de error.