Back to basics: Screens

Niek Haarman
6 min readSep 30, 2018

--

In a previous article, I talked about things that are wrong with the Activity class, the main conclusion being that it does too much. This time I want to look at an alternative way of looking at the concept of screens, stepping away from the design flaws of the Activity class.

What is a screen?

In a typical mobile application, the user can navigate from one destination to another. Each of these destinations can be regarded as a ‘screen’: a set of components that fulfill a very specific use case. For example, the main screen of a Twitter client may show a list of tweets to the user.

The notion of ‘screen’ here is very much tied to the presentation layer of the application: when talking about screens, you’re inevitably talking about UI and navigating through them. This means that business logic, such as processing use cases, calculating values and fetching network results has nothing to do in there.

The lifecycle

A lot of people have difficulties grasping the concept of the Activity lifecycle, but this part is one thing the Activity (somewhat) did right. When a screen becomes active in the application, it may want to start calling use cases in the business layer to retrieve some data. When it becomes visible to the user, it may want to register listeners to the UI to react to events. When it is replaced by another screen, it may need to stop listening for updates, and when it is destroyed (by popping it from a back stack for example), it may need to do some cleanup.
Lifecycle callbacks are an excellent way to go and handle these scenarios.

The user interface

Usually, screens have something to show to the user. A screen may gather the data to show and pass this to the user interface, which uses this data and turns it in something visual for the user to interact with.
This is one case where the Activity violates the single responsibility principle: next to handling lifecycle events, it is also responsible for inflating the user interface.

Saving state

Android’s multitasking mechanism introduces the concept of process deaths: the system can kill an application at any time to reclaim memory. To solve the problem of losing state when a process death occurs, Android allows Activities to save their instance state.
Typically every non-trivial screen in an application needs to save some state to be able to be restored from scratch. This could be just the View hierarchy state, but it could also include the id of an item the screen represents, or anything else worth saving. Ideally, you would wanna save the smallest data set necessary to be able to restore the entire screen.

Taking back control

The Activity class is the original way of representing a screen in an Android application. However, its severe violation of any kind of single responsibility principle and the fact that it invites monolith God classes makes it very hard to build a maintainable and testable component.

Another difficulty with Activities is that the Activity is instantiated by the system through reflection. This results in the fact that every Activity needs an empty constructor, and providing dependencies through the constructor (like it’s meant to) is impossible.

And even if it was possible to inject dependencies in the constructor, the Activity has an extensive Android-tied internal state that has to be prepared before you can do something. Testing Activities on the JVM is out of the question.

Finally, the Activity instance gets destroyed on configuration changes, such as a screen orientation change or a locale change. What follows, is years of developers having difficulties with sharing asynchronous requests between multiple instances that represent the same exact screen.

Instead, we can take back control by separating the concerns that concern screens.

Business logic

Business logic has absolutely nothing to seek in the presentation layer. Executing network requests, storing data in databases or doing complex computations has nothing to do with the UI, or with anything Android related for that matter. And because of that, you can write your use-cases in an Android-agnostic manner, making it modular, testable and portable.

Presentation

As mentioned before, screens need lifecycle management. In the basis, screens can be started, stopped and destroyed. Next to that, they can have instances of the user interface attached to them. More importantly, they have no business of instantiating views or even knowing about Android classes. In fact, much like the business logic, screens can be implemented completely Android-agnostic in a pure Java module.
By defining Container interfaces that describe how the user interface behaves, you can invert your source code dependencies against the flow of control.

Navigation

Navigation is about traveling to new screens in an application. To be able to build true modular screen components, screens should have no knowledge of the current navigation state in the application, nor should they have the responsibility to decide where to go next. That means commands like goTo(NextScreen()) and backStack.pop() have no business in screen classes. Instead, screens can publish events that tell a dedicated navigation component something interested has happened: onUpClicked or onItemClicked(itemId). The navigation component listens to these events and decides where to go next.

User interface

Only at this point is where you need to include the Android framework into your code. In the UI layer you implement the Container interfaces defined by the presentation layer using Android’s View classes. When the Activity is available, you create these Views and attach them to the screens. When the Activity goes away you detach them again. A configuration change is thus nothing more than replacing the first View with another.

The image below shows what this looks like in a diagram. The hard curvy lines are boundary lines between modules. Notice that the arrows that cross the boundary point towards the business rules: the business module knows nothing about the presentation module, and the presentation module knows nothing about the UI module.

Properly separating your concerns. The hard curvy lines are source code boundary lines. Closed arrows mean ‘uses’, open arrows mean ‘implements’.

Furthermore, your business module and presentation module are completely Android-agnostic¹. This means that you can test all your code on the JVM without any problems.

Translating to code

When regarding application screens in this way, all you need is a lifecycle, a Container interface, and state saving. The snippet below defines an interface of everything that a screen needs:

The lifecycle is simple: started, stopped and destroyed. A screen is active when it’s started, and a screen is visible when a Container instance is attached.

Without the Android framework in the way of needing an empty constructor, you can now write your classes the ‘normal’ way, including saving and restoring from a saved state:

Closing remarks

Application architecture is about boundaries. Higher level policies (such as your business rules) must be properly separated from lower level policies (the UI, the database, the web). With traditional Android development that includes Activities and Fragments these boundaries fade away very quickly, allowing business logic to get tangled with UI implementation.

When you look at the diagram above, the pattern looks very much like any MV* pattern you’ll come across. And this is no coincidence: separating View logic from business logic is the first step of taking back control.
Unfortunately, many libraries or patterns over-simplify either the lifecycle, neglect state saving, are still tightly coupled to the Android system, or fail to actually entirely apply separation of concerns.

Next up

In another article I want to dig deeper in the basics of navigation: travelling through different screens in your application. I talk about this in Back to basics: Navigation.
Finally, all this needs to be tied to an Activity, to be able to actually show anything to the user. Spoiler alert: the Activity is not much more than a window!

¹ Now there’s little chance your application will run on anything other than Android in the short future, so separating the UI and presentation into separate modules may be overkill. However, by doing so you are enforcing yourself to be Android-agnostic in the presentation module which in turn strengthens testability of your system.

--

--