Как лучше всего обрабатывать ошибки конечных точек api в архитектуре MVVM?

Моя цель (а также вопрос) - это, скажем, централизованная обработка ошибок. В большинстве случаев ошибки для каждой конечной точки API будут обрабатываться одинаково, поэтому я не хочу иметь дубликаты или много операторов if else.

Архитектура моего приложения соответствует описанной в developer.android.com

Как лучше всего обрабатывать ошибки конечных точек api в архитектуре MVVM?

Итак, это означает, что я должен передавать ошибки из repo через viewModel в UI layer (Activity/Fragment), чтобы вносить изменения пользовательского интерфейса с этого уровня.

Некоторые маленькие части моего кода:

myService.initiateLogin("Basic " + base64, authBody)
                .enqueue(new Callback<UserTokenModel>() {
                    @Override
                    public void onResponse(Call<UserTokenModel> call, Response<UserTokenModel> response) {
                        userTokenModelMutableLiveData.setValue(response.body());
                    }

                    @Override
                    public void onFailure(Call<UserTokenModel> call, Throwable t) {
                        // TODO better error handling in feature ...
                        userTokenModelMutableLiveData.setValue(null);
                    }
                });

Допустим, нам нужно показывать Toast для каждого вызова метода onFailure(...) или когда errorBody не будет null в методе onResponse(...) для каждого вызова API.

Итак, каковы будут предложения по «централизованной» обработке ошибок при сохранении архитектуры такой, какая она есть сейчас?

Разрешен ли в вашем проекте RxJava2? Или вы бы предпочли решение без rx?

Janos Breuer 09.12.2018 00:46

Я использую RxJava2, так что да, это разрешено. Есть хорошая идея ??

Hayk Nahapetyan 09.12.2018 13:37
6
2
2 497
2

Ответы 2

Я думаю, что лучшим решением является создание объекта Liveata в модели просмотра для передачи ошибок. Затем вы можете наблюдать ошибки в любом месте.

Спасибо за комментарий. Первый шаг - у меня есть обратный вызов на уровне репозитория, и этот обратный вызов имеет 2 метода onResponse () и onFailure (). Из обоих методов мне нужна логика для возврата ошибки, и мне нужно делать это для каждого вызова, если у меня не будет «централизованной обработки обратного вызова» для каких-либо вызовов на уровне репозитория. После этого второго шага будет что-то вроде того, что вы сказали - инициализация некоторых liveData в ViewModel для обработки их на уровне пользовательского интерфейса. Но также для этого нужна некоторая логика для избежания дубликатов.

Hayk Nahapetyan 04.12.2018 20:10

Итак, ваш метод onResponse () тоже возвращает ошибки? @HaykNahapetyan

melianor 04.12.2018 20:14

да, в методе onResponse () мы получаем ответ от сервера, поэтому в этом объекте ответа также есть errorBody, которая представляет собой ошибки с сервера (например, неверные учетные данные, недостаточный баланс и т. д.). Так что подобные ошибки будут обрабатываться одинаково для всех вызовов, поэтому я тоже хочу сделать это в одном месте.

Hayk Nahapetyan 04.12.2018 20:54

Поправьте меня, если я ошибаюсь, но, если ваш api возвращает 404, 403 и т. д., Модернизация возвращает обратный вызов onFaillure (). Мне не нужно было прослушивать ошибки в методе onReponse (), пока я использую дооснащение. Вы пробовали это?

melianor 04.12.2018 20:59

Я помещу комментарий из исходного кода onFaillure () ** *. Вызывается, когда сетевая исключительная ситуация возникает при разговоре с сервером или когда возникает неожиданное * исключение, создавшее запрос или обрабатывающее ответ. * / Итак, вызовы api типа 404, 403 я получаю в методе onResponse ().

Hayk Nahapetyan 05.12.2018 07:12

Вы можете создать класс для передачи ошибок, который имеет тип ошибки 2 переменных и сообщение об ошибке. Вы можете создавать и отправлять в liveata этот класс как в onResponse, так и в onFaillure.

melianor 05.12.2018 08:50

В этом случае я должен делать этот штат (перенос ошибок с помощью некоторого класса ErrorTransporter) для каждого ответа. Так что это мысль, которую я пытаюсь избежать. Я не хочу писать много одного и того же кода

Hayk Nahapetyan 05.12.2018 09:43

Итак, если вы хотите обработать эти ошибки за один обратный вызов, возможно, вы можете использовать дооснащение с помощью rxJava и одноразового использования. Взгляните на этот medium.com/@ibrahimsn98/…gist.github.com/ibrahimsn98/…

