RxJava e tratamento de erros
1. Introdução
Neste artigo, veremos como lidar com exceções e erros usando o RxJava.
Primeiro, lembre-se de que o Observable normalmente não gera exceções. Em vez disso, por padrão, Observable chama o método onError () _ do seu _Observer, notificando o observador de que um erro irrecuperável acabou de ocorrer e, em seguida, é encerrado sem chamar mais nenhum dos métodos do seu _Observer’s.
*Os operadores de manipulação de erros que estamos prestes a introduzir alteram o comportamento padrão ao retomar ou tentar novamente a sequência _Observable_.
===* 2. Dependências do Maven *
Primeiro, vamos adicionar o RxJava no pom.xml:
<dependency>
<groupId>io.reactivex.rxjava2</groupId>
<artifactId>rxjava</artifactId>
<version>2.1.3</version>
</dependency>
A versão mais recente do artefato pode ser encontrada https://search.maven.org/classic/#search%7Cga%7C1%7Cg%3A%22io.reactivex.rxjava2%22%20AND%20a%3A%22rxjava%22 [ aqui].
===* 3. Manipulação de erros *
Quando ocorre um erro, geralmente precisamos lidar com isso de alguma maneira. Por exemplo, altere estados externos relacionados, continuando a sequência com resultados padrão ou simplesmente deixe-o para que o erro possa se propagar.
====* 3.1 Ação em caso de erro *
Com doOnError, podemos invocar qualquer ação necessária quando houver um erro:
@Test
public void whenChangeStateOnError_thenErrorThrown() {
TestObserver testObserver = new TestObserver();
AtomicBoolean state = new AtomicBoolean(false);
Observable
.error(UNKNOWN_ERROR)
.doOnError(throwable -> state.set(true))
.subscribe(testObserver);
testObserver.assertError(UNKNOWN_ERROR);
testObserver.assertNotComplete();
testObserver.assertNoValues();
assertTrue("state should be changed", state.get());
}
No caso de uma exceção ser lançada durante a execução da ação, o RxJava quebra a exceção em CompositeException:
@Test
public void whenExceptionOccurOnError_thenCompositeExceptionThrown() {
TestObserver testObserver = new TestObserver();
Observable
.error(UNKNOWN_ERROR)
.doOnError(throwable -> {
throw new RuntimeException("unexcepted");
})
.subscribe(testObserver);
testObserver.assertError(CompositeException.class);
testObserver.assertNotComplete();
testObserver.assertNoValues();
}
====* 3.2 Continuar com itens padrão *
Embora possamos invocar ações com doOnError, o erro ainda interrompe o fluxo de sequência padrão. Às vezes, queremos retomar a sequência com uma opção padrão, é o que onErrorReturnItem:
@Test
public void whenHandleOnErrorResumeItem_thenResumed(){
TestObserver testObserver = new TestObserver();
Observable
.error(UNKNOWN_ERROR)
.onErrorReturnItem("singleValue")
.subscribe(testObserver);
testObserver.assertNoErrors();
testObserver.assertComplete();
testObserver.assertValueCount(1);
testObserver.assertValue("singleValue");
}
Se um fornecedor de item padrão dinâmico for preferido, podemos usar o onErrorReturn :
@Test
public void whenHandleOnErrorReturn_thenResumed() {
TestObserver testObserver = new TestObserver();
Observable
.error(UNKNOWN_ERROR)
.onErrorReturn(Throwable::getMessage)
.subscribe(testObserver);
testObserver.assertNoErrors();
testObserver.assertComplete();
testObserver.assertValueCount(1);
testObserver.assertValue("unknown error");
}
====* 3.3 Continuar com outra sequência *
Em vez de retornar a um único item, podemos fornecer a sequência de dados de fallback usando onErrorResumeNext ao encontrar erros. Isso ajudaria a impedir a propagação de erros:
@Test
public void whenHandleOnErrorResume_thenResumed() {
TestObserver testObserver = new TestObserver();
Observable
.error(UNKNOWN_ERROR)
.onErrorResumeNext(Observable.just("one", "two"))
.subscribe(testObserver);
testObserver.assertNoErrors();
testObserver.assertComplete();
testObserver.assertValueCount(2);
testObserver.assertValues("one", "two");
}
Se a sequência de fallback diferir de acordo com os tipos de exceção específicos, ou a sequência precisar ser gerada por uma função, podemos passar a função para o _onErrorResumeNext: _
@Test
public void whenHandleOnErrorResumeFunc_thenResumed() {
TestObserver testObserver = new TestObserver();
Observable
.error(UNKNOWN_ERROR)
.onErrorResumeNext(throwable -> Observable
.just(throwable.getMessage(), "nextValue"))
.subscribe(testObserver);
testObserver.assertNoErrors();
testObserver.assertComplete();
testObserver.assertValueCount(2);
testObserver.assertValues("unknown error", "nextValue");
}
====* 3.4 Manipular Exceção * Apenas
O RxJava também fornece um método de fallback que permite continuar a sequência com um Observable fornecido quando uma exceção (mas nenhum erro) é gerada:
@Test
public void whenHandleOnException_thenResumed() {
TestObserver testObserver = new TestObserver();
Observable
.error(UNKNOWN_EXCEPTION)
.onExceptionResumeNext(Observable.just("exceptionResumed"))
.subscribe(testObserver);
testObserver.assertNoErrors();
testObserver.assertComplete();
testObserver.assertValueCount(1);
testObserver.assertValue("exceptionResumed");
}
@Test
public void whenHandleOnException_thenNotResumed() {
TestObserver testObserver = new TestObserver();
Observable
.error(UNKNOWN_ERROR)
.onExceptionResumeNext(Observable.just("exceptionResumed"))
.subscribe(testObserver);
testObserver.assertError(UNKNOWN_ERROR);
testObserver.assertNotComplete();
}
Como mostra o código acima, quando ocorre um erro, o onExceptionResumeNext não entra em ação para retomar a sequência.
*4. Repetir com erro *
A sequência normal pode ser interrompida por uma falha temporária do sistema ou erro de back-end. Nessas situações, queremos tentar novamente e esperar até que a sequência seja corrigida.
Felizmente, o RxJava nos dá opções para fazer exatamente isso.
====* 4.1 Repetir *
Ao usar http://reactivex.io/RxJava/2.x/javadoc/io/reactivex/Observable.html#retry--[retry_ξ,, o _Observable será reinscrito várias vezes até que não haja erro. Mas, na maioria das vezes, preferimos uma quantidade fixa de novas tentativas:
@Test
public void whenRetryOnError_thenRetryConfirmed() {
TestObserver testObserver = new TestObserver();
AtomicInteger atomicCounter = new AtomicInteger(0);
Observable
.error(() -> {
atomicCounter.incrementAndGet();
return UNKNOWN_ERROR;
})
.retry(1)
.subscribe(testObserver);
testObserver.assertError(UNKNOWN_ERROR);
testObserver.assertNotComplete();
testObserver.assertNoValues();
assertTrue("should try twice", atomicCounter.get() == 2);
}
====* 4.2 Repetir sob condição *
A tentativa condicional também é viável no RxJava, usando retry com predicados ou usando _http://reactivex.io/RxJava/2.x/javadoc/io/reactivex/Observable.html#retryUntil-io.reactivex.functions.BooleanSupplier- [retryUntil] _:
@Test
public void whenRetryConditionallyOnError_thenRetryConfirmed() {
TestObserver testObserver = new TestObserver();
AtomicInteger atomicCounter = new AtomicInteger(0);
Observable
.error(() -> {
atomicCounter.incrementAndGet();
return UNKNOWN_ERROR;
})
.retry((integer, throwable) -> integer < 4)
.subscribe(testObserver);
testObserver.assertError(UNKNOWN_ERROR);
testObserver.assertNotComplete();
testObserver.assertNoValues();
assertTrue("should call 4 times", atomicCounter.get() == 4);
}
@Test
public void whenRetryUntilOnError_thenRetryConfirmed() {
TestObserver testObserver = new TestObserver();
AtomicInteger atomicCounter = new AtomicInteger(0);
Observable
.error(UNKNOWN_ERROR)
.retryUntil(() -> atomicCounter.incrementAndGet() > 3)
.subscribe(testObserver);
testObserver.assertError(UNKNOWN_ERROR);
testObserver.assertNotComplete();
testObserver.assertNoValues();
assertTrue("should call 4 times", atomicCounter.get() == 4);
}
====* 4.3 RetryWhen *
Além dessas opções básicas, há também um método interessante de nova tentativa: retryWhen.
Isso retorna um Observable, _ diga _ “NewO”, _ que emite os mesmos valores da origem _ObservableSource