Reactive Error Handling in an MVVM Driven Architecture

Filip Babic
COBE

--

Well if you like it, read on!

So here we are, months after Google announced their Architecture components, and while MVVM is being slowly absorbed and embedded into our beloved Android projects, some questions are still in the air.

One of those questions is: “How to handle errors?”.

If we’re going to go full on Reactive with either LiveData or Rx so we could let the UI update itself, we are going to need a smart and clean way to handle error cases, most importantly the ones that are connected to input forms such as a Registration or Login screen.

The Problem

Ideally we would want to use the aforementioned LiveData/Rx constructs. However, having something similar to this:

val emailEmptyError = MutableLiveData<Boolean>()
val emailInvalidFormatError = MutableLiveData<Boolean>()

val passwordEmptyError = MutableLiveData<Boolean>()
val passwordTooShortError = MutableLiveData<Boolean>()

is an overkill. We would have too many LiveDatas to observe, one for an empty state, one for a “too short” state (for usernames/passwords), and one for an invalid format (emails, usernames and passwords if they need at least one special character). And we’d have to have all those datas for each input field.

On an average registration screen there are a name, an email, a password and a repeated password. This could easily escalate to a dozen of LiveDatas just to handle all the error states.

It’s even more annoying that we would have to be able to reuse the logic for multiple screens (email in register and login) which is all in all a big nope.

Yeah grumpy cat, that’s a huge NOPE

So we do need LiveData of an error state, but in which form? You just answered the question to yourself! We’d have an error state per input field.

But how???

Let me introduce you to something called enums!

How could an enum encapsulate all that?

Enums are really special, they are statically compiled objects which can have fields on. Fields again are resolved at compile time, and you just cannot create them later on, they are declared when writing an enum case.

Wonder if we could use that to our advantage? (Hint: the answer is yes)

The solution

First we’ll declare an interface so we could abstract out the Errors:

interface ErrorEvent {

@StringRes
fun getErrorResource(): Int
}

There is only a single method, which returns the current error event’s resource. Shouldn’t this be a String though? Nope, we want to have them drawn from resources, so they’re automatically localised. Notice how this is achieved by using the compile time defined values.

Then we define our Events. We’ll take just one for now, the rest follow the same principle.

enum class EmailErrorEvent(@StringRes private val resourceId: Int) : ErrorEvent {
NONE(0),
EMPTY(R.string.email_empty_error),
INVALID_FORMAT(R.string.email_format_error);

override fun getErrorResource() = resourceId
}

We could define tons of cases here, depending on our Application requirements. The key point here is that instead of numerous LiveData<Boolean> objects in our viewModel, we can have just one: a LiveData<EmailErrorEvent>.

What would our validation look like now?

when {
//name validation
...

email.isBlank() -> emailError.value = EmailErrorEvent.EMPTY
!email.isEmail() -> emailError.value = EmailErrorEvent.INVALID_FORMAT
//password validation
...
else -> { //let's register the user
val request = RegisterRequest(email, password, name)

authInteractor.registerUser(request, getUserRegisterCallback())
}
}

Our validation is now very simple and readable and it can be tested purely using unit tests (by asserting proper enum values), and it’s very Open/Closed since we can add a new case, without breaking anything. And all the UI has to do is:

//an extension method to shorten our code
viewModel.emailError.subscribe(this, this::onEmailError)
private fun onEmailError(emailError: EmailErrorEvent) {
email.error = getError(this, emailError)
}

Where the getError(context, errorEvent) global function is defined as:

fun getError(from: Context, errorEvent: ErrorEvent) = if (errorEvent.getErrorResource() == 0) {
null
} else {
from.getString(errorEvent.getErrorResource())
}
We are victorious!

Benefits (trust me, there are a few)

  1. Now we have abstracted away our error states behind a series of Enums. This allows us to cleverly use when statements and LiveData objects to quickly assign the current error for display.
  2. The UI doesn’t have to do any excess work, it just passes an error String to the affected input field.
  3. The errorEvents are highly reusable so you can add them to other validation screens without any trouble.
  4. It comes down to pure unit tests (more less) to check if the correct error is displayed.
  5. Adding a new error is as easy as adding a new enum case.

Conclusion

Overlooking various options for error handling, which I’ve come to grip with over my development life, I’ve realised this one is the cleanest and the most understandable for me.

After showing it to my colleagues, we all agreed this is something that’s readable and self explanatory, and furthermore offers documentation of sorts for all the errors on each screen.

If you, however, have any other ideas, or improvements for our current error handling, be sure to let me know! Learning is best done by experimenting with things together. :)

Filip Babić is an Android developer at COBE and a Computer Science student at FERIT, Osijek. He is a huge Kotlin fan, and occasionally holds mini work-shops and Kotlin meet-ups in Osijek. He likes to learn new stuff, play DnD and write about the things he loves the most. When he’s not coding, writing about coding, learning to code, or teaching others how to code, he nurtures his inner nerdiness by gaming and watching fantasy shows.

While you are here, check more articles from COBE:

--

--

Writer for

Android developer. Praise Kotlin :] Keen and enthusiastic learner and mentor, passionate about teaching and helping others. GDE @ Android, Osijek, Croatia.