web-dev-qa-db-fra.com

Coroutines et Retrofit, le meilleur moyen de gérer les erreurs

Après avoir lu ce numéro Comment gérer les exceptions et ce support Mise en réseau Android en 2019 - Rénovation avec les coroutines de Kotlin J'ai créé ma solution qui consiste en un BaseService capable d'effectuer l'appel de retrofit et de retransmettre les résultats et exceptions le long de la "chaîne":

[~ # ~] api [~ # ~]

@GET("...")
suspend fun fetchMyObject(): Response<List<MyObject>>

BaseService

protected suspend fun <T : Any> apiCall(call: suspend () -> Response<T>): Result<T> {
    val response: Response<T>
    try {
        response = call.invoke()
    } catch (t: Throwable) {
        return Result.Error(mapNetworkThrowable(t))
    }
    if (!response.isSuccessful) {
        val responseErrorBody = response.errorBody()
        if (responseErrorBody != null) {
            //try to parse to a custom ErrorObject
            ...
            return Result.Error...
        }
        return Result.Error(mapHttpThrowable(Exception(), response.raw().code, response.raw().message))
    }
    return Result.Success(response.body()!!)
}

ChildService

suspend fun fetchMyObject(): Result<List<MyObject>> {
    return apiCall(call = { api.fetchMyObject() })
}

Repo

    suspend fun myObjectList(): List<MyObject> {
        return withContext(Dispatchers.IO) {
            when (val result = service.fetchMyObject()) {
                is Result.Success -> result.data
                is Result.Error -> throw result.exception
            }
        }
    }

Remarque: nous avons parfois besoin de plus que de lancer une exception ou un type de résultat de réussite. Pour gérer ces situations, voici comment nous pouvons y parvenir:

sealed class SomeApiResult<out T : Any> {
    object Success : SomeApiResult<Unit>()
    object NoAccount : SomeApiResult<Unit>()
    sealed class Error(val exception: Exception) : SomeApiResult<Nothing>() {
        class Generic(exception: Exception) : Error(exception)
        class Error1(exception: Exception) : Error(exception)
        class Error2(exception: Exception) : Error(exception)
        class Error3(exception: Exception) : Error(exception)
    }
} 

Et puis dans notre ViewModel:

when (result: SomeApiResult) {
    is SomeApiResult.Success -> {...}
    is SomeApiResult.NoAccount -> {...}
    is SomeApiResult.Error.Error1 -> {...}
    is SomeApiResult.Error -> {/*all other*/...}
}

En savoir plus sur cette approche ici .

BaseViewModel

protected suspend fun <T : Any> safeCall(call: suspend () -> T): T? {
    try {
        return call()
    } catch (e: Throwable) {
        parseError(e)
    }
    return null
}

ChildViewModel

fun fetchMyObjectList() {
    viewModelScope.launch {
        safeCall(call = {
            repo.myObjectList()
            //update ui, etc..
        })
    }
}

Je pense que le ViewModel (ou un BaseViewModel) devrait être la couche gérant les exceptions, car dans cette couche se trouve la logique de décision de l'interface utilisateur, par exemple, si nous voulons simplement montrer un toast, ignorer un type d'exception, appeler une autre fonction etc ...

Qu'est-ce que tu penses?

EDIT: J'ai créé un medium avec ce sujet

5
GuilhE

Je suggérerais de gérer les exceptions dans le référentiel et de renvoyer une seule réponse sous la forme d'un objet de classe scellé à viewmodel. Garder cela dans le référentiel rend le référentiel comme source unique de vérité et votre code plus propre et plus lisible.

1
Ashok Kumar