Respostas/Erros com Retrofit 2 e RxJava2
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.
Conteudo
TL;DR
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.
ViewModels e Subjects
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.
Errors!
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