Please, describe the error

Found a bug? Helps us fix it by filling in this form

Flexible way for error handling in android

Sergey Opivalov
Android developer

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 to the user. But it is not true for every case.

What do we expect from the ErrorHandler in our project? – All errors should be processed in one place. – We need to know how 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, this approach will work for any kind of presentation pattern.

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 :
abstract class BasePresenter<View : MvpView> : MvpPresenter<View>() {

    protected var disposables = CompositeDisposable()

    override fun detachView(view: View?) {
        super.detachView(view)
        disposables.clear()
    }
}

It also may contain code for attaching and detaching views and some code for surviving orientation changes. But in my case MVP presenter will do it for me. So I am with you, guys. =)

How does specific implementation of a BasePresenter actually look like?

class SpecificPresenter @Inject constructor(
    private val useCase1 : UseCase1
    private val router: Router)
    : BasePresenter<SpecificView>() {

    fun function1() {
        useCase1.foo()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .doOnSubscribe { viewState.showLoading(true) }
                .doFinally { viewState.showLoading(false) }
                .subscribe({ viewState.onSuccess(it)}, { viewState.showError(it) })
    }
}

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 urge to make something like this :

abstract class BasePresenter<View : MvpView> : MvpPresenter<View>() {

    protected var disposables = CompositeDisposable()

    abstract val resourceManager : ResourceManager

    override fun detachView(view: View?) {
        super.detachView(view)
        disposables.clear()
    }

    protected fun handleError(error : Throwable)  {
    when(error) {
     is HttpException -> when (error.code()) {
                401 -> resourceManager.getString(R.string.error_unauthorized)
                500 -> resourceManager.getString(R.string.error_server)
                else -> resourceManager.getString(R.string.error_unknown)
            }
            is IOException -> {
                resourceManager.getString(R.string.error_network)
            }
            else -> resourceManager.getString(R.string.error_unknown)
}
}

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 (as almost all presenters should be able to handle error). There is actually a lot of space for improvement. But this approach has some serious drawbacks:

  1. Your code is not SOLID. Your presenters are responsible for error handling.
  2. 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 CanShowError { fun showError(error: String) } Interface for error handler : interface ErrorHandler { fun proceed(error: Throwable) fun attachView(view: CanShowError) fun detachView() }

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 :


class DefaultErrorHandler @Inject constructor(private val resourceManager: ResourceManager) : ErrorHandler { private lateinit var view: WeakReference<CanShowError> override fun proceed(error: Throwable) { Timber.e(error) val view = view.get() ?: throw IllegalStateException("View must be attached to ErrorHandler") val message = when (error) { is HttpException -> when (error.code()) { 304 -> resourceManager.getString(R.string.not_modified_error) 400 -> resourceManager.getString(R.string.bad_request_error) 401 -> resourceManager.getString(R.string.unauthorized_error) 403 -> resourceManager.getString(R.string.forbidden_error) 404 -> resourceManager.getString(R.string.not_found_error) 405 -> resourceManager.getString(R.string.method_not_allowed_error) 409 -> resourceManager.getString(R.string.conflict_error) 422 -> resourceManager.getString(R.string.unprocessable_error) 500 -> resourceManager.getString(R.string.server_error_error) else -> resourceManager.getString(R.string.unknown_error) } is IOException -> { resourceManager.getString(R.string.network_error) } else -> resourceManager.getString(R.string.unknown_error) } view.showError(message) } override fun attachView(view: CanShowError) { this.view = view.weak() } override fun detachView() { view.clear() } }

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 restrictions of the view types.


abstract class BasePresenter<View : MvpView> : MvpPresenter<View>() { protected var disposables = CompositeDisposable() override fun detachView(view: View?) { super.detachView(view) disposables.clear() } } abstract class ErrorHandlingPresenter<View> : BasePresenter<View>() where View : MvpView, View : CanShowError { abstract val errorHandler: ErrorHandler override fun detachView(view: View?) { super.detachView(view) errorHandler.detachView() } override fun attachView(view: View) { super.attachView(view) errorHandler.attachView(view) } }

Now our specific presenters that are able to process errors will look like this:

class SpecificPresenter @Inject constructor(
    private val useCase1 : UseCase1,
    override val errorHandler : ErrorHandler,
    private val router: Router)
    : ErrorHandlingPresenter<SpecificView>() {

    fun function1() {
        useCase1.foo()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .doOnSubscribe { viewState.showLoading(true) }
                .doFinally { viewState.showLoading(false) }
                .subscribe({ viewState.onSuccess(it)}, { errorHandler.procced(it) })
    }
}

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’ message 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 :


class SpecificErrorHandler @Inject constructor( private val defaultErrorHandler: DefaultErrorHandler, private val router: Router) : ErrorHandler { override fun proceed(error: Throwable) { when (error) { is HttpException -> when (error.code()) { 404 -> router.replaceScreen(AppScreens.USER_UNREGISTERED_SCREEN) } else -> defaultErrorHandler.proceed(error) } } override fun attachView(view: CanShowError) { defaultErrorHandler.attachView(view) } override fun detachView() { defaultErrorHandler.detachView() } }

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.


class SpecificPresenter @Inject constructor( private val useCase1 : UseCase1, @LocalErrorHandler override val errorHandler : ErrorHandler, private val router: Router) : ErrorHandlingPresenter<SpecificView>() { fun function1() { useCase1.foo() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnSubscribe { viewState.showLoading(true) } .doFinally { viewState.showLoading(false) } .subscribe({ viewState.onSuccess(it)}, { errorHandler.procced(it) }) } }

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.

Read and comment

Krasnodar

Kommunarov, 268,
3 fl, offices 705, 707

+7 (861) 200 27 34

Houston, TX, USA

3523 Brinton Trails Lane
Katy, TX 77494

+1 833 933 0204