melianor 05.12.2018 14:48

Общий обратный вызов для модернизации

Чтобы передать ошибки уровня репо в пользовательский интерфейс, вы можете обернуть класс модели вместе с ошибкой в ​​общую комбинированную модель следующим образом:

class Resource<T> {

    @Nullable private final T data;
    @Nullable private final Throwable error;

    private Resource(@Nullable T data, @Nullable Throwable error) {
        this.data = data;
        this.error = error;
    }

    public static <T> Resource<T> success(@NonNull T data) {
        return new Resource<>(data, null);
    }

    public static <T> Resource<T> error(@NonNull Throwable error) {
        return new Resource<>(null, error);
    }

    @Nullable
    public T getData() {
        return data;
    }

    @Nullable
    public Throwable getError() {
        return error;
    }
}

В отдельном вспомогательном классе мы определяем общий обратный вызов Retrofit, который обрабатывает ошибки и преобразует результат API в ресурс.

class ResourceCallback {

    public static <T> Callback<T> forLiveData(MutableLiveData<Resource<T>> target) {

        return new Callback<T>() {
            @Override
            public void onResponse(Call<T> call, Response<T> response) {
                if (!response.isSuccessful() || response.body() == null) {
                    target.setValue(Resource.error(convertUnsuccessfulResponseToException(response)));
                } else {
                    target.setValue(Resource.success(response.body()));
                }
            }

            @Override
            public void onFailure(Call<T> call, Throwable t) {
                // You could examine 't' here, and wrap or convert it to your domain specific exception class.
                target.setValue(Resource.error(t));
            }
        };

    }

    private static <T> Throwable convertUnsuccessfulResponseToException(Response<T> response) {
        // You could examine the response here, and convert it to your domain specific exception class.
        // You can use
        response.errorBody();
        response.code();
        response.headers();
        // etc...

        return new LoginFailedForSpecificReasonException(); // This is an example for a failed login
    }
}

Вы можете использовать этот общий обратный вызов Retrofit во всех местах, где вы вызываете API на уровне репозитория. Например.:

class AuthenticationRepository {

    // ...

    LiveData<Resource<UserTokenModel>> login(String[] params) {

        MutableLiveData<Resource<UserTokenModel>> result = new MutableLiveData<>();
        myService.initiateLogin("Basic " + base64, authBody).enqueue(ResourceCallback.forLiveData(result));

        return result;
    }
}

Украшение наблюдателя

Теперь у вас есть общий способ использования Retrofit API, и у вас есть LiveData, которая объединяет модели и ошибки. Эти LiveData поступают на уровень пользовательского интерфейса из ViewModel. Теперь мы украшаем наблюдателя за живыми данными общей обработкой ошибок.

Сначала мы определяем интерфейс ErrorView, который можно реализовать, как бы вы ни хотели показывать свои ошибки пользователю.

interface ErrorView {
    void showError(String message);
}

Это можно реализовать, показывая сообщение Toast, но вы можете свободно реализовать ErrorView со своим фрагментом и делать все, что хотите, с сообщением об ошибке на своем фрагменте. Мы используем отдельный класс, чтобы один и тот же класс можно было использовать в каждом фрагменте (рекомендуется использовать композицию вместо наследования).

class ToastMessageErrorView implements ErrorView {

    private Context context;

    public ToastMessageErrorView(Context context) {
        this.context = context;
    }

    @Override
    public void showError(String message) {
        Toast.makeText(context, message, Toast.LENGTH_LONG).show();
    }
}

Теперь мы реализуем декоратор наблюдателя, который обертывает декорированного наблюдателя и украшает его обработкой ошибок, вызывая ErrorView в случае ошибки.

class ResourceObserver {

    public static <T> Observer<Resource<T>> decorateWithErrorHandling(Observer<T> decorated, ErrorView errorView) {

        return resource -> {
            Throwable t = resource.getError();
            if (t != null) {
                // Here you should examine 't' and create a specific error message. For simplicity we use getMessage().
                String message = t.getMessage();

                errorView.showError(message);
            } else {
                decorated.onChanged(resource.getData());
            }

        };
    }
}

В вашем фрагменте вы используете декоратор наблюдателя следующим образом:

class MyFragment extends Fragment {

    private MyViewModel viewModel;

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        viewModel.getUserToken().observe(this, ResourceObserver.decorateWithErrorHandling(
                userTokenModel -> { 
                    // Process the model
                }, 
                new ToastMessageErrorView(getActivity())));
    }

}

P.S. См. это для более подробной реализации ресурса, объединяющего API с локальным источником данных.

Другие вопросы по теме