Clean Architecture in Android Project
We all have heard about Clean Architecture. However, It is just a “good thing to do, but lots of work..” for most people. This is right, It is a good approach for projects which need scalability. It comes with flexibility, reusability and testability. This is the answer for questioning why It should be implemented or known.
So, why don’t we implement clean architecture for all of the projects if that amazing? Because implementation of it has a cost, it is better to use when the project is big and complex. There is no exact value for these properties. It just depends on the project’s needs.
This is the common graph to represent the layers in clean architecture which is drawn by Uncle Bob. You can visit the link which is attached to the graph to read clean architecture details from Uncle Bob.
Each circle represents a layer in the application. Note that the arrows are from the outside to the inside. It is the flow of dependency in architecture. That means each circle depends on the closest inward circle. Additionally to this rule, while going to the center circle, abstraction is increased. So, the arrows also from the most abstract layer to the most concrete one. Clean architecture follows the abstraction principle while connecting layers.
To build a clean architecture, clean code is a must. SOLID principles are a group of five design principles to make code cleaner, more flexible and easier to change. Besides, SOLID is not specified to a programming framework or language. The name SOLID is an acronym. Part of the acronyms are;
- Single responsibility principle
- Open-closed principle
- Liskov substitution principle
- Interface segregation principle
- Dependency inversion principle
Before understanding clean architecture, SOLID should be covered. You can find so many resources about SOLID principles, but I want to recommend Berk’s article (https://blog.berkberber.com/solid-principles-in-kotlin-4f37e9b62dde) about it. It is written in kotlin and he uses so clear narration.
Architecture Layers
There are some different thoughts about the number of layers in the clean architecture. Layers are not defined exactly. I want to make it simple for this article to understand the approach, so will define 3 layers; data, domain and presentation layers.
According to clean architecture, the domain layer depends on neither domain nor presentation. That means we are not going to access any other layers. We shouldn’t get confused dependency flow is not across the architecture layers.
Presentation Layer handles interactions with UI. Sometimes, the ViewModel topic is a bit confusing. ViewModel is located in the presentation layer, like View.
Domain Layer is the layer that contains business logic. Even it is a mid-layer between data and presentation, changing data source in the data layer or changing View or ViewModel does not have any effect on the domain layer. UseCase classes whose purpose for occurrence is to contain business logic are in the domain layer. Furthermore, there is a model component that is different from the data model. The domain layer should contain a repository interface that provides independence from the data class.
Data Layer manages network operations like retrieving/sending data from a network or local database. Service endpoints, DataModels, repository implementations, data sources (local/online) are located in the data layer. data models are mapped to DomainModels in the data layer.
Creating Project
Let's organize the project structure. I skipped the part the project is created. (New Project → Empty Activity → Fill Name & Package Name / Min API: 23)
Project structure can be built in some different ways. It is different from layers. We will prefer to create a core module that contains data & domain layers. So, the core module is neither using any framework nor android SDK. By the way, we already have an app default. App module will hold and use android SDK components and represents the presentation layer.
Note that we can also separate data and domain layers into two different modules. But, we will separate them as core modules together.
We just created the core module now. You can see that the Gradle file for the core module is also created automatically. But, we will add the dependency to the core module in the app Gradle. As we mentioned before, the presentation layer was dependent on inner circles, data and domain layers.
//In app gradle, in dependency scope add this line to implement dependency implementation project(':core')
Then, create data and domain packages in the core module. These packages will help us with separation.
About project structure, here is a helpful article that Igor Wojda has written. You can read that article to see some different structures used to implement the clean architecture. Also, he shows us the advantages of each kind of different structure.
Let's start to fill the modules. Before writing the code, I want to summarize the scenario that is going to implement. We will create an application that provides us to track some cryptocurrencies. To get information about coins, we will use CoinGecko API which is free (https://www.coingecko.com/en/api). Before implementation, you can check its documentation. As a first step, we are just going to show 3 coins that have the biggest market cap value. To complete the scenario is up to you. 😌
Firstly, what will we have in the domain package?
The domain will hold a repository, use-cases and models.
Models are data classes using in business logic. They should contain properties only needed (single responsibility). We are going to list coins with summarized data. So we have a CoinEntity like this (even api gives us much more information);
Note that we used serializable because we are going to use this class for network operations.
Repositories are interfaces contain methods for specific purpose thanks to the separation of concerns principle. In our scenario, we have a CoinRepository.
Now, UseCases… It also called Interactors. They are rules which are used for user actions. For almost all actions from users, we have a UseCase. Login, register, start to track a coin, get all coins... In our scenario, the user will list the coins. Then, we should have a use case that returns the list of coins with a use case.
BaseUseCase is a generic abstract class:
I also created a Util package to store some utilization and helper classes. As you see in the repository and use-case implementations, returns are wrapped with the Resource class. I am always in favour of using that kind of class to make calls standard. In this tutorial, I want to make it simple to show the clean architecture, so we will skip the Resource helper wrapper class.
Note that we are using suspend keywords with the functions. It means that function can be blocking. It is entered our life with coroutines instead of callbacks. I highly recommend coroutines if you never use them in a project. In a nutshell, it suspends the execution of the code running in coroutine without blocking the current thread. So, callbacks become unnecessary code blocks. For example, we were using that callback in retrofit (https://square.github.io/retrofit/2.x/retrofit/retrofit2/Callback.html). onFailure and onResponse methods would have different implementations. In suspend functions, execution is suspended until the async method returns.
To get informed about coroutines, you can watch this video from Manual Vicente Vivo in Android Developers Youtube channel (https://www.youtube.com/watch?v=FWxeDqM_WIU&ab_channel=AndroidDevelopers).
Let's fill the data package. It will provide an abstract definition of data sources and implementation of the repository class from the domain layer. In repository implementation, we use the data source to access data. DataSources should be implemented in the framework, app layer. Therefore, repository and data source packages should be created. Then, create these classes:
CoinDataSource:
CoinRepository:
CoinRepositoryImp class extends CoinRepository that is created in the domain layer.
Also, It is better that ApiService class contains endpoints is in the data layer. For now, we only have one endpoint to get the first 3 coins according to market capsize. You can analyze the API information in CoinGecko.
In the data layer, there is also mapper classes to convert API models to entity classes if it is needed. If we need different models from the same endpoint to get data, mapper is unavoidable. In this sample project, I didn’t use to apply the dry principle. If you want to use it, create an interface named Mapper:
Then, implement mapper class for each of model in data layer like (CoinNetwork is the data class which is used to get data from endpoint);
Then, this mapper class should be a parameter in the data source class implemented in the presentation to map from the CoinNetwork object that comes from service to CoinEntity used in domain/presentation.
Let's go to the app module; which has an implementation of framework and presentation. We didn’t use any framework or android SDK in the core module. Here, we will have them. Actually, we are so familiar with this module. Because we always work on only app modules, before clean architecture.
Firstly, let's implement the data source interfaces defined in data layers.
By the way, we will have a general UseCases class to wrap all use cases in ViewModels;
We will implement the MVVM architecture pattern. I will not mention MVVM and its features. In ViewModel, UseCases are going to be used. Because actions come from View and trigger method which brings data from data layer by UseCases. Let's see how it looks but first, we will give use cases as parameters (Application is also passed as a parameter, so we can use it later). We will make use of ViewModelFactory to handle this. ViewModel is also generated from BaseViewMode;
BaseViewModel:
Application Class:
ViewModelFactory:
The rest of the project is building the logic inactivity, fragments and ViewModel. You can see these implementation details in the repository which is stated below. The only thing that I want to left here is calling ViewModel in fragment and ViewModel implementation;
//in fragment
viewModel = ViewModelProvider(this, TFCViewModelFactory).get(ShowAllCoinsViewModel::class.java)//getAllCoins from fragment
viewModel.getAllCoins()
To complete the coding of the scenario, we get and store 3 coins data in the coins live data variable. You can observe that variable in the fragment and set them into an adapter which is used for a recycler view in the fragment. 🍀
Conclusion
Clean architecture is an approach that helps us to organize projects. As I mentioned before, clean architecture needs more code to implement. So, it should be figuring out that implementing a clean architecture for a small project is a mistake. But It absolutely reduces the complexity and it is necessary for big-scale projects. There are so many techniques and design patterns that should be applied for that kind of project (dependency injection is unavoidable for these projects). So, this article does not contain the exact perfect project style. Besides, every project may have its own requirement.
I know that it is a kind of long article but I didn’t want to split the topic. Hope that it was helpful and you learnt something new.
Thank you for reading. Please contact me if you have any questions or comment about the article. 🍀✨
project source:
references: