Flexible way to handle exceptions in Android
Introduction
As your project becomes bigger, you will inevitably face problems of error handling in different parts of your app. Error handling can be tricky — in some cases you should simply show a message to the user, while some errors require to perform navigation to a screen, etc…
The majority of developers use some kind of a structure pattern in the presentation layer of any project — MVP, MVVM, some guys use MVI… In most cases the exceptions which occurred in the data layer should be raised to the presentation layer for appropriate handling and showing message to the user. But it is not true for every case.
What do we expect from the ErrorHandler in our project?
- We need to be able to handle various types of exceptions.
- We want to use it not only in the presenters.
- The code should be reusable.
Although, I will be talking in terms of MVP, but this approach will work for any kind of presentation pattern. And one more thing: it will work even at iOS
DIRTY IMPLEMENTATION
So, you are using MVP. I am pretty sure that most of you:
1. Use some kind of an MVP library for reducing boilerplate code(Moxy, Mosby, EasyMvp and etc.)
2. Have a BasePresenter that looks like this :
It also may contain code for attaching and detaching views and some code for surviving orientation changes. But in my case MvpPresenter will do it for me.
So I am with you, guys. =)
How does specific implementation of a BasePresenter actually look like?
Good. Let’s imagine, that you are not getting particular messages in error responses from backend and you have to handle it on the client side. Almost all presenters should be able to handle errors, so maybe you will have an idea to make something like this :
P.S. ResourceManager is just a wrapper around the application context, since we do not want to have Android dependencies in the presenters to ensure they are convenient for unit testing.
At this point, you can come up with a lot of details related to the implementation. For example, you can pass a lambda to the handleError function, or create a new abstract class ErrorHandlingPresenter which will extend BasePresenter and move the handleError function there. There is actually a lot of space for improvement. But this approach has some serious drawbacks:
- Your code is not SOLID. Your presenters are responsible for error handling.
- Your code is not reusable. Imagine that you need to process errors in the Retrofit Interceptor while you are trying to refresh your access token? What should you do? Just copy and paste the handleError function?
Let’s try to fix it.
THE RIGHT WAY
Let’s assume that the most common way to handle any error is to show a message to the user. Note: that is true almost for any project, but feel free to adjust it to your project requirements.
Further, I will show you a complete hierarchy of classes with explanations.
First, we need an interface for the classes, which is able to show the error. In most cases it will be implemented by activity/fragments:
Interface for error handler :
Since our error handlers live in the presenters, and the presenters survive orientation changes, we cannot pass instance of the CanShowError view to the error handler constructor.
The following is the most common implementation of the ErrorHandler :
The implementation has a weak reference of the view, that is able to show errors to the user. It has a resource manager for fetching strings and it has the most simple and common logic of error processing. The DefaultErrorHandler will be a global singleton, and the views will be attached and detached from the ErrorHandler depending on what the screen had presented to the user.
Good. Now we need to inject ErrorHandler implementation to every presenter that is able to handle errors and not forget to attach and detach view to it. For this purpose I prefer using inheritance, so I have two base presenters: the most common one has CompositeSubscription and ErrorHandlingPresenter, that attaches and detaches view to error handler automatically.
Please pay attention to the generic constraints of the view types.
Now our specific presenters that are able to process errors will look like this:
With the help of the Dagger2 we can pass the DefaultErrorHandler as implementation of the ErrorHandler interface to constructor. But if you remember, the DefaultErrorHandler can only show errors, so in my case the business rules were: if you get a 404 Not found error from the backend, then perform navigation to another screen, if something else — show the error to the user.
So, I create a new implementation of the ErrorHandler :
Here I used composition and injecting the DefaultErrorHandler to the SpecificErrorHandler. Also, a router for performing screen navigation. In proceed function we try to catch 404 error and navigate to another screen, if we cannot — we delegate the control to the defaultErrorHandler. The same is with the ‘attach’ and ‘detach’ methods — the control is delegated to the defaultErrorHandler.
Since the DefaultErrorHandler is a global singleton, we should use Qualifier for injecting SpecificErrorHandler implementation.
That is it. Now it should work as expected.
As you can see, it does not depend on the MVP details, it is flexible, SOLID and we can use it in any class that can spawn errors.