Clean Android Code articles series:

We are not perfect, and even best of us make mistakes when implementing a new feature. Things sometimes go wrong and application breaks, resulting in a quite bad experience for the user.

There are 3 types of errors I would like to talk about.

Program errors

— implementation bugs, bugs in libraries that are used in the application and, basically, anything, related to code of your application.

Yes, it’s bad when these things happen, but we need to deal with it. And, setting aside the time for actual fix, it’s quite simple — all you need to have is a crash reporting tool.

I use Crashlytics by Twitter, but there are many others, like a new Firebase Crash Reporting. Just check that you can pass in additional parameters with the crash — it can help you tremendously when debugging the problem. Another nice thing to have — ability to decrypt proguard-obfuscated classes, otherwise it would be difficult to figure things out in production.

Logical errors

— when things don’t work the way they should because logic is off. Like, screen does not open when user presses the button, or item is not deleted from the list when swiping it left.

These errors are difficult. The best solution, of course, is to have full acceptance test coverage, checking every tiny aspect of your application. Unfortunately, it’s hardly realistic to do, unless you are a team of many developers and have time for detailed coverage, or you have a QA team that does automation for you (if such thing even exists in this world).

Having a good unit-test coverage helps a lot with stability — if you are confident your classes logic is solid you would get much less errors in the UI. But still, you can get this:

Or this:

What I would recommend — have at least a basic smoke-test for your app. What you need to do:

test navigation of the app by visiting every screen, going through every navigation graph branch and ensuring that screens at least show up with no crashes

define list of most significant features of your application and test them. For example, news application would let you subscribe to a topic, or let you define order of sections you see on your start page. Don’t dig too deep, it’s a smoke test after all.

Use Spoon to run your tests on many connected devices and take screenshots here and there, so you could check later that UI is not broken on some device.

If you have money — use cloud testing, like AWS Device Farm or Firebase Test Lab.

Use Google Test Bot that runs automatically on every new release published in Play Store — for that you need to enable it in Play Store application settings.

I will dig deeper into Espresso and Acceptance Testing in future articles of the series.

Expected Errors

Some errors we expect to receive. If your REST API, or whatever API you are using, is designed well — it will report you errors properly when something goes wrong. Good chance you would receive HTTP error codes, that could be of default subset (200, 404, 500, etc), or maybe even custom values that you have defined for your project.

Do not forget that even with error response you can pass some data. See how Twitter handles errors, I find their example very good for APIs.

So if there is something wrong — backend should properly notify you about that, and your job is to keep user properly informed and not staring on empty screen.

For that I suggest to use a Resolution Strategy pattern.

Resolution Strategy is a strategy that you apply when a particular error happens.

For example, when server is responding with 503, you might want to display a SnackBar with message “Sorry, we are experiencing technical issues”, and provide a Retry button to send same request again.

Or, you could have an agreement with your backend that if user session was expired for whatever reason, server would return your 403, then receiving this in your application you would display a popup asking user to log in again.

Another example would be displaying some persistent message when there is no connection available on the device, notifying user that refreshing lists will not work and posting message will not work either.

I’m sure many modern applications already use such approach in one way or another. Let’s see how can we apply this in our application.

First, let’s define which kinds of situations we would like to resolve.

RxHttpResolution — when we are calling network reactively there are some things that we can expect going wrong(HttpException), but some thing’s we can’t predict, so we just wait for some Throwable

NetworkConnectivityResolution — we would want to do something when network is or no longer available

LocationRequestResolution — even though location is still not used in this project, as an example, we might want to handle when location exception comes — an exception that we would throw manually on timeout, for example.

These three resolutions, along with more you can come up with, combine into our Resolution interface

ResolutionByCase expands our HttpException resolution to call particular methods depending on error codes. It might also do same for other resolution interfaces, if needed.

Now, implementation of resolution itself depends on you. You might display Snackbars in UI as I usually do, or you could have a TrackingResolution implementation that might track when these errors are happening and, for example, send this data to the backend for analysis.

Here is UIResolution that I usually use:

UIResolution implements our Resolution interface, and UIResolver knows how to display resolution in the UI by showing a snackbar.

Now we need to tie this UIResolution to our requests.

Because UIResolution is something UI-layer knows about, let’s introduce ResolvedScreenContract, that would provide a Resolution

interface ResolvedScreenContract : ScreenContract {

fun getResolution(): Resolution?

}

In most cases this would be our UIResolution, but some screens might want to override default behavior and provide their own version of Resolution Strategy.

Next thing we introduce is ResolvedSubscriber — a specific kind of Subscriber that would know about our resolution, and if something goes wrong during Rx network request — it would ask for resolution.

You use this ResolvedSubscriber the regular way, just pass in a resolution too

flickrRepo.searchPhotos(latitude, longitude, testRadius)

.subscribeOn(schedulers.io)

.observeOn(schedulers.mainThread)

.subscribe(ResolvedSubscriber(view?.getResolution()!!, {

googleMapViewExtension.addPhotoMarkers(it)

}, onCompletedFunc = {

googleMapViewExtension.navigateTo(latitude, longitude)

}))

And to show an example that Resolution Strategy really works, let’s use our GlobalBus, introduced in the previous article, to notify us about network connectivity, that would trigger Resolution depending on the network state.

For that we need to:

implement ConnectivityBroadcastReceiver that would receive events about network state

implement ConnectionChecker that would know how to check network state

implement ConnectionEventsHandler that would know when events come in and would handle them, and use that handler on MainPresenter

Where does it make us?

We now have a way to add new strategies to our Resolution Strategy pattern, we handle network request errors gracefully, screens now can provide resolution, and we can call not only from reactive subscriptions, but from any place where we would want to resolve an error.

Also, when using ResolvedSubscriber, we will never miss on any errors happening in Rx — sometimes that’s the case when you forget to implement onError handler.

Running our app with no network available now correctly displays a notification to the user:

Network connectivity error resolution

And, of course, we should not forget about tests: ResolutionByCaseTest, UIResolutionTest, ResolvedSubscriberTest, GoogleMapsPresenterTest, ConnectionCheckerTest, ConnectionEventsHandlerTest, MainPresenterTest

You can find relevant changes on Github branch

Clean Android Code articles series: