Skip to main content

Android Actual Clean Architecture (AACA)

by Adrian Tache, 06.05.2024

The UI Layer

What is a UI layer?


The UI Layer receives information from the use case, formats it and displays it to the user, without doing any data interpretation or processing. It also receives user interactions, and sends them to the domain, as raw as possible.

The UI layer represents the user interface that allows a user to receive information from the app, and to interact with it. In an Android app, these can be views, TTS and voice recognition, or any other kind of UI.

Regarding the display of information, this should be a very simple, unidirectional thing. Views should be as dumb as possible, handling only visual aspects of displaying the data. Presenters only map that data to whatever data structure or format that the views expect. Everything flows downhill and there isn't any serious logic in this layer.

Regarding processing user interactions, this has two sides to it. Fundamentally, receiving input is similarly simple and bare-bones. For example, if the user types some amount in an input field, the string of what he has typed is passed to the callback unmodified, and without any extra formatting etc. However, there is one use case where user interactions draw complexity, and that is where the UI itself is complex, and processing is related to transforming UI interactions into data, without having a strong opinion regarding the data (read the example below to learn more).

Practical example

Regarding showing data to the user, it makes sense, for example, to format a currency or date based on the user's locale and display it in a format that is expected (in order to avoid having a large number of decimals, or a date the user cannot read at all in the case of Unix time). The mechanism through which this is performed will be explained below, and it involves the Presenter and potentially a Provider.

Regarding taking data input, things are a bit more complicated. For simple inputs, like input fields and checkboxes, they already provide us the data in a format that we expect (Booleans and Strings). Mind you, the UI should never convert a string that represents a number into a number, this is something that an entity should do, because only it knows the correct rules to do so, and how to handle errors etc. That way we also maintain a single source of truth, and the state of the view is always a formatted mirror of the state of the entity, and the view can never contain a state that's newer than that of the entity. So the flow is that the user interacts with a UI element, the entity receives the result of that interaction, updates its data, and then that new data goes through the data flow back to the UI to update the UI, with the presenter ensuring appropriate formatting etc.

