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.