Skip to main content

Android Actual Clean Architecture (AACA)

by Adrian Tache, 06.05.2024

The Domain (Use Cases)

What is a use case?

TL;DR

A Use Case is the application logic that enables the business logic inside an Entity to be performed, in the context of an app. This means all the orchestration that is necessary to display UI, get user events, get and save data, etc., but without the tangible implementation of those components.

The Use Cases are classes that hold abstracted application logic, to enable the business logic to be performed in the context of an application. This includes saving and fetching data, it includes displaying a UI and reacting to user events, and it also includes orchestrating the entities themselves.

Normally, they contain instances of the entities, and are the only component to interact with them, or to map them (using a Mapper) to UI objects to pass on to the UI layer, or to and from data objects to interact with the data layer.

In the theory of Clean Architecture, these are also named "Interactors".

One important thing to mention about how Use Cases are defined within the AACA is that they are more like workflows than just one-function utilities, since their purpose is to orchestrate the functioning of the app, rather than simply provide access to a partial workflow. The one-off functionality is usually inside the entity instead, since that is where the business logic belongs.

Now is probably a good moment to remember the AACA main diagram:

Main AACA Diagram

How is it different from an Entity?

TL;DR

To put it simply, the Entity is how the process works, and the Use Case is how that process is run inside an app, without caring too much about what the process does.

Where the entity represents business logic, the use case represents application logic. I've struggled with this as well, so here's a distinction between the two:

  • an Entity represents a real world object or interaction. If its functionality wouldn't exist in the real world, then it shouldn't be there. An entity shouldn't know that it's inside an app, except for the necessary abstractizations to be able to interact with it, or for it to perform its functionality.
  • a Use Case however, doesn't know that much about the functioning of the entity, but it does know when to access it and what to do with its results, or which other entities to call next. It is also aware that it's inside an app, so there should be a UI to present to the user, somewhere to fetch the data from or to persist it to, etc.

Fundamentally, the Use Case (and all the other objects inside the domain) is everything the Entity isn't, it's the glue that makes the app more than a set of rules which you have to call from code. The use case still doesn't know HOW the other layers work (for example the UI can be anything), but it knows that this app has to perform some functions, with some layers (for example that the user needs to see program data and maybe even interact with the program).

UI objects and mappers

TL;DR

The Use Case interacts with the UI layer by providing special objects, which are built precisely for this communication, and which are unaware of the tangible implementation of the UI and could theoretically apply to any UI. In order to build these objects, we use mappers.

In order to provide the UI with data to display, without passing our entities to it, we build UI objects. These are simple data classes, with no logic of their own, which simply map the entity data to the kind of data that might be needed for any possible UI that might be showing it. This means that a UI object should be usable in different types of UI, whether it's an app, a website, a simple printer, or a phone call. To reiterate, you should not build these objects to match your current or expected UI, but rather any UI that could possibly exist using this information.

It is very important that, when creating these UI objects, your perspective shifts from the logic used in an entity to the logic used in a UI, regardless of how generic. This means that certain variables from the entity, while adequately representing what they are in that context, need to be renamed or split in order to give the UI more information as to how they should be displayed.

For example, the ubiquitous isValid name from most entities doesn't provide much information to the UI as to how it should be used, so we should give it a better name to suit its functionality, for example canSubmit in a state that also provides a onSubmit callback. You will understand this better as you practice this architecture, but the notion is again of a black box. The people receiving this UI object and State should be able to use it, and know what information it provides, even if they have no knowledge or access to the Domain layer. As such, the perspective of the person building this object may need to shift from what they did in the entity, to provide this extra context as needed.

Also, UI objects don't necessarily need to have the same structure as entities. Just because a nested group of entities makes sense, doesn't mean the UI object can't be simplified, and only include the data from those nested entities. Of course, this may not always be the case, especially with entities that are reused elsewhere in the project, but it's something to consider. For example, if you have a Payment entity, and it contains an Amount entity and a Currency entity, it probably won't matter to the UI that these are separate, and all you need to express in the PaymentUi object is an amount field, a currencySymbol field and maybe a currencyPrecision field to help with formatting the amount (if not already formatted due to business rules).

