In my this a few years to learn and grow, profound consciousness to build an Android application framework is a very painful thing, it is not only to meet the growing needs of the business, but also ensure the neatness of architecture itself, and this make it very challenging, but we have to do so, because of robust Android architecture is the foundation of a good APP. The code samples for this article are available from Github at * Android-easy-Clean Architecture.
Why we need an architecture?
The Android Framework does a lot of things for us. You can even write a simple APP without having to learn more, like putting a few views in an Activity or Fragment to display on the screen. Time-consuming tasks in the background are executed in services, and data is Broadcast between components, so “anyone can be an Android engineer”, is that really true?
Of course not!!
If we start programming so naively, we’ll pay for it sooner or later. Confusing dependencies and inflexible code will be our biggest obstacle, leaving the project in such a mess that it will be difficult to add new features and we will have to refactor or override it. Before we start programming, we should not underestimate the complexity of an application. We should think of your APP as a complex system with front-end, back-end, and storage features.
In addition, in the field of software engineering, there are always some principles worth learning and abiding by, such as: single responsibility principle, dependency inversion principle, avoid side effects and so on. The Android Framework doesn’t force us to follow these principles, or it doesn’t limit us at all. Think of tightly coupled implementation classes, activities or fragments that handle a lot of business logic, ubiquitous EventBus, hard-to-read data flow, and messy callback logic. They don’t cause the system to crash right away, but as the project grows, they become difficult to maintain and even difficult to add new code, which can be a terrible impediment to business growth.
Therefore, it is very important for developers to have a good architectural guidance specification.
Working on Android, I always thought we could make the APP better. I’ve seen a lot of good and bad software designs, and I’ve done a lot of different things myself. I kept learning and making changes until I met Clean Architecture, and I was sure this was what I wanted, and I decided to use it. The goal of this article is to share my experience building projects with Clean Architecture and hopefully inspire you to improve your projects.
Avoid God Activity
Perhaps for “fast iteration,” you’ve integrated this all-purpose Activity that can do anything:
- Manage your own lifecycle (work on tasks in the right lifecycle)
- Maintain UI state (save/restore view state when configuration changes)
- Handling intents (receiving and sending the right intents)
- Data update (synchronizing data with remote apis, local storage)
- thread
- Business logic……
Breaking through all the constraints: adding business code to the Android world; In BaseActivity, you define all the variables that subclasses can use, and so on. It is indeed a God now, a convenient and omnipotent God!
As the project grows, it becomes too big to add code to, so you write helper classes for it, and you want to refactor it:
Without realizing it, you’ve planted a black bomb.
It seems that the business logic has been moved into the help classes, and the code in the Activity is reduced and less bloated. The help classes take the pressure off the “universal classes”, but as the project grows, the business expands, and the help classes become more numerous, it’s time to continue to break them down by business. APIHelperThis, APIHelperThat, etc. The old problems resurfaced, the cost of testing was still there, the cost of maintenance seemed to have increased, and the messy and hard-to-reuse programs were back, and all our efforts seemed to have been wasted.
However, what is the purpose of writing this universal class? Do you want to use some functions quickly and easily, especially if you want to get them quickly in the subclasses?
Of course, some people will separate out different abstract classes based on different business functions, but they are still versatile compared to that business scenario.
Regardless of the reason for the creation of “god” is should avoid as far as possible, we should not focus on writing those a lot of classes, but the effort to write the ease of maintenance and testing of low coupling class, if you can, it is best not to let the business logic into pure Android world, this also is I have been trying to target.
Clean architecture and The Clean rule
This “onion” ring is Clean Architecture. The different colored “rings” represent different system structures that make up the whole system. The arrows represent dependencies.
I won’t go into the details of its composition here, because there are too many articles that are better and more detailed than I am. It is also worth mentioning that Architecture is designed for software, it should not be language differences, and this article will focus on how to build your Android applications with Clean Architecture.
Before using clean architecture to build projects, I read a lot of articles and put many practices into practice. I learned a lot. My experience and lessons taught me that there are three principles that make an architecture clean and tidy:
- Hierarchical principle
- Rely on the principle of
- Abstract principle
So let me explain what I think these principles are and why.
Hierarchical principle
First of all, it’s worth noting that the framework doesn’t limit how many layers you can layer your application into. You can have as many layers as you want, but on Android I usually divide it into three:
- Outer layer: Implementation layer
- Middle layer: interface adaptation layer
- Inner layer: business logic layer
Next, let’s look at what these three layers should contain.
Implementation layer
In short: The implementation layer is the Android framework layer. This place should be a concrete implementation of the Android Framework, and it should include all Android stuff. That is to say, the code here should solve Android problems, be platform-specific, and implement specific details, such as jumping to activities, creating and loading fragments, Handling intEnts or starting services.
Interface adaptation layer
The purpose of the interface adaptation layer is to connect business logic to framework-specific code and act as a bridge between the outer layer and the inner layer.
Business logic layer
The most important layer is the business logic layer, where we address all the business logic. This layer should not contain Android code and should be able to test it without the Android environment, meaning that our business logic can be tested, developed and maintained independently. This is the main benefit of the Clean architecture.
Depend on the rules
Rely on rules consistent with the direction arrow, outer “rely on” inner, here “rely on” does not mean that you write in your gradle the dependency of the statement, should understand it into “see” or “know”, outer know lining, on the contrary the inner don’t know the outer, inner or outer know is how to define the abstract, The inner layer does not know how the outer layer is implemented. As mentioned earlier, the inner layer contains the business logic and the outer layer contains the implementation details, combined with the dependency rule that the business logic neither sees nor knows the implementation details.
For a project, the exact dependency is entirely up to you. You can divide them into different packages and manage them through the package structure. Be careful not to use external package code in internal packages. Using package is very simple, but also exposed the fatal problem at the same time, if someone don’t know depend on the rules, may write the wrong code, because this kind of management mode can not stop the destruction of the people to rely on the rules, so I prefer to conclude their different Android module, adjusting the dependent relationships between the module, The inner code does not know that the outer layer exists at all.
It’s also worth noting that while there’s nothing stopping you from skipping adjacent layers to access code from other layers, I strongly recommend only accessing data from adjacent layers.
Abstract principle
In the dependency principle, I’ve alluded to the abstraction principle, the more abstract something is as it moves from side to center in the direction of the arrow, and the more concrete it is as it moves from side to side. The inner circle contains the business logic and the outer circle contains the implementation details.
I’ll use an example to illustrate the principle of abstraction:
On the one hand, the business logic can use it directly to display notifications to the user. On the other hand, we can implement this interface on the outer layer, using NotificationManager provided by the Android Framework to display notifications. The business logic uses only the notification interface and does not know the implementation details, how the notification is implemented, or even that the implementation details exist.
This is a good example of how to use the principle of abstraction. When abstractions are combined with dependencies, business logic using abstract notifications can’t see or know the implementation using the Android notification manager, which is exactly what we want: business logic doesn’t notice the implementation details, much less know when it will change. The principle of abstraction helps us do this very well.
Apply on Android
Following the layering principle mentioned above, I split the project into three layers, meaning it has three Android Modules, as shown below:
Business logic rules are defined in Domain, interface interaction is realized in UI, and Model is the concrete implementation of business logic (Android Framework). The arrow direction represents the dependency relationship, the inner layer is abstract, the outer layer is concrete, the outer layer knows the inner layer, the inner layer does not know the outer layer.
The specific framework structure of Android is shown in the figure below:
You might be a little confused, why is Domain pointing to Data? Since the Domain contains the business logic, it should be the center of the application. It should not depend on the Model. According to the principle mentioned earlier, Domain is abstract, Model is concrete, and Model depends on Domain, not Domain depends on Model.
This is easy to understand and I always emphasize that “dependency” does not mean dependency configured in Gradle. You should think of it as “knowing”, “knowing”, “being aware”. The arrows in the diagram represent the invocation relationships, not dependencies between modules. We should be able to understand: abstraction is theory, dependency is practice, abstraction is logical layout of application, dependency is combination strategy of application. When it comes to understanding framework architecture, we should go beyond the code level and not be confined to conventional thinking, otherwise we will soon fall into a cycle of logic chaos.
Corresponding to the invocation relationship is the direction of the data flow:
It accepts user behavior in app, accesses real data in Model according to business rules defined in domain, and then returns in turn, and finally updates the interface, which is a complete data flow direction.
To make it easier to understand, I took the project apart and added a use-case description of the class to the diagram, which looks something like this:
To sum up what is shown above:
First, the project is divided into three layers:
- App: UI, Presenter…
- Domain: Entity, Use case, Repository…
- Model: DB, API…
Second, more detailed submodule division:
UI
View, which contains all Android controls and is responsible for displaying the UI.
Presenter
Handles user interactions, invokes the appropriate business logic, and sends the data results to the UI for rendering. In other words, Presenter is responsible for the interface adaptation layer, connecting the Android implementation to the business logic, and taking care of data delivery and callbacks.
Entity
Entities, business objects, are the core of your app. They represent the main functions of your app, and you should be able to judge the functionality of your app by looking at them. For example, if you have a news app, those entities will be sports, cars or finance.
Use case
Use cases, interactors, or business services, are extensions of entities as well as extensions of business logic. They contain logic that is not specific to one entity, but can handle many entities. A good use case would describe what is being done in colloquial language, for example, a transfer could be called TransferMoneyUseCase.
Repository
Abstract cores, which should be defined as interfaces that provide inputs and outputs to UseCase and can perform operations such as CRUD directly on entities. Or they can expose more complex operational behaviors, such as filtering, aggregation, etc., the implementation details can be implemented by the outer layer.
DB&API
The implementation of both the database and the API should be placed here, as in the example above, where the DAO, Retrofit, JSON parsing, and so on could be placed. They should be able to implement the interfaces defined in Repository, which are implementation details that allow direct manipulation of entity classes.
Show code
You can combine your MVPView and MVPPresenter to make them easier to manage and maintain, as demonstrated in the previous UML diagram.
First I define BaseView and BasePresenter. In BaseView I use the RxJava Observable as the result type. :
public interface BaseView<T> {
void showData(Observable<T> data);
void showError(String errorMessage);
}
Copy the code
public interface BasePresenter<V> {
void attachView(V view);
void detachView(a);
}
Copy the code
Suppose you have a requirement to get movies playing in that city based on its ID, then you can combine your MovieView and MoviePresenter interfaces like this:
interface MovieContract {
interface Presenter<Request.Result> extends BasePresenter<View<Result>> {
void loadData(Request request);
}
interface View<Result> extends BaseView<Result> {
void showProgress(a); }}Copy the code
The addition of generics effectively ensures the type safety of data.
Next, implement your own XXXPresenter and XXXView implementation classes like this:
class MoviePresenterImp implements MovieContract.Presenter<MovieUseCase.Request.List<MovieEntity>> {
@Override public void attachView(UserContract.View<List<MovieEntity>> view) {
/*subscribe MovieUseCase and do some initialization*/
}
@Override public void detachView(a) {
/*unsubscribe MovieUseCase and release resources*/
}
@Override public void loadData(MovieUseCase.Request request) {
/*load data from MovieUseCase*/}}class MovieActivity extends AppCompatActivity implements MovieContract.View<List<MovieEntity>> {
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/*also initialize the corresponding presenter*/
}
@Override public void showData(Observable<List<MovieEntity>> data) {
/*show data and hide progress*/
}
@Override public void showError(String errorMessage) {
/*show error message and hide progress*/
}
@Override public void showProgress(a) {
/*show progress*/}}Copy the code
The UseCase.Request in the example comes from Clean Architecture: Dynamic Parameters in Use Cases: Create a static internal class Request in XXXUseCase as a container for Dynamic Request Parameters. This makes sense and is entirely true, because UseCase is where you define business rules, and combining business (request) conditions with business rule definitions is not only easier to understand but also more manageable. But IN the next article I’ll introduce another dynamic parameter approach that I’ve been using.
Conclusion:
I believe you and I, in the process of building frame in all kinds of challenges, learn from your mistakes, and constantly optimize the code, adjust the dependencies, even reorganize module structure, these changes are you want to let the architecture become more robust, we have been hoping that applications can become easy to develop easy to maintain, this is the true sense of the team to benefit.
I have to say that there are many different ways to build an application architecture, and I don’t think there is a one-size-fits-all architecture, it should be iterative and tailored to the business. So, you can try to build your application with the business in mind by following the ideas provided in this article.
It is also worth noting that if you want to do better, you can add templating, componentization, etc., to your project, because there is no one framework for a project. 🙂
Finally, I hope this article will be helpful to you. If you have other better architectural ideas, please share them with me.