Working with Clean Architecture on Android

Stelios Papamichail
7 min readJul 23, 2021

You can now find this and other posts over on my substack page at https://thatgreekengineer.substack.com/ !

If you’re like me, reading through Uncle Bob’s guidelines on implementing Clean Architecture may not have been as straightforward as you’d think, especially when it comes to its implementation on Android. In this article, I’ll go over the goal of Clean Architecture, how to go about implementing it on Android, and some further clarifications regarding the implementation details.

What’s its purpose?

Let’s start with what Clean Architecture is trying to solve: limiting the blast radius of change, and specifically managing those areas that tend to change the most often in long-lived code bases. Pretty much everything uncle bob talks about in the architecture space is about that (it’s also the primary reason for the SOLID principles: Clean Architecture is essentially SOLID applied at the architecture level).

For example, if you want to change the persistence model from SQLite to a key-value store, that shouldn’t affect the domain model, views, or comms models at all. If you switch from retrofit to ktor, the persistence models, view, and domain shouldn’t change.

The idea is that changes in pieces of the outer circles don’t affect those in the inner circles. Changes in the innermost circle limit their effect on more outer layers by the use of ports. If the “shape” (type/schema) of the port doesn’t need to change for a specific use case, the repository that use-case uses to make calls doesn’t need to change (hence the use case layer — whose sole purpose is to limit what the outside world can do with the domain to specific, well, use cases).

So I asked myself, are there any differences when it comes to implementing Clean Architecture on Android based on Uncle Bob’s guidelines? Well of course, each framework and platform is different which means that the implementation details may be different and there will likely be tradeoffs. Let’s see those differences.

Clean Architecture on Android

First and foremost, according to Uncle Bob, the data layer should be a pure layer. Meaning, it should be a Java/Kotlin module. This doesn’t work for Android development though since the data layer is the one making network calls and that means that it should be an Android module, with its own dependencies (i.e. Retrofit, Gson, etc.) & manifest (for internet permission). If Retrofit were to release a Kotlin/Java specific version of their library, then maybe the data layer could be converted to a pure module. Let me know if there are other ways to achieve this goal.

Secondly, using a dependency injection library such as Koin or Dagger along with Clean Architecture may seem trivial at first but can raise a lot of questions since it’s not covered in most Clean Architecture guidelines posts that I could find. For example, if we follow uncle bob’s guidelines, the presentation/app layer shouldn’t reference the data layer. But if that’s the case, how would our app notify Koin about the dependency injections in the data layer? This “forces” us to hold a reference to both the domain & the data layer from the presentation/app module. That way, we can now define each layer’s injections in a separate file that will reside in each module separately, and then the presentation module will handle the initialization and lazy injection process.

Wait a minute, if we create a reference to the data module from within the presentation module, aren’t we risking someone mistakenly accessing the files (such as models, repositories, APIs, etc.) of the data layer and causing real trouble? Indeed, this case breaks the Single Responsibility & Dependency Inversion principles. Alright then, what do we do? Well, as with all Architectural patterns, there will be tradeoffs.

In Android’s case, we can keep our reference to the data module but we should make extensive use of Kotlin’s amazing internal keyword. The internal keyword allows us to limit the scope of our data module’s classes, functions, and files to the module itself. Ideally, all models, repository implementations, and API declarations should be declared as internal. You can exclude the dependency injection file of the layer since we need to use it in the presentation module. That way, we can now inject our dependencies across our modules while still preventing anyone from mistakenly accessing and referencing unwanted or “dangerous” classes and files from the outside.

Mistakes in a real-world production project

I recently had to review the architecture of one of our team’s android projects, and a lot of questions occurred to me regarding some best-practices. Let’s go over those questions and remarks one by one.

Initially, I saw that the project contained “duplicate” classes but with different names for the domain and data layers (i.e. NetworkUser & User), but I could not understand the point. The data models were being mapped to the respective domain model classes and that was the only difference I could find. However, I was completely wrong and there is a very good explanation on why things should be done this way.

The reason the entities in the data “layer” are largely a duplicate of those in the domain layer is largely an artifact of the fact that our app hasn’t had to change the persistence model yet. What Clean Architecture gives us is the ability to do precisely that without changing the domain or views. The domain calls domain-defined “ports” whose implementation “adapters” can modify that domain data into the appropriate form used by the outer layer.

If somewhere along the way you were to decide that your comms layer would be better served by switching to, say, MQTT, the domain models won’t care. But that comms layer will define adapter types/methods that the ports will talk to and will transform those domain objects into the appropriate payloads. Those may or may not be a duplicate of those entities in the domain. The key concept is that the reasons they change are different in different layers, and Clean Architecture is applying the Single Responsibility Principle of SOLID to manage that. Onto the second question we go.

My next query regarded the use of both Repositories and Use-cases in the domain layer. They seemed to me like they were doing the same thing and that they were only causing code duplication and costing us extra effort when something needed to change in the domain’s interface. Well, I was wrong about that too, here’s why.

For simple cases, Use-Cases don’t buy us much. They shine when we want to coordinate between multiple repositories. For example, let’s say we have a login API that gives us a token and another “Favorite Items” API that requires a token. These could be different repositories (for example if the login token is also used for other APIs). So your GetFavoriteItemsUseCase depends on both LoginRepository and ItemsRepository and executes the calls in the right order.

The above example mentions multiple repositories but it doesn’t even have to be that. It could also be that a use case requires us to perform multiple operations on a repository. While you could have your repository do this combined operation, it is often cleaner to have the repository do only “leaf” operations and have use cases combine them.

For example: suppose we want to calculate a route to a destination from your current location. Your repository could have a calculateRouteFromCurrentLocation(destination: Location) method. However, it is cleaner to have getCurrentLocation() and calculateRoute(source: Location, destination: Location) in your repository. Then your CalculateRouteFromCurrentLocationUseCase can call those methods in the right order.

Another advantage of Use Cases is reusability. The “get login token” part above could be extracted into a LoginUseCase which is then reused by several other use cases.

In this example, the login just returns the token from the server but you could also include business logic in there like validation. Even for simple pass-through use-cases, it makes sense to validate or otherwise transform the data that the repository returns. This way you achieve a high level of testability for your business logic!

So these were some of the answers that other developers gave me over on Reddit regarding my first two questions. My final question though, had nothing to do with specific platform/framework implementation details but rather with the architecture’s implementation in general.

Conclusion

I wanted to know if it is even possible to completely abide by Clean Architecture guidelines in a real-world project while still providing the functionality needed by it. The gist of the answers I got from other developers was that this is not a binary answer. It is possible to adhere to Clean architecture principles to a very high degree but there will always be cases where you violate one principle or another due to various limitations (whether those are attributed to the framework or to the architecture). Keep in mind that Clean Architecture is simply a single architectural style, and like all architectures, they have a sweet spot for where they apply. Responsible software engineers need to take that applicability into account when choosing an apps architecture.

--

--

Stelios Papamichail

Android developer, Comp. Science student & fitness enthusiast