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
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?
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.
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.
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.
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.
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;
}
}
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.
I think best solution is creating a livedata object in the viewmodel to pass errors. Than you can observe that errors in anywhere.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With