Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the best practice to handle errors from api endpoints in MVVM architecture?

My goal (and also question) is to do, let's say, centralized error handling. For most of cases errors for each API endpoint is going to be handled in the same way, so I don't want to have duplicates or a lot of if else statements.

My application's architecture corresponds to the one described in developer.android.com

enter image description here

So, that means that I should pass errors from repo via viewModel to UI layer (Activity/Fragment), in order to do UI changes from that layer.

Some little parts from my code:

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);
                    }
                });

Let's say we need to show Toast for every onFailure(...) method call or when errorBody will not be null in onResponse(...) method for every api call.

So, what will be suggestions to have "centralized" error handling meanwhile keeping architecture as it is now?

like image 805
Hayk Nahapetyan Avatar asked Dec 04 '18 14:12

Hayk Nahapetyan


People also ask

How do you handle errors in API?

The simplest way we handle errors is to respond with an appropriate status code. Here are some common response codes: 400 Bad Request – client sent an invalid request, such as lacking required request body or parameter. 401 Unauthorized – client failed to authenticate with the server.

What is error write the technique for handling error?

Error handling helps in handling both hardware and software errors gracefully and helps execution to resume when interrupted. When it comes to error handling in software, either the programmer develops the necessary codes to handle errors or makes use of software tools to handle the errors.

How does react handle API errors?

Error handling with Error Boundaries — For class components. Error boundaries are the most straightforward and effective way to handle errors that occur within your React components. You can create an error boundary component by including the life cycle method componentDidCatch(error, info) if you use class component.

What are the suggested fields and formats needed in an HTTP error response?

Error responses MUST use standard HTTP status codes in the 400 or 500 range to detail the general category of error. Error responses will be of the Content-Type application/problem, appending a serialization format of either json or xml: application/problem+json, application/problem+xml.


2 Answers

Generic Retrofit Callback

To pass the repo layer errors to the UI, you could wrap the model class together with an error into a generic combined model like this:

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;
    }
}

In a separate helper class, we define a generic Retrofit callback that processes errors, and converts the API result to a Resource.

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
    }
}

You can use this generic Retrofit callback in all places you call an API in your repository layer. E.g.:

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;
    }
}

Decorating the Observer

Now you have a generic way to use your Retrofit API, and you have LiveData that wraps models and errors. This LiveData arrives to the UI layer from the ViewModel. Now we decorate the observer of the live data with generic error handling.

First we define an ErrorView interface that can be implemented however you want to show your errors to the user.

interface ErrorView {
    void showError(String message);
}

This can be implemented by showing a Toast message, but you could freely implement the ErrorView with your Fragment and do whatever you want with the error message on your fragment. We use a separate class so that the same class can be used in every Fragment (using composition instead of inheritance as a best practice).

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();
    }
}

Now we implement the observer decorator, that wraps a decorated observer and decorates it with error handling, calling the ErrorView in case of error.

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());
            }

        };
    }
}

In your fragment, you use the observer decorator like this:

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. See this for a more detailed Resource implementation combining the API with a local data source.

like image 183
Janos Breuer Avatar answered Oct 19 '22 22:10

Janos Breuer


I think best solution is creating a livedata object in the viewmodel to pass errors. Than you can observe that errors in anywhere.

like image 1
melianor Avatar answered Oct 19 '22 21:10

melianor