Обработка исключений 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.