In order to perform this transformation, my recommendation is to build mappers, which are simple extension functions that take in the entity and return the UI object. This may seem unnecessary, but there are a few benefits to doing it this way:

  • it simplifies the entity and the use case by extracting the mapping logic
  • it hides away some of the logic to convert from entity logic to UI logic (where it's more complex that just passing on the values)
  • it prevents confusion related to where the mapping should be done (entity? use case? etc.)
Practical example

This isn't as much an example, as a practical way of building UI objects. The main purpose of these UI objects is to provide everything a reasonable UI might expect, so that changes to the UI do not result in changes to the domain, which is the main purpose of building all this architecture.

In order to do this easily, I highly recommend thinking about the UI in heavily restricted scenarios, and here are a few examples I always try to consider, and which help me think a bit more laterally (and it also tends to improve UX at times):

  • Think of the UI as an old school dot matrix printer. All of the output is printed, slowly, on expensive paper, in black and white. Users need to wait for it all to be done before they can read it.
  • Think of the UI as a slow and limited input method, like people typing using their TV remote. It's slow and frustrating, so users don't want to have complicated UI. Furthermore, since the process is so complicated, users will usually not see most of the UI you'd expect on a mobile device, instead being focused only at the task at hand.
  • Think of the UI as a phone system. Only the relevant information is read to the user, and they speak back their inputs. Think of how this affects interactions. Think of what happens in case of errors, or if the speech recognition gets their inputs wrong.
  • Think of the UI as an automated system. The information is parsed automatically by an AI. It's also incredibly fast, so reactions to the data can happen in milliseconds. How does that stress impact your use case?

The point isn't to spend a lot of time in building objects for these particular use cases, but rather to approach building UI objects in a generic way. It's ok if you miss details that you need, you can always add them after the fact, but if you get the abstraction right, it helps a lot from the beginning.

States

TL;DR

Another way to simplify the unidirectional dataflow and keep the logic easy to think about is to build States, which simply represent what state of the process we're in, and limit the ability of the UI layer only to interactions that the Use Case permits, for that particular state.

In order to keep the application logic manageable, and to inform the UI as to what to display and what interactions are available to it, each use case uses state objects to both pass data to the UI in a way that is easy to understand and process, as well as to limit the access of the UI only to callbacks that are applicable for the current state.

States are usually a sealed class of objects, and each state usually contains at least one callback, used to allow the UI to signal that a state change is possible, as well as UI objects for most states. They are supplied to the UI as a Kotlin StateFlow, and are the only thing that the UI can access directly from the use case (as well as a similar flow for Events).

Generally, all state classes contain an Init state, used to inform the Use Case that the UI is ready to receive data, and to separate the instantiation of the use cases from actual interactions.

Most user actions will trigger a change in state, which is why I also recommend using an updateState method, which just refreshes the current state with the new data from the entity, mapped as appropriate.

Events

TL;DR

Sometimes, we get one-off events, for example errors or navigation, which don't necessarily impact the current state, but rather require one-off notifications or reactions to them.

While states are enough to represent most actions that take place in an app, sometimes we need to trigger events to perform actions without changing the state that we are in. In my experience, this includes fatal errors that are not linked to a particular state, or other events such as app navigation (although if you implement a platform layer).

Another difference between events and states is that events should be ephemeral, and should only be consumed once. For this reason, they are usually wrapped in an Event class, which simply returns its contents once, after which it just returns null.

They are supplied to the UI as a Kotlin StateFlow, and are the only thing that the UI can access directly from the use case (as well as a similar flow for States).

Of course, if you don't have any events to send to the UI, you can safely skip implementing this flow.

Data/Platform objects, mappers and interfaces

TL;DR

The Use Case interacts with the data and platform layers by providing and receiving special objects, which are built precisely for this communication, and which are unaware of the tangible implementation of these layers. In order to build these objects, or convert them to entities, we use mappers.

Similarly to how the Use Case communicates to the UI, when interacting with the Data Layer or the Platform Layer, the Use Case prepares special objects to aid in communication to and from those layers.

For the data layer, objects going to it are usually called Payload, and objects coming from it are just called Data. So, in essence, we send payloads and receive data, which keeps things easy to reason about.

Just like for the UI objects, these data or platform payloads and data are transformed using mappers, if necessary (for example, in the case of the data layer we rarely need domain level mappers, since we have DTOs which are mapped directly to the domain objects inside the data layer itself).

Finally, for these layers in particular, we can also define interfaces to mediate communication with them, in order to ensure the interaction behaves consistently, regardless of what is going on with the tangible implementations in those layers. This is because both the data and platform layers usually provide more complex tasks, but which return a result. In essence, communication with these layers could also be done via States, but I feel that would complicate the abstraction unnecessarily. The most common interface of this kind you will see is the Repository interface, which essentially instructs the Repository what kind of initial and final data we require, while letting its tangible implementation manage data sources and combine DTOs and processes to create it. More details about that are in the Data Layer documentation.

Testing

TL;DR

Use Cases are built as a linear flow of states, so they can be tested by simply following the normal progression of those states, and even using real entities to simplify testing the outputs. It's also a good idea to test edge cases, such as desyncs from the UI and issues related to garbage collection.

As the use case is its own self-contained entity, it can be tested relatively easily by starting from its initial state, and then going through each state as expected in normal program operation.

I recommend using test fakes for all the components in any other layer. For entities, I have found that using them as is makes for the easiest testing (especially since their internal logic is already covered by unit tests), as most of what the use cases do comes from the logic inside the entities, so it doesn't make much sense to remove that logic and still try to test this behaviour.

I also highly recommend testing of edge cases, such as UI being able to call the wrong state due to a desync, to call the same state multiple times quickly in a row, concurrency issues and finally resilience against garbage collection (depending on the DI you use, I have noticed that on some devices, primarily Samsung, some objects can just be garbage collected even though the use case itself is not, potentially leading to problems).

Finally, from a lifecycle perspective, Use Cases are usually linked to an Android ViewModel in order to ensure resources are freed up when not in use. Tests should be performed to ensure that Dependency Injection restores the correct state (usually the initial state) of a Use Case when the app is killed, and that there are no unintended side-effects from this operation.

What do you mean the app is finished now?

TL;DR

Implementing the Entities, Use Cases, and related objects, mappers and interfaces is the important and infrequently changed part of the app. Once this is done, implementing the other layers is a detail that should be relatively straightforward, and shouldn't lead to us returning to these layers to make them fit.

That's right, now that you have implemented the Entities, Use Cases, Interfaces, Objects and Mappers, you have a complete app. But we don't have a UI, or a database, or API calls or...! It's true, the app currently can't be used by a user, but all the building blocks are there. All those other layer implementations are details, and they can be whatever they want to be, without affecting our core logic. Another benefit of this is that any other layer can be easily swapped to whatever you want, enabling you to test new UI or frameworks, new databases, API changes and migrations, etc., without risking flaws in your core logic.

The point here is to think of the domain layer as a self-contained entity, and work as much as possible without consideration to what your UI, data storage and even platform interactions are going to look like. This is one of the core benefits of Clean Architecture itself, and what it offers is the ability to delay decisions regarding certain components up until the last possible time, giving you the flexibility to choose and change up your mind about (as well as experiment with) the components which are most likely to frequently change. This also means that, once you want to change the way your program works, or the technologies you use, or even the OS you use, it should be much easier to simply replace the components external to the core logic and leave everything else as is, without rewriting critical logic and thus exposing yourself to new bugs you didn't expect, or which you've already previously solved.

Practical example

I once worked on a project that was a somewhat complex implementation of a new banking standard. I painstakingly went through a thick document outlining all the specifications for the standard, and modeled entities based on it, as well as every single error condition that was expected, to a very fine level of precision. In doing so, I even discovered some very unexpected edge cases within the standard, which weren't very intuitive yet had far-reaching consequences and would have otherwise led to crashes. I also discovered some flaws in my implementation when writing tests to fully cover the entities and use cases, which again could have resulted in problems for our users if not corrected. This whole undertaking was significant, and it is my opinion that it was my background in Finance that allowed me to catch some of these issues, and that other developers might have missed them.

However, since I built this new system with AACA (although I hadn't given it a name back then), it was a trivial matter for the iOS developers to simply take my code, convert it to Swift, convert the tests as well, and have all this work and robustness within a couple of days, without even having to go through the standard themselves! If we had implemented our normal "Clean Architecture" (essentially a "domain" class between the ViewModel and Repository, which only did some filtering and sorting), it would have been quite difficult to port it, since the logic would have been split among the various layers, and it would have been very tightly coupled to them.

Also, since I started building this logic from the business logic (basically the standard itself), further additions and changes over time, to the flow of this feature as well as the UI and backend, did not impact the critical logic at all, requiring no complex refactoring, no rewriting of tests, no new bugs or unexpected interactions, since those changes happened primarily in other layers (with only minor changes to the use cases and UI/data objects in order to support new functionality etc.).

TODO add section on navigation!