Respostas/Erros com Retrofit 2 e RxJava2

Tempo de leitura: 6 minutes

Por um tempo, não estou completamente satisfeito com a forma como estou lidando com os erros de REST Api. Parecia confuso e confuso; nunca sabendo completamente como lidar com os erros de uma forma limpa e concisa com muito poucas soluções documentadas.

Foi quando me deparei com uma base de código que continha uma boa solução usando RxJava. Não vou mentir, demorei algumas horas para entender o que estava acontecendo, mas no final eu fiquei do outro lado mais confortável com RxJava, então acho que vale a pena tentar entender o código de outras pessoas mesmo que a princípio pareça excessivamente complexo.

RxJava é legal. Kotlin é incrível. O retrofit é fantástico.

 

Chamada Api de Retrofit Simples

Você pode ter feito isso muitas vezes com o Retrofit; definir uma chamada de API que retorna um Observable. Isso pode parecer assim em sua classe de interface de API – ah, a propósito, este exemplo está em Kotlin, embora você possa fazer exatamente a mesma coisa em Java, apenas de uma forma mais inchada e feia.

interface MovieDbApi {

    @GET("movie/popular")
    fun getPopularMovies(@Query("page") page: Int?): Observable<PopularMoviesResponse>

}

Temos nosso endpoint que parece ótimo. Agora precisamos usar este ponto de extremidade.

Eu, como muitas pessoas, gosto de código limpo e de torná-lo o mais testável possível. Por esta razão, eu uso o padrão MVVM ou MVP. Para os fins deste exemplo e o fato de ter sido lançado apenas recentemente, estarei usando o novo componente de arquitetura ViewModel fornecido pelo Google, embora não explicarei em detalhes esse componente.

 

O gatilho

Agora, o cenário é este. Tenho um botão na minha visualização que, quando pressionado, fará uma solicitação de obtenção da API. Como podemos fazer isso de forma reativa?

No meu ViewModel, exponho um Observer que é um PublishSubject assim:

fun getPopularMoviesRefreshObserver() : Observer<Unit> {
    return refreshSubject
}

Então, a partir da minha Activity, chamo esse assunto de onNext() quando quero que seja acionado dessa forma.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    btn_ma_popular_movies.setOnClickListener {
        viewModel.getPopularMoviesRefreshObserver().onNext(Unit)
    }
}

 

Api Call

Em sua forma mais básica, que expandiremos mais tarde, aqui está um Observable que será emitido quando o onNext do refreshSubject for chamado. Como você pode ver, por enquanto estamos limitando apenas a primeira página de resultados.

private val basicApiCall: Observable<List<MovieSummary>>

init {
    basicApiCall = refreshSubject
            .flatMap { return@flatMap movieDbApi.getPopularMovies(1)
                    .subscribeOn(Schedulers.io())
                    .map { it.results } }
}

Portanto, no código acima, estou pegando meu refreshSubject, passando sua emissão para o observável movieDbApi.getPopularMovies() e garantindo que apenas a lista de resultados seja retornada em vez do wrapper de resposta.

Este Observable é recuperado da visualização da seguinte forma:

override fun bindViewModel() {
    viewModel.getPopularMoviesRetrievedObservable()
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeBy(
                onNext = { 
          it.forEach { Log.d("MainActivity","Movie found: $it") } },
                onError = {
          Log.e("MainActivity","Api call error",it)})
}

Perfeito. Estamos recebendo respostas da Api com uma lista de filmes sendo impressos cada vez que o botão é pressionado.

Sem a configuração atual, temos um problema. Se ocorrer um erro, como com o Retrofit lançando uma HttpException ou havendo uma SocketTimeoutException devido a uma conexão de Internet ruim, este observável não funcionará mais! Pressionar o botão após o onError ser chamado não fará com que o basicApiCall emita nenhum item porque seu fluxo foi encerrado.

Poderíamos retornar um novo Observable cada vez que a visualização se vincular ao ViewModel, mas isso não funcionaria para o nosso código mais tarde, então vamos fazê-lo funcionar desta forma. Para fazer isso, envolvemos quaisquer erros lançados por nossa chamada de Api em um objeto.

