Skip to main content
· 10 min read

Implementing Clean Architecture in Android

BlogPostImage

In today's rapidly evolving tech landscape, building scalable, maintainable, and testable software has become paramount. One approach that stands out is Clean Architecture, a paradigm that's as effective as it is elegant.

What is Clean Architecture?

It's an architecture where the dependencies are pointed inwards towards the entities at the core of the design. It's all about separating an application into distinct, loosely-coupled layers, each with its own clearly defined responsibility.

Clean Architecture allows us to create a structured codebase that promotes code reusability, testability, and most importantly, scalability. It's like building with Lego blocks – each piece has its place and purpose, and you can keep adding and adjusting pieces as your application grows.

In the next section, we will take a deeper look into the basics of Clean Architecture – the various layers, how they interact. Let's dive in!

Basics of Clean Architecture

Clean Architecture, at its heart, is a layered architecture. Each layer has its distinct role, creating a well-structured system where each part can evolve independently of the others. The structure of Clean Architecture comprises four layers.

Untitled

1. Entities

  • An entity can be an object with methods, or it can be a set of data structures and functions.
  • They encapsulate the most general and high-level rules of the software system. Entities are plain data holders and should be kept simple.
Copy code
// Example of an entity in a blogging app could be a `Post`.
public class Post {
private String title;
private String body;

// getters and setters
}

2. Use Cases

  • Use Cases encapsulate all the use cases of a system, or in other words, the specific business rules. Each use case is an actionable event that the application can perform.
  • They orchestrate the flow of data to and from the Entities and decide how they are created, modified, and destroyed. These operations are performed via one or multiple entities.
// Example of a use case in a blogging app could be `CreatePost`.
public class CreatePost {
private PostRepository postRepository;

public CreatePost(PostRepository postRepository) {
this.postRepository = postRepository;
}

public void execute(Post post) {
postRepository.create(post);
}
}

3. Interface Adapters

  • This layer converts data from the format most convenient for Use Cases and Entities to the format convenient for things such as the Database or the UI.
  • This is where all the conversion logic goes, making data suitable to be presented in the UI or to be stored in the database.
// An example of an interface adapter could be a `PostPresenter`.
public class PostPresenter {
private View view;

public PostPresenter(View view) {
this.view = view;
}

public void present(Post post) {
view.display(post.getTitle(), post.getBody());
}

public interface View {
void display(String title, String body);
}
}

4. Frameworks and Drivers

  • This is the outermost layer and typically includes frameworks and tools such as the database and the web.
  • The purpose of this layer is to provide utility operations such as Database access, Web access, UI frameworks, etc.
// An example from Android could be the Activity or Fragment displaying the posts.
public class PostActivity extends AppCompatActivity implements PostPresenter.View {
private PostPresenter presenter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_post);

// Init presenter and call some method
presenter = new PostPresenter(this);
presenter.present(new Post("Title", "Body"));
}

@Override
public void display(String title, String body) {
// Update the UI here
}
}

One fundamental rule is the Dependency Rule. This rule states that dependencies should point inwards – from outer layers to inner layers. The entities should not depend on anything else, use cases should only depend on entities, and so on. This rule helps maintain the decoupling between layers, resulting in a more maintainable, testable system.

Preparing for Clean Architecture

  1. Understanding of Software Architecture

    Untitled

  2. Familiarity with SOLID Principles

    • SOLID is an acronym for five design principles intended to make software designs more understandable, flexible, and maintainable.
    • These principles form a core part of Clean Architecture and will significantly influence how you design your classes and interfaces. This article provides a good overview of SOLID principles.
  3. Knowledge of Design Patterns

    • They assist in structuring your code, making it more modular, reusable, and easier to understand. Check out this link for more on design patterns.
    • Patterns like Repository, Factory, or Strategy become instrumental when implementing Clean Architecture.
  4. Experience with Unit Testing

    • Clean Architecture promotes testability, and thus unit testing plays an integral role. It's crucial to understand how to write effective unit tests and how different layers of Clean Architecture can be tested independently.
    • This unit testing guide for Android would be an excellent place to start.

Setting up the Project

Once we've established a firm understanding of the principles behind Clean Architecture, it's time to implement it in our Android project.

1. Modularization

  • Breaking down your app into modules will greatly enhance your ability to implement Clean Architecture.
  • With the help of Android's Gradle build system, you can structure your project into modules, each having a distinct role corresponding to the layers of Clean Architecture.

2. Choosing the Right Libraries

  • Implementing Clean Architecture will often involve the use of third-party libraries to help with tasks like dependency injection, network operations, database management, etc.
  • Some of the go-to libraries for these tasks include Dagger or Hilt for dependency injection, Retrofit for network operations, and Room for database operations.

