Book Image

Clean Android Architecture

By : Alexandru Dumbravan
Book Image

Clean Android Architecture

By: Alexandru Dumbravan

Overview of this book

As an application’s code base increases, it becomes harder for developers to maintain existing features and introduce new ones. In this clean architecture book, you'll learn to identify when and how this problem emerges and how to structure your code to overcome it. The book starts by explaining clean architecture principles and Android architecture components and then explores the tools, frameworks, and libraries involved. You’ll learn how to structure your application in the data and domain layers, the technologies that go in each layer, and the role that each layer plays in keeping your application clean. You’ll understand how to arrange the code into these two layers and the components involved in assembling them. Finally, you'll cover the presentation layer and the patterns that can be applied to have a decoupled and testable code base. By the end of this architecture book, you'll be able to build an application following clean architecture principles and have the knowledge you need to maintain and test the application easily.
Table of Contents (15 chapters)
1
Part 1 – Introduction
6
Part 2 – Domain and Data Layers
10
Part 3 – Presentation Layer

Software design principles

In this section, we will analyze a set of design principles that are adopted by developers worldwide to improve their systems and can also be applied to Android development. We will mainly focus on the principles defined by Robert C Martin (also known as Uncle Bob) for classes and components because they are well suited to Android development.

Based on the examples in the previous section, we understand that our code bases should be maintainable, understandable, and flexible. There is a set of software design principles that we can turn to for help when we develop classes or components. Think of a component as the minimum amount of code that can be released as part of a system. In Android, you can view them as individual modules. They don't necessarily need to be modules, but they can be organized as if they are.

SOLID principles

These are some of the most known design principles. The name is an acronym for a set of design principles that were collected by Robert C Martin. These principles are as follows:

  • Single responsibility principle
  • Open-closed principle
  • Liskov substitution principle
  • Interface segregation principle
  • Dependency inversion principle

Let's look at these principles in detail:

  • Single Responsibility Principle: This states that a class should have one responsibility or one reason to change. Looking at our example, let's suppose someone makes a change to the BaseRequest class to change how the HTTP request is executed. Let's assume that we now have two different AsyncTasks that will load the data. Both of these will be impacted by the change in the BaseRequest class. A solution would be to delegate the execution of the request to different classes for each particular use case. This would also allow developers to work on different features related to backend communication without changing the same source file.
  • Open-Closed Principle: This states that a class should be open for extension and closed for modification. Thinking back to our example, this principle would answer the question, "What would happen if an activity requires this particular use case?" The abstractions we discussed in how to answer that question would serve as a good example of implementing this principle.
  • Liskov Substitution Principle: This states that a parent class should be replaced by a child class without changing the behavior of the system. An example of this principle is if you have a class called Bird and a sub-class called Duck. If you are using references of Bird in your code and substitute those usages with Duck, then your code should remain unchanged. A famous example of a violation of this principle is having a Rectangle class with two members named width and height and a sub-class named Square. In reality, a square is a rectangle, but our modeling of a square wouldn't be a rectangle because the rules in Square would mean that the width and height will always have to be the same. If you were to swap these two dependencies, then your code would break.
  • Interface Segregation Principle: This states that we should avoid using large interfaces and instead break them up into smaller interfaces. The idea here is that code shouldn't depend on methods it doesn't use. An example of this is defining interfaces whose methods don't need to be implemented. A good example of this is the approach that's taken in Android user interfaces by separating OnClickListener, OnLongClickListener, and OnTouchListener.
  • Dependency Inversion Principle: This states that we should depend on abstractions rather than concretions. The idea here is to depend as much as possible on abstract classes and interfaces. This can be very difficult to achieve considering that we rely on concretions a lot of the time. Here, we should identify parts of the code that are constantly developed and subject to change and introduce layers of abstractions between our code and these classes. A good way to protect against this is through dependency injection frameworks such as Dagger and Hilt, which generate factories to create volatile components.

SOLID principles are used across the object-oriented programming (OOP) field to create applications that are flexible and able to incorporate new features and requirements. The principles that follow represent an expansion of SOLID.

Component cohesion principles

We can define cohesion by how well the classes in a component belong together or what classes belong in a certain component. In the past, components were assembled based on the context without any particular guiding principle. This would cause issues such as a change in the dependencies of a component triggering a change in the dependants of this component, without this having any relevance to the dependants.

The three principles are as follows:

  • Reuse/Release Equivalence Principle (REP): This states that we group classes in a component that can be released together. In Android development, this would translate to making sure that every module you create should be able to be published and used by other developers.
  • Common Closure Principle (CCP): This states that components should have one reason to change. This principle is an application of the single responsibility principle for components.
  • Common Reuse Principle (CRP): This states that a component should only have classes that should be used together. This represents the interface segregation principle for your component. In Android, this would mean that you should make sure that the users of your Android modules depend on all your classes in the module, not just some.

When these principles are incorporated, they end up conflicting with each other. REP and CCP tend to make components bigger, while CRP tends to make them smaller. The idea is to always match the current requirements of the application and find the middle ground between these principles. After that, you should constantly monitor how new requirements would affect this middle ground.

Now that we've seen how SOLID can be applied to building a particular component through the component cohesion principles, let's learn how to manage a set of components.

Component coupling principles

These principles deal with how to manage the relationships between our components in an Android application. In Android, this would be represented by how to manage the Gradle dependencies between different modules. The principles are as follows:

  • Acyclic Dependencies Principle: This states that we should avoid cyclic dependencies between components. Applying this to Android would mean that the dependencies that our modules have most not be cyclical (for example, module A depends on module B, which depends on module A). Fortunately, this rule is currently enforced by the build system, which doesn't allow cyclical dependencies. A solution to this would be to create a new module in which we apply the dependency inversion principle and make one of the modules depend on the abstraction and create the implementation in the second module. If this is not possible, we can create a new module that can depend on both existing modules. An example of this can be seen in the following diagram:
Figure 1.2 – Cyclic module dependency

Figure 1.2 – Cyclic module dependency

  • Stable Dependencies Principle: This states that less stable modules should depend on more stable modules. A component's stability is defined as the ratio between outgoing dependencies (dependency on other components) and the total number of dependencies. The closer the number is to 0, the more stable a component becomes. This means that stable components should avoid having changes made because this will cause potential issues for the components that depend on the stable ones. One solution to avoid the dependencies between stable components and volatile components would be using abstract components. These are components that will contain nothing but abstractions.
  • Stable Abstractions Principle: This states that components that are likely to change should be more concrete and that stable components should be more abstract. This principle represents an application of the open-closed principle. We would want our high-level architecture decisions to be flexible enough to be changed without having to modify existing source code. We can achieve this using abstract classes. The abstractness of a component is defined as the ratio between the number of abstract classes and interfaces inside a component and the total number of classes in the component. The closer to 1 the value gets, the more abstract the component becomes. A component with 0 stability and 0 abstractness represents a zone of pain because it is very hard to change. A component with 1 stability and 1 abstractness is called a zone of uselessness because we have an independent component with no implementations. The aim is to get as many components as possible in either the 0 stability and 1 abstractness or 1 stability and 0 abstractness range.

With that, we have looked at some of the key design principles that should help us tackle problems that we face while developing an application. The SOLID principles show us how we should structure our code into classes, while the component cohesion principles and component coupling principles show us how we should structure our classes into separate modules, as well as how we should establish the relationships between those modules. In the next section, we will see how these principles lead to the evolution of the Android platform and what an application may look like now.