Skip to main content

Android Actual Clean Architecture (AACA)

by Adrian Tache, 08.05.2024

The Platform layer

Why use this layer?

TL;DR

The Platform layer exists to handle all OS/device interactions which we can't really consider UI.

The Platform Layer exists because the Android OS requires a reference to a Context, Application or Activity for a number of operations, for example displaying a biometrics prompt, starting an activity for a result (which can also be used for other purposes), connecting to a WiFi network, etc. However, these operations are often unrelated to the UI layer where they are usually placed, and can bring undesired logic and complexity to UI components which we have otherwise been able to simplify. Using these Android components and references inside this layer is not entirely straightforward, and is probably very difficult to do without using Dependency Injection, so I recommend setting that up.

The point to this layer is to isolate "platform" logic to its own layer, since in my opinion this is neither UI logic nor data logic. A big part of how this architecture works is to extract logic to black boxes which handle tangible implementations in order to simplify the logic calling these components, as well as to ease testability by replacing them with fakes during unit tests.

What you will usually find inside this layer are interactions with the OS, as well as any results coming from those interactions, when they are not part of the user interactions but simply part of the logic of the application. What belongs here is of course debatable, but you will know you're building this right when they are neither part of the UI itself, nor sources or destinations for app data.

This layer doesn't have a structure of its own, its purpose being only the separation between use cases and the Android OS, via interfaces.

Should you not want to use this layer and the complications it brings, you can always move these interactions to Events, and this simplification is described a bit more in the UI section.

Why not just put this logic inside the data layer?

TL;DR

The Data layer is for interactions with data, whereas this layer deals with much more, and forcing the data layer to take on additional responsibility might introduce unnecessary complexity and therefore reduce the reliability of our code.

For the most part, this logic can, of course, go to the data layer if you don't want to build another layer, but in my mind this is not related to data, but rather abstractions of the operating system or device itself, and as such require unique logic that isn't always compatible with the way you think about things in the data layer.

Some of the biggest issues I've had working with code have come from people needlessly making their code generic and trying to shoehorn components to do more than what they were built for. Similarly, here you could consider, for example, interactions with the Android Keystore, to be part of data interactions, but it behaves in strange ways, requiring extra error handling, having strict rules about concurrency, and so on, and just forcing the data layer to support all this extra functionality can lead to limitations in what we can accomplish. However, if we simply put this component inside the platform layer, things become much simpler, because we can make it as simple, complex, or just plain weird as it has to be, without concerning ourselves with breaking existing patters, or building new patterns that support too many possible scenarios.

Having said that, this is one decision that you can choose to ignore, the main goal of this architecture is to keep things clean and easy to think about, so if it makes more sense to you to just keep this as yet another "data source", see how it works for you and then revisit the decision after a few months.

Complications of Context and Activities

TL;DR

Be careful when consuming these objects and make sure you're pointing to the right ones.

When using this layer, you will sometimes need access to Android components such as Context and Activities. While this is mostly trivial, you must be careful how you do this in order to avoid memory leaks or other issues (for example, displaying a BiometricPrompt with the wrong activity will make it invisible until that activity becomes visible).

My suggestion for this is to, of course, limit your usage of these components to what is strictly necessary, and ensure cleanup after usage. Dependency injection can help with this, as it usually has a reference to the application context, but you must remember to refresh the activity reference whenever it is destroyed (and since we cannot reliably know this, whenever it is started).

TL;DR

Since navigation concerns itself with logic more than just UI, it might make more sense to have it here.

Another component that would belong here, in my opinion, is navigation. While there are UI components to navigation (mainly screen transitions), the act of navigating is not a matter of UI but rather one of application logic. Therefore, it is my opinion that navigation (via an abstraction) should be its own component, which then has its own UI component in the UI layer, if necessary.

The benefit to separating navigation from UI is that, in general, navigating between screen is business logic, and is controlled by the use cases anyway, so we can simplify this part of the UI and

Alternatively, I have successfully implemented this as Events in the past, handled by a special Navigator component that is passed to the State Machines (or accessed via Dependency Injection), which has its own abstraction of navigation (in order to be able to easily change the navigation framework, or migrate to one, if needed) and its own navigation objects (we tried reusing UI objects for navigation and it was frankly a mess, because they needed to change for different reasons).

An alternative perspective

Another way of looking at the functionality of this layer might make it more appropriate to have it be, in fact, unified with the UI layer, but have the dependency inverted, meaning that the UI layer should be a part of the platform layer, rather than the other way around. This makes sense because, if you think about it, the UI appears as a sub-component of the OS and the application and the activity, so why not add just another layer of separation (or distinction, they don't really need to be subordinated to each other) in order to keep things separated by their functionality.

The big question with this approach, however, is how do you access that component, and it will probably be via the UI, which doesn't really solve any issues. Alternatively, you could just make it directly accessible via Dependency Injection, but at that point, it's probably cleaner (if maybe not easier) to just keep it as its own distinct layer instead.

If you have any suggestions as to how to do this cleanly, feel free to contact me with them.