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

2

7

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?

Adventurer answered 4/12, 2018 at 14:29 Comment(2)
Is RxJava2 allowed in your project? Or would you prefer a non-rx solution? – Fyn
I'm using RxJava2, so yes, it's allowed. Any good idea πŸ’‘ ? – Adventurer
F
3

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.

Fyn answered 10/12, 2018 at 16:28 Comment(0)
T
1

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

Thiele answered 4/12, 2018 at 18:14 Comment(8)
Thanks for the comment. First step is that I have callback in Repository layer, and that callback has 2 methods onResponse() and onFailure(). From both methods I need to have logic for returning error, and I need to do that for each call if I'll not have any "centralized callback handling" for any calls in Repository layer. After that Second step will be something like you said - initialisation of some liveData in ViewModel for handling that in UI layer. But also for that there is need for some logic for escaping duplicates. – Adventurer
So, your onResponse() method returns errors too? @HaykNahapetyan – Thiele
yes, in onResponse() method we are receiving response from server, so in that response object there is also errorBody, which is the errors from the server (e.g. invalid credentials, insufficient balance etc. ) . So that kind of errors are going to be handled in the same way for the all calls, so I want to do it in one place too. – Adventurer
Correct me if I wrong but, if your api returns 404, 403 etc. retrofit returns onFaillure() callback. I was't need to listen errors on onReponse() method while i'm using retrofit. Did you try that? – Thiele
I'll put comment from the source code of onFaillure() ** * Invoked when a network exception occurred talking to the server or when an unexpected * exception occurred creating the request or processing the response. */ So api calls like 404, 403 I'm receiving in onResponse() method. – Adventurer
You may create a class for error transporting which has 2 variables error type and error message. You can create and send to livedata that class in both onResponse and onFaillure – Thiele
In that case I should do that staff (error transportation with some ErrorTransporter class) for every response. So that's the think I'm trying to avoid. I don't want to write a lot of same code – Adventurer
So, if you want to handle that errors in one callback, maybe you can use retrofit with rxJava and disposable. Take a look at that medium.com/@ibrahimsn98/… gist.github.com/ibrahimsn98/… – Thiele

© 2022 - 2024 β€” McMap. All rights reserved.