Niek Haarman
8 min readJan 28, 2019

Back to basics: Plugging in the Activity

…and wrapping up this series.

In previous articles, I wrote about how you can create screens and navigators as composable building blocks for your application. Screens become little independent blocks of code that take input and produce an output, navigators tie these screens together by providing the input and listening for the output. Navigators in turn can be composable as well and accept input and provide output. In the end, you may end up with a tree of navigators with screens at their leafs.

An example of navigation through an application where the user registers and enters the main application flow.

See Back to basics: Navigation for more information.

At the root of the tree, this leaves us with a stream of active screens which we can use for several things, like for testing, analytics or logging. But above all, this leaves us with a perfect hook to provide the user interface!

Presenting screens

In traditional Android application development, an Activity represented a single screen. To navigate to a different screen, you would start a new Activity and the OS would add it to a back stack for you. Activities have their own lifecycles and deal with their own layout.
Unfortunately, the Activity class is a God class, and quickly leads to messy implementations.

Over the years a lot of developers have switched to so called ‘single Activity’ applications, where the app would utilize a single Activity class to host the entire application flow. The most straightforward way to do this is using Fragments and the back stack in the FragmentManager. More recently, the Navigation Architecture Component provides another way to implement a single Activity application.
Fragments however are just as messy as their Activity counter parts. In fact, Fragments are designed to be able to do everything an Activity can do!

Displaying screens

Instead, we can take our Activity instance and treat it as nothing more than a window that can display a user interface. It registers a listener with the root navigator in our tree, and listens to changes in active screens. When a new screen becomes active, we simply replace the layout in our Activity with a layout for the new screen. Simple as that. Now, the Activity becomes a pluggable component.

A couple of basic use cases should be able to demonstrate the simplicity of this. In the GIFs below, an Activity registers itself with a Navigator, which provides the Activity with a screen (the green S). The Activity has a reference to its layout, in which it can inflate and place views (V). Whenever a screen becomes orange, it is being replaced; whenever an Activity becomes red, it is stopped.

A new Activity instance

When starting an application, the Android system launches a new Activity instance that can be used to host the UI. In our case, it registers a listener with our Navigator. It will receive the current active screen and inflate the layout for it. Finally, it will attach the layout to the active screen so it can start displaying data.

A new Activity appearing.

Changing the active screen

When a new screen (say screen B) becomes active, the Activity detaches the current layout from the previous screen A. It inflates the new layout for screen B, replaces layout A with layout B using a suitable transition animation, and attaches the new layout to the newly activated screen.

Changing from screen A to screen B and vice versa.

Navigating away from and returning to the app

When the user navigates away from the application, the application ‘loses’ its window to the user. The Activity is stopped and detaches the user interface from the active screen. When the user returns to the application, the Activity is started again and can simply attach its layout to the active screen again.

Stopping and starting an Activity.

When the user returns to the application after the Activity was destroyed, it needs to inflate the active screen’s layout again and attach it to the screen. This behavior is pretty much the same as our first case ‘A new Activity instance’.

Destroying and recreating an Activity.

Changing configurations

When the user rotates the device, enters split screen mode or changes the system language, the system will destroy any active Activity instances and recreate them with the new configuration. When this happens, the original Activity simply detaches its layout from the active screen, and the new Activity inflates the new layout and attaches it to the active screen.
From a screen perspective, changing configurations now simply means switching out views.

Rotating the device.

Lifecycles

As mentioned before, Activities are destroyed when the user rotates the device. This leads to many difficulties when doing work that should survive these orientation changes.
Treating the Activity as a pluggable window that can show our user interface now allows us to split our screen lifecycles in two: on the one hand we have created — started — stopped — destroyed, and on the other hand we have attached — detached.

A screen’s lifecycles.

The four-tier lifecycle represents the actual screen lifecycle. It is created when the constructor has finished executing, started when it is the active screen in the application, stopped when it isn’t the active screen anymore, and destroyed when it has served its purpose.

Next to that, the the two-tiered attached — detached lifecycle represents the coming and going of the Activity that hosts our UI. It doesn’t have to affect the screen lifecycle when rotating the device, taking away the difficulties we had when using the Activity lifecycle.

In practice

Over the past couple of months, I have been working to translate the ideas and thoughts in the past few articles into a new, functional library. In this library, I’ve tried to keep several concerns as pure as possible: there is a small, pure JVM core module that captures the ideas using only interfaces and data classes. Similarly, there is an Android core module that does the same when applying these ideas to Android.

On top of that, there are several ‘extension modules’ that provide an implementation of how the core libraries could be used. There are base implementations for screen and navigator classes, and the entire wiring of the Activity is provided for you. There is support for RxJava and the AndroidX lifecycle, and there are several testing utilities for both the JVM modules and the Android modules.

Acorn is designed to be flexible and highly extensible, allowing you to have full control over how to put your application together. On the other hand it is also very opinionated, to protect itself from inheriting the possible flaws we are forced to deal with during traditional Android development.

Why reinvent the wheel?

Dozens of libraries already exist that step away from the platform, reshaping the wheel. Think Conductor, Mosby or Square’s Flow.
Unfortunately, the wheel is square. Or hexagonal at the most. We’ve been kicking it for 10 years now, subconsciously copying bad design choices over and over again: screen components remain god objects and responsibilities are entangled, making the code difficult to test.

The wheel can also be multi-platform: why not create an API written in Kotlin Native that has the potential to be shared across platforms? A huge part of a mobile application consists of views, screen controllers and navigational logic. Sharing the controllers and the navigational logic should be a thing, right?

The cost

Change comes with a cost. New libraries come with a cost, especially libraries that aren’t widely used. It is important to be aware of these costs when incorporating a new library into your application. Things that are unclear, broken or just not supported may be difficult to overcome at first.

Modularization also comes with a cost. One of Acorn’s main pillars is modularization, which means that you may need to write extra code to wire everything together. Acorn was purposefully written in a way that protects itself from entangling concerns and taking shortcuts. As the library matures and common pains become clear, naturally new solutions may appear.

All this being said, Acorn has been successfully incorporated in two entirely different types of greenfield projects. The first one being an app with a relatively complex navigation flow, the second one a simpler, data-driven application with repeating UI segment types in the same screen for both mobile and tablet layouts.
Acorn is also (almost) fully documented, providing class documentation, example projects, and a collection of articles that explain the background and ideas and workings of the library.

Wrapping up (this series)

To be able to create stable applications you want to protect yourself from changes or bugs in code that you don’t control. The Android platform is an example of such code, and you want to keep it at an arm’s length.

We’ve been thinking of Android apps as Android apps, instead of just “apps” that must also run on Android while also respecting the android lifecycle / ecosystem (quoted from /u/Zhuinden). Build your apps for the Android platform instead of on top of it. Make your business logic the most important piece of code in your application and make it as platform-agnostic as you can. You will profit in flexibility, testability and protect your code base from bugs and weird intricacies in the platform.

With Acorn I hope to provide a different view on architecturing applications for Android, in a way that actually moves the Android framework towards the outer layers of your application where it can do the least harm.

Acorn is nearing 1.0 soon, with the core API’s being stable for a significant amount of time now. You may expect (small, possibly breaking) changes before 1.0 once some of the more advanced issues are being resolved, but overall the library should be stable.

Thanks!