What should the View layer be responsible for in Android?

Recently, the excellent "Guide to app architecture" was released. This is an awesome resource for anyone entering the Android app development field. It summarises many of the important architectural decisions that need to be taken in mind when creating a new Android app. Whatever your experience level may be, a quick read definitely deserves your time.

Of course, when it comes to software architecture, there's no truly a single universal way of doing things. And the "borders" between layers, no matter how hard you might try to describe them, are subjective.

In the "Types of logic" section, the "UI business logic" is defined as:

UI behavior logic or UI logic is how to display state changes on the screen. Examples include obtaining the right text to show on the screen using Android Resources, navigating to a particular screen when the user clicks a button, or displaying a user message on the screen using a toast or a snackbar.

And this is where I disagree with the authors.

Traditionally, UI testing is quite hard (and fragile) in the Android world. So we end up having quite a strong coverage for non-UI elements of the app, and a few integration tests to test end-to-end the entire app. Moving as much logic outside of the view should be the aim when building an Android app.

In my opinion, obtaining the right strings to display, or navigating to different screens, should not be the responsibility of the view layer. This should be handled in the business logic layer (i.e. View Model, if following the popular MVVM approach). Putting it in the view layer makes it harder to test and reason about. And these are just too important responsibilities to leave untested (or under-tested).

But you might ask what happens with the concept of isolating dependencies to the Android system away from the ViewModel (or another business logic layer)? The View Model should never know anything about Android, if possible, and this is the right approach. This is where abstraction comes into play. Depending on how strongly you want to follow this isolation guideline, you can abstract away as many parts of the Android system by wrapping them with your own classes that can easily be mocked/replaced in tests.

For instance, when I want to assemble some strings for the view layer to show, I need access to the Android's Resource object. I tend to create a SystemController that can access system resources and can be easily mocked in Unit tests. So the responsibility of gathering and assembling strings stays in View Model. The View will get exactly what needs to be displayed. No "how to display" logic is stored in the View layer, but rather "where to display" logic.

@Singleton
class SystemController @Inject constructor(
    @ApplicationContext private val context: Context
) {

    fun getString(@StringRes stringResId: Int): String = context.getString(stringResId)
}

Of course, this has the downside that the View Model knows about resource ids, but I am fine with this. If you insist on complete isolation from the Android system, you can go one step further and abstract these resource ids away (which will be more verbose, but doable).

Hopefully, even if you disagree with this approach, this kind of rant was useful food for thought :)

Happy coding!