However, if you have a UI that requires processing to get data from a user interaction, it is the UI's responsibility to perform the conversion (ideally through a helper class). For example, if the UI input is speech recognition, there's no point in forwarding an audio file to the domain layer, but rather the component that provides this recognition should simply offer us the resulting text (although this particular use case might be better placed in the Platform Layer, so it's debatable). Similarly, if you have a knob that you turn to select a value, there's no point in giving the domain the amount of degrees the user has rotated, but rather directly what value that rotation represents.

Here is an overview diagram of the UI layer, with more details about each component below:

AACA UI Diagram

You will find more details about these components below, and my recommendation is to construct them in the order in which they are described, starting with presenters and finishing with views, which should be extremely easy to construct, since they do not contain any logic.

Why do we use a ViewModel?


Android ViewModels are used to link the domain layer and the flows it exposes to the lifecycle of the UI, in order to improve memory usage and reliability.

The Android ViewModel is a component which, in this architecture, has one single purpose: caching the flows that come from the Domain Layer, in order to prevent data loss in the case of configuration changes or app backgrounding. Furthermore, depending how your dependency injection is set up (and if you use any), this component will ensure that Use Cases are scoped to the ViewModel lifecycle, meaning they survive lifecycle changes as appropriate, but they get recreated or destroyed if their respective ViewModel suffers the same fate. This is a good thing, in order to avoid taking up memory with Use Cases that are irrelevant, as well as in order to prevent unpleasant edge cases where objects have been garbage collected prematurely, but the Use Case has not (usually when ViewModels are destroyed, Use Cases should be recreated).

Beyond GC and memory concerns, however, this component is technically unnecessary, as an Application-scoped Use Case will keep state even when the app is backgrounded.

What is a State Machine?


A State Machine is a component which consumes States and Events produced by the Use Case and either displays the appropriate UI or runs the relevant callbacks.

Since the core functioning of the app is dictated by States, we need a matching element in the UI layer to react to these states (as well as Events). This is the purpose of the State Machine, which is often a very simple Composable function that only contains a when function to handle every incoming state and display the appropriate UI for them, with the help of a Presenter. In the case of States which only contain a callback (like the Init state common to every flow), they are handled inside a LaunchedEffect.

If there are also any Events that are received, they will usually be handled as they appear by another method inside the State Machine.

An important thing to mention here is that, due to limitations within Android, some things need to be performed inside the UI layer. My recommendation is to move as many of them as possible inside the Platform layer, using Dependency Injection to provide Android relevant things such as Context, but I will agree that some things are very difficult to move there. For the things that are difficult to move, my suggestion is to trigger them using Events, which are then handled inside the State Machine (ideally via interfaces to ensure some decoupling). Any results from these interactions can be returned to the domain layer using the callbacks from these Events (although one must take care to cache them if necessary, since Events can only be consumed once).

What’s a Presenter?


A Presenter is a component that transforms the UI-independent UI Objects coming from the domain layer into View Objects, tailored to the concrete implementations of Views that we need.

A Presenter is a component that receives a UI Object and tailors it to the tangible UI implementation that we have for that feature. In essence, it's just a smarter kind of mapper, which converts a UI Object into a View Object.

The purpose of this component is two-fold. Firstly, it applies the appropriate formatting to the data coming from the domain, and it can use various Formatters to do so. Secondly, it can apply special UI-level logic to this data, for example integrating the data with Android-level resources such as strings (via a Provider), or interpreting the data to a format that is relevant to the UI (e.g. transforming a value to the rotation of a knob).

The Presenter is usually kept as an instance inside the State Machine and accessed as needed.

Why have multiple Presenters?

An app can have multiple Presenters for the same flow, to cover various cases. For example, it can make sense to have another Presenter for very large screens, or for foldable displays, in order to display some UI elements differently (e.g. more detail). Alternatively, it can make sense to have only one flow for certain common interactions in the app (e.g. displaying a user's balance and details inside a banking app), but have different Presenters to feed that data to different UI.

As always, implement what makes sense to you, but depending on the situation, the Presenter is linked either to a certain UI or to a certain UI Object (coming from the domain, if that data is always processed the same way).

What’s a Provider?


Providers give Presenters access to resources and any other information they need, in a centralized, easy to test way.

Sometimes, inside a Presenter, you will need access to certain components that are difficult to obtain, in order to transform and combine them. A good example for this is translated Strings or String templates, which are then combined to build, for example, an AnnotatedString. If you just need to put some information into a string template, this pattern isn't necessary (you could just provide the string resource id and the parameter separately), but it's definitely easier to work with it this way.

You could certainly avoid this component if you just pass a Context object into your Presenter, but in that case you make it difficult to Unit test your Presenter, since now you have to either make it a slow instrumented test, or mock Context, which is really an unnecessary complication. In that case, we can build a simple Provider interface, which takes a string resource and optional arguments, and has two implementations: a real one which obtains Context from Dependency Injection (or manually), and a test one, which will most likely just give you back your string resource ID and maybe some extra text to ensure output is different than input. Of course, it's not a bad idea to then instrument test the real implementation of the provider, just to make sure it works, but you've turned many presenter tests into fast, more likely to break Unit tests, and only have one instrumented test for something that is very unlikely to break anyway.

What’s a Formatter?


Formatters offer a centralized, easy to test way to format the data coming from the UI Object into what the user expects.

A formatter is another component that is provided to a presenter, but its purpose is much simpler: it represents a centralized way of handling formatting. The benefit of this is having the same formatting rules applied to your entire app, and avoid having random formatting rules or defaults. This can apply to numbers, dates, currency rules, custom input field masks, etc. The receive data from a UI Object and return whatever the view is expecting, usually a string.

If formatters need information, for example the string template for an amount with a currency, I recommend providing it to them together with the data by the presenter, in order to simplify interactions between components. Having said that, this might complicate certain interactions, so it's a point that you might want to adapt according to your particular tangible implementation.

These classes are, similarly to Providers, hidden behind interfaces, in order to facilitate testing.

When is a Formatter not a formatter?


Formatters should only contain UI related logic, and in case of business logic, the logic should be moved to the entity or duplicated there.

The issue with building a formatter is that you are, to a certain extent, putting logic inside them. This isn't too much of an issue if this logic is purely UI logic, for example adjusting a number for display according to the locale settings the user is expecting, in order to have, for example, the kind of decimal separator they expect in order to avoid confusions.

However, things become problematic when formatting starts impacting business decisions. A very simple example here is the amount of decimals displayed to a user, and the inevitable rounding that occurs as a result of it. UI shouldn't decide what the correct way to round an amount is (except when there is no significant impact to the user, but keeping full precision would significantly negatively impact the UI/UX). Care must be taken to avoid these scenarios, in order to keep the transformation of data to a UI usable value inside the entity, where it belongs. In the case of decimals, it is usually either the entity or the mapper from DTO to data object which takes care of transforming a large, human-unfriendly number of decimals into a more expected number, based on business rules. And it really isn't as simple as it sounds, because yes, for currencies this will probably just be 2/0 decimal places, but how would you handle crypto, especially the ones with a very high price vs the ones with a small price. Care must be taken in all these scenarios, and when taking these decisions, ensure that if rounding happens in a formatter, it does not in any way impact the business logic or mislead the user.

What’s a View Object?


A View Object represents all the data needed for a view to be displayed, and all the callbacks to react to user events.

In order to keep views as simple as possible, View Objects should be provided to them. These are data classes and contain all the data and all the callbacks that would ever be required by a view to display its content. You will know that you have built a view object the right way when you are building the view itself, and there will be absolutely no logic at all inside the view beyond displaying that data or calling a callback (not even retrieving the first element inside a list, or checking if that list is empty). The benefit of building these objects this way is that, while they are slightly more verbose, all the logic is kept in one of two places: either the Entity, or the Presenter, and you know where to search based on whether the logic is purely UI-related or not.

In the Clean Architecture terminology, these are referred to as "View Models", but I have renamed them in order to avoid confusion with Android ViewModels.

All the data inside a View Object should come either from the UI Object (processed or formatted by the Presenter as appropriate), or be generated inside the Presenter as required for this particular view.

All the callbacks that are inside this object will come either from the same UI Object, or if it is the case that a UI requires special conversion of user inputs into data, from the Presenter, but with the sole purpose of feeding that data to the domain.

Avoid the temptation to quickly close simple loops, such as checking a checkbox directly inside a view or presenter, before feeding that user interaction to the use case through its dedicated callback. Doing this will introduce another location for state to reside in, and might lead to desyncs between the UI and the domain, which can eventually lead to bugs. Instead, send the user interaction to the domain, and let it update the UI state as appropriate. Should this lead to slow updating of the UI, send a UI update as soon as the interaction reaches the domain in order to trigger this change, and then another one as soon as whatever background calculations have been performed. And remember, a good UI will be capable of handling loading and error states, even if that means that a checkbox isn't checked immediately when clicking on it (but other kinds of feedback are provided to the user).

What’s a View?


A View is a very logically simple component which receives everything it needs to display from a View Object, with no need for extra processing or logic beyond displaying UI components.

A View is a UI building block which consumes one or more View Objects and displays a UI for the user, in order to display information to them, and, if applicable, receive inputs from them.

Views should be extremely simple objects, performing absolutely no logic that is not purely related to displaying data to the user. Any complexity they may have should come from the need to display complex UI, and should not be related to any business or application logic.

If there are platform behaviours happening inside the State Machine that need access to a view, they should have their own invisible views rather than be forwarded to real views.