open class Result<T>( val data: T? = null, val error: Throwable? = null) {
    companion object {
        fun <T> fromData( data: T ) : Result<T> {
            return Result(data, null)
        }

        fun <T> fromError( error : Throwable ) : Result<T> {
            return Result(null, error)
        }
    }

    fun isError() : Boolean{
        return error != null
    }

    fun isSuccess() : Boolean {
        return data != null
    }
}

Este é o objeto de invólucro. Isso pode ser usado para que ainda possamos recuperar nossos erros, mas o observável também continua emitindo itens. Nós o usamos assim:

basicApiCall = refreshSubject
        .flatMap { return movieDbApi.getPopularMovies(1)
                .map { Result.fromData(it.results) }
                .onErrorResumeNext( Function { Observable.just(Result.fromError(it)) })
                .subscribeOn(Schedulers.io())

Observe o mapa no observable getPopularMovies. Este é o melhor caso e se chegar a esse ponto, a chamada da Api foi bem-sucedida. No entanto, se um erro for lançado, é quando o operador onErrorResumeNext entra em ação. Isso basicamente diz, se um lançável for emitido, então retorne este observable.

 

Mas isso parece feio, ouço você dizer

Afinal de contas, este é Kotlin, deve haver uma maneira de fazer com que pareça bom! Verifique esta adição à classe Result. Isto está fora da classe Result, mas dentro do mesmo arquivo.

fun <T> Observable<T>.toResult() : Observable<Result<T>> {
    return map { Result.fromData(it) }
            .onErrorResumeNext( Function { Observable.just(Result.fromError(it)) })
}

Este é um dos meus recursos favoritos no Kotlin; extensões. Ele permite que você estenda classes sobre as quais você normalmente não teria controle e permite que você faça coisas legais como esta.

basicApiCall = refreshSubject
        .flatMap { return@flatMap movieDbApi.getPopularMovies(1)
                .map { it.results }
                .toResult()
                .subscribeOn(Schedulers.io())}

Sim, tivemos que adicionar uma linha para mapear nossa emissão para uma lista de filmes, mas o .toResult() é muito mais agradável de ler do que o exemplo anterior.

Nossa visão então se atualiza assim para mostrar os resultados:

return viewModel.getPopularMoviesRetrievedObservable()
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe {
            it.data?.forEach {
 Log.d("MainActivity","Movie found: $it") }
            it.error?.let{Log.e("MainActivity","Api call error", it)}
        }

 

Começar a trabalhar

Portanto, nossa chamada de Api agora pode ser disparada várias vezes, mesmo após a ocorrência de um erro. No entanto, para lidar com códigos de erro individuais, seria necessário haver muita lógica de negócios na exibição. Agora vamos nos envolver mais com o RxJava!

Portanto, estou feliz com nossa chamada Api básica observável que retorna um Result<List<MovieSummary>. O problema é que quero compartilhar a emissão desses itens com outros observáveis. Para fazer isso, transformamos nosso Observable regular em ConnectableObservable adicionando o parâmetro .publish() ao final.

val basicApiCall 
     = refreshSubject
        .flatMap { return@flatMap movieDbApi.getPopularMovies(1)
                .map { it.results }
                .toResult()
                .subscribeOn(Schedulers.io())}
        .publish()

Este Observable basicApiCall que estamos usando não estará mais acessível ao usuário. Em vez disso, queremos dar acesso à visualização, apenas a Observables específicos que correspondem a certos estados que esperamos. Veja uma resposta bem-sucedida, por exemplo. Sabemos que não haverá exceção e uma lista de objetos MovieSummary será retornada. Vamos criar um Observable apenas para isso.

fun getPopularMoviesRetrievedObservable() : Observable<Result<List<MovieSummary>>> {
    return basicApiCall.filter { it.isSuccess() }
}

Agora podemos garantir que quando este observável disparar, será apenas porque recebeu uma resposta bem-sucedida da Api e a visualização só precisa lidar com este cenário.

return viewModel.getPopularMoviesRetrievedObservable()
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe{ it.data?.forEach { Log.d("MainActivity","Movie found: $it") }

 

Vamos lidar com um erro.

Quando uma página de valor 0 é passada para o endpoint, ela retorna com uma HttpException que tem um código de 424. Esta é uma boa para lidar, pois podemos garantir a resposta a cada vez, no entanto, seus aplicativos do mundo real podem não ser tão úteis .

fun getPopularMoviesPageErrorObservable() : Observable<Unit> {
    return basicApiCall.filter { it.isError() }
            .filter { it.error is HttpException }
            .map { it.error as HttpException }
            .filter { it.code() == 424 }
            .map { Unit }
}

Como você pode ver, estamos compartilhando basicApiCall, ConnectableObservable da mesma forma que o sucesso Observable faz. Este, entretanto, apenas emite um item se uma HttpException com o código 424 for emitida.

Mas espere! Isso é feio! Vamos adicionar mais algumas extensões ao nosso arquivo Result.kt, mas não na classe, para tornar nosso código mais agradável.

fun <T> Observable<Result<T>>.onlySuccess() : Observable<T> {
    return filter { it.isSuccess() }
            .map { it.data!! }
}

fun <T> Observable<Result<T>>.onlyError() : Observable<Throwable> {
    return filter { it.isError() }
            .map { it.error!! }
}

fun <T> Observable<Result<T>>.onlyHttpException() : Observable<HttpException> {
    return filter{ it.isError() && it.error is HttpException}
            .map { it.error as HttpException }
}

fun <T> Observable<Result<T>>.onlyHttpException(code: Int) : Observable<HttpException> {
    return onlyHttpException()
            .filter { it.code() == code }
}

fun <T> Observable<Result<T>>.onlyHttpExceptionExcluding(vararg codes: Int) : Observable<HttpException> {
    return onlyHttpException()
            .filter { codes.contains(it.code()) }
}

Agora, nosso erro de página específico Observable torna-se:

fun getPopularMoviesPageErrorObservable() : Observable<Unit> {
    return basicApiCall.onlyHttpException(424)
            .map { Unit }
}

E nosso Observable de sucesso se torna:

fun getPopularMoviesRetrievedObservable() : Observable<List<MovieSummary>> {
    return basicApiCall.onlySuccess()
}

 

Tratamento genérico de erros

Já tratamos do caso de detectar erros específicos, agora vamos pegar todos os outros!

fun getPopularMoviesGenericErrorObservable() : Observable<Unit> {
    return basicApiCall.onlyHttpExceptionExcluding(424)
            .map { Unit }
}

 

Conectando-se ao View

Já conectamos o observável de sucesso à nossa visão (que agora requer algumas modificações após a adição da extensão), agora vamos ligar os erros.

override fun bindViewModel() {
    viewModel.getPopularMoviesRetrievedObservable()
            .bindToLifecycle(this@MainActivity)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe { it.forEach { Log.d("MainActivity","Movie: $it") } }
    viewModel.getPopularMoviesPageErrorObservable()
            .bindToLifecycle(this@MainActivity)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe { Log.d("MainActivity","Page of 0 error") }
    viewModel.getPopularMoviesGenericErrorObservable()
            .bindToLifecycle(this@MainActivity)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe { Log.d("MainActivity","Something went wrong") }
}

Observe o método bindToLifecycle(). Isso é para que não vazemos nossos descartáveis usando uma biblioteca chamada RxLifecycle.

Assinamos cada observável no encadeamento io para que seja executado em segundo plano e observamos no encadeamento principal para que possamos fazer coisas em nosso Ui quando os itens são emitidos.

Por enquanto, eles estão apenas registrando os diferentes erros e respostas, mas você pode ver como pode conectar isso rapidamente a diferentes partes de sua interface do usuário.

 

Conclusão

Eu prefiro muito mais essa forma de lidar com minhas solicitações de API. Tudo é separado com observáveis claros e distintos para diferentes cenários. A lógica de negócios também está longe de ser vista.

Todo o código desta postagem pode ser encontrado aqui