3. Directory Structure

  • Within each module, you can further organize your files into packages based on their functionality.
  • For instance, inside the domain module, you might have packages for entities, repositories, and usecases
domain
├── entities
├── repositories
└── usecases

4. Defining Dependencies

  • As per the Dependency Rule in Clean Architecture, outer layers should depend on inner layers. You can manage this dependency flow using Gradle.
  • Define the dependencies such that the app module depends on all the other modules, and the data module depends on the domain module.

This was a basic project setup with Clean Architecture. Of course, based on the project's complexity and your preferences, you might choose a different project structure.

Unit Testing in Clean Architecture

One of the most powerful advantages of Clean Architecture is how it encourages separation of concerns, which directly leads to more testable code. By isolating dependencies and focusing on business rules, our unit tests become more robust and reliable.

1. Testing Entities and Use Cases

  • As entities are plain data holders, they usually don't have business logic to test. However, our use cases, which contain the business logic, are prime candidates for unit testing.
  • As use cases depend on abstractions (like repositories), we can use mock implementations for testing.
// A simple unit test for a use case
@Test
fun `test getAllPosts returns expected data`() = runBlocking {
val mockRepo = mockk<PostRepository>()
val posts = listOf(Post("1", "Title", "Content"))
coEvery { mockRepo.getAllPosts() } returns posts

val useCase = GetAllPosts(mockRepo)
val result = useCase()

assertEquals(posts, result)
coVerify { mockRepo.getAllPosts() }
}

2. Testing Interface Adapters

  • In Android, ViewModel is often used as an interface adapter. We can also unit test ViewModel by providing mock use cases.
// A simple unit test for ViewModel
@Test
fun `test PostViewModel gets all posts`() = runBlockingTest {
val useCase = mockk<GetAllPosts>()
val posts = listOf(Post("1", "Title", "Content"))
coEvery { useCase() } returns posts

val viewModel = PostViewModel(useCase)
val result = viewModel.posts.getOrAwaitValue()

assertEquals(posts, result)
}

3. Testing Frameworks and Drivers

  • Testing this layer often falls into the realm of Instrumentation tests, such as UI tests.
  • Since they require Android system components, they're often slower and more complex. That's why we aim to keep as much logic as possible in the inner layers, which are easier to test.

Through the effective use of unit tests, we can ensure the integrity of our business logic and facilitate safer code evolution

Potential Challenges

While Clean Architecture offers numerous benefits like testability, independence from UI, DB, and external agency, and organization, there are also trade-offs that we must consider.

  1. Complexity: Clean Architecture requires careful planning and a clear understanding of business rules and entities. For small projects, the overhead may not always be worth it. However, for medium to large-scale projects, this initial setup and planning can pay off significantly in the long run.
  2. Learning Curve: Clean Architecture introduces some concepts, such as inversion of control, use cases, and strict layer segregation, which might be unfamiliar to many developers. It could take some time to grasp these concepts thoroughly.
  3. Development Speed: With its increased complexity and learning curve, Clean Architecture can slow down the initial development process. However, it's also important to note that this approach could speed up development in the later stages of the project, due to fewer bugs and easier feature additions.
  4. Over-Engineering: Clean Architecture encourages a level of abstraction that, if not properly managed, can lead to over-engineering.

FAQs on implementing clean architecture in android

Why is my codebase becoming complex after implementing Clean Architecture?

  • Clean Architecture, by its very nature, introduces a level of abstraction that can increase the complexity of your codebase.
  • To manage this complexity, make sure to properly structure your packages and classes. Naming conventions, regular refactoring, and diligent documentation can also help keep your codebase manageable.

Why is my development speed slow after adopting Clean Architecture?

  • Clean Architecture has an inherent learning curve, which may slow down initial development.
  • Over time, the benefits of maintainable, scalable, and testable code can lead to faster development. Try to invest in learning and understanding the concepts deeply to overcome the initial slowdown.

How can I effectively write unit tests in Clean Architecture?

  • In Clean Architecture, business logic is located in use cases and interactors, making it easy to write unit tests for these classes.
  • When testing classes that interact with Android frameworks (like Activities or ViewModels), consider using libraries like Mockito or Robolectric to mock or stub out Android dependencies.

Clean Architecture is an exciting way to structure our applications. It can help us achieve a high level of abstraction, increase testability, and create a decoupled and scalable codebase. These benefits are highly appealing, especially for medium to large-scale projects that demand maintainability and scalability.

Clean Architecture may be your next step in creating Android applications that are easier to manage, test, and develop. Be brave, take that leap, and keep coding!

Authors
Abhishek Edla
Share

Related Posts