Обработка исключений API в RxJava
Я пытаюсь обернуть голову вокруг RxJava в настоящее время, но у меня небольшие проблемы с обработкой исключений сервисного вызова в элегантной манере.
В принципе, у меня есть (дооснащение), которая возвращает Observable<ServiceResponse>
. ServiceResponse
определяется следующим образом:
public class ServiceResponse {
private int status;
private String message;
private JsonElement data;
public JsonElement getData() {
return data;
}
public int getStatus() {
return status;
}
public String getMessage() {
return message;
}
}
Теперь я хочу отобразить этот общий ответ на List<Account>
, содержащийся в поле данных JsonElement (я предполагаю, что вам все равно, как выглядит объект Account
, поэтому я не буду загрязнять сообщение с помощью Это). Следующий код работает очень хорошо для случая успеха, но я не могу найти хороший способ обработать мои исключения API:
service.getAccounts()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.map(new Func1<ServiceResponse, AccountData>() {
@Override
public AccountData call(ServiceResponse serviceResponse) {
// TODO: ick. fix this. there must be a better way...
ResponseTypes responseType = ResponseTypes.from(serviceResponse.getStatus());
switch (responseType) {
case SUCCESS:
Gson gson = new GsonBuilder().create();
return gson.fromJson(serviceResponse.getData(), AccountData.class);
case HOST_UNAVAILABLE:
throw new HostUnavailableException(serviceResponse.getMessage());
case SUSPENDED_USER:
throw new SuspendedUserException(serviceResponse.getMessage());
case SYSTEM_ERROR:
case UNKNOWN:
default:
throw new SystemErrorException(serviceResponse.getMessage());
}
}
})
.map(new Func1<AccountData, List<Account>>() {
@Override
public List<Account> call(AccountData accountData) {
Gson gson = new GsonBuilder().create();
List<Account> res = new ArrayList<Account>();
for (JsonElement account : accountData.getAccounts()) {
res.add(gson.fromJson(account, Account.class));
}
return res;
}
})
.subscribe(accountsRequest);
Есть ли лучший способ сделать это? Эта работает, onError загорится моему наблюдателю, и я получу ошибку, которую я бросил, но это определенно не похоже, что я делаю это правильно.
Спасибо заранее!
Edit:
Позвольте мне уточнить, чего я хочу достичь:
Я хочу иметь класс, который можно вызвать из пользовательского интерфейса (например, Activity или Fragment или что-то еще). Этот класс примет Observer<List<Account>>
как параметр, например:
public Subscription loadAccounts(Observer<List<Account>> observer, boolean forceRefresh) {
...
}
Этот метод вернет подписку, которая может быть отписана при отключении/уничтожении пользовательского интерфейса /etc.
Параметрированный наблюдатель будет обрабатывать onNext для успешных ответов, проходящих в списке учетных записей. OnError будет обрабатывать любые исключения, но также получит любые исключения API (например, если статус ответа!= 200, мы создадим Throwable и передадим его onError). В идеале я не хочу просто "бросать" Исключение, я хочу передать его непосредственно Наблюдателю. Это то, что делают все примеры, которые я вижу.
Усложнение заключается в том, что моя служба Retrofit возвращает объект ServiceResponse
, поэтому мой наблюдатель не может подписаться на это. Самое лучшее, что я придумал, - создать обсервер Observer вокруг моего наблюдателя, например:
@Singleton
public class AccountsDatabase {
private AccountsService service;
private List<Account> accountsCache = null;
private PublishSubject<ServiceResponse> accountsRequest = null;
@Inject
public AccountsDatabase(AccountsService service) {
this.service = service;
}
public Subscription loadAccounts(Observer<List<Account>> observer, boolean forceRefresh) {
ObserverWrapper observerWrapper = new ObserverWrapper(observer);
if (accountsCache != null) {
// We have a cached value. Emit it immediately.
observer.onNext(accountsCache);
}
if (accountsRequest != null) {
// There an in-flight network request for this section already. Join it.
return accountsRequest.subscribe(observerWrapper);
}
if (accountsCache != null && !forceRefresh) {
// We had a cached value and don't want to force a refresh on the data. Just
// return an empty subscription
observer.onCompleted();
return Subscriptions.empty();
}
accountsRequest = PublishSubject.create();
accountsRequest.subscribe(new ObserverWrapper(new EndObserver<List<Account>>() {
@Override
public void onNext(List<Account> accounts) {
accountsCache = accounts;
}
@Override
public void onEnd() {
accountsRequest = null;
}
}));
Subscription subscription = accountsRequest.subscribe(observerWrapper);
service.getAccounts()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(accountsRequest);
return subscription;
}
static class ObserverWrapper implements Observer<ServiceResponse> {
private Observer<List<Account>> observer;
public ObserverWrapper(Observer<List<Account>> observer) {
this.observer = observer;
}
@Override
public void onCompleted() {
observer.onCompleted();
}
@Override
public void onError(Throwable e) {
observer.onError(e);
}
@Override
public void onNext(ServiceResponse serviceResponse) {
ResponseTypes responseType = ResponseTypes.from(serviceResponse.getStatus());
switch (responseType) {
case SUCCESS:
Gson gson = new GsonBuilder().create();
AccountData accountData = gson.fromJson(serviceResponse.getData(), AccountData.class);
List<Account> res = new ArrayList<>();
for (JsonElement account : accountData.getAccounts()) {
res.add(gson.fromJson(account, Account.class));
}
observer.onNext(res);
observer.onCompleted();
break;
default:
observer.onError(new ApiException(serviceResponse.getMessage(), responseType));
break;
}
}
}
}
Я все еще чувствую, что я не использую это правильно, хотя. Я определенно не видел, чтобы кто-либо еще использовал ObserverWrapper раньше. Возможно, я не должен использовать RxJava, хотя ребята из SoundCloud и Netflix действительно продали меня на этом в своих презентациях, и я очень хочу его изучить.
Ответы
Ответ 1
Прочтите ниже. Я добавил редактирование.
Совершенно корректно бросать в Action/Func/Observer с RxJava. Исключение будет распространяться по структуре вплоть до вашего наблюдателя.
Если вы ограничиваете себя вызовом onError, тогда вы будете скручиваться, чтобы это произошло.
С учетом сказанного предложение состоит в том, чтобы просто удалить эту оболочку и добавить простую проверку
Действие в цепочке service.getAccount... Observables.
Я бы использовал doOnNext (новый ValidateServiceResponseOrThrow), прикованный цепью с картой (новый MapValidResponseToAccountList). Это простые классы, которые реализуют необходимый код, чтобы сохранить цепочку Observable более читаемой.
Здесь ваш метод loadAccount упрощен, используя то, что я предложил.
public Subscription loadAccounts(Observer<List<Account>> observer, boolean forceRefresh) {
if (accountsCache != null) {
// We have a cached value. Emit it immediately.
observer.onNext(accountsCache);
}
if (accountsRequest != null) {
// There an in-flight network request for this section already. Join it.
return accountsRequest.subscribe(observer);
}
if (accountsCache != null && !forceRefresh) {
// We had a cached value and don't want to force a refresh on the data. Just
// return an empty subscription
observer.onCompleted();
return Subscriptions.empty();
}
accountsRequest = PublishSubject.create();
accountsRequest.subscribe(new EndObserver<List<Account>>() {
@Override
public void onNext(List<Account> accounts) {
accountsCache = accounts;
}
@Override
public void onEnd() {
accountsRequest = null;
}
});
Subscription subscription = accountsRequest.subscribe(observer);
service.getAccounts()
.doOnNext(new ValidateServiceResponseOrThrow())
.map(new MapValidResponseToAccountList())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(accountsRequest);
return subscription;
}
private static class ValidateResponseOrThrow implements Action1<ServiceResponse> {
@Override
public void call(ServiceResponse response) {
ResponseTypes responseType = ResponseTypes.from(serviceResponse.getStatus());
if (responseType != SUCCESS)
throw new ApiException(serviceResponse.getMessage(), responseType));
}
}
private static class MapValidResponseToAccountList implements Func1<ServiceResponse, List<Account>> {
@Override
public Message call(ServiceResponse response) {
// add code here to map the ServiceResponse into the List<Accounts> as you've provided already
}
}
Edit:
Если кто-то не говорит иначе, я думаю, что лучше всего возвращать ошибки с помощью flatMap.
Я выбросил Исключения из действия в прошлом, но я не считаю это рекомендуемым способом.
У вас будет более чистый стек Exception, если вы используете flatMap. Если вы выбросите изнутри Action Exception stack
на самом деле будет содержать rx.exceptions.OnErrorThrowable$OnNextValue
Исключение, которое не является идеальным.
Позвольте мне продемонстрировать пример выше, используя вместо этого flatMap.
private static class ValidateServiceResponse implements rx.functions.Func1<ServiceResponse, Observable<ServiceResponse>> {
@Override
public Observable<ServiceResponse> call(ServiceResponse response) {
ResponseTypes responseType = ResponseTypes.from(serviceResponse.getStatus());
if (responseType != SUCCESS)
return Observable.error(new ApiException(serviceResponse.getMessage(), responseType));
return Observable.just(response);
}
}
service.getAccounts()
.flatMap(new ValidateServiceResponse())
.map(new MapValidResponseToAccountList())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(accountsRequest);
Как вы видите, разница тонкая. ValidateServiceResponse
теперь реализует Func1
вместо Action1
, и мы больше не используем ключевое слово throw
. Вместо этого мы используем Observable.error(new Throwable)
. Я считаю, что это лучше подходит для ожидаемого контракта Rx.
Ответ 2
Вы можете прочитать эту хорошую статью об обработке ошибок http://blog.danlew.net/2015/12/08/error-handling-in-rxjava/