The most practical Android architecture design principles

The most practical Android architecture design principles

Before we begin, I assume you have read my previous article “Architecting Android…The clean way?” If you haven’t read it, you should take this opportunity to read it to better understand this article:


Architecture evolution

Evolution implies a gradual process of change from some state to a different state, where the new state is usually better or more complex.

With that said, software evolves and changes over time, architecturally. In fact, good software design must help us evolve and scale solutions, keeping them robust, without having to rewrite everything (although in some cases rewriting is better, but that's a topic for another article, so trust me, let's focus on the topic discussed above).

In this article, I will go through the points that I think are necessary and important. To keep the basic code clear, keep the following picture in mind. Let’s get started!


Responsive approach: RxJava

I am not going to discuss the benefits of RxJava here (I assume you already have some experience with it) as there are already many articles on this topic and there are great and impressive people doing this. However, I will point out the interesting aspects of it in terms of Android application development and how it helped me to form a clear architectural approach.

First, I chose a reactive pattern by converting the usecase (called interactor in this clear architectural naming convention) to return Observables<T>, indicating that all underlying layers follow this chain and also return Observables<T>.

As you can see, all use cases inherit from this abstract class and implement the abstract method buildUseCaseObservable(). This method will build an Observables<T>, which does the heavy lifting and returns the required data.

It is important to emphasize that in the execute() method, we must ensure that Observables<T> is executed in a separate thread, so as to minimize the degree of blocking the Android main thread. As a result, the main thread will be pushed back to the end of the thread queue by the Android main thread scheduler.

So far, we have our Observables<T> up and running. However, as you know, the sequence of data emitted must be observed. To do this, I improved presenters (part of the presentation layer of the MVP pattern) and turned them into observers (Subscribers), which "react" to the emitted items through use cases in order to update the user interface.

The observer is this:

Each observer is an inner class of each presenter and implements a Defaultsubscriber<T> interface, which creates basic default error handling.

Putting all the pieces together, you can get the full concept from the following diagram:

Let’s list some benefits of moving away from an RxJava based approach:

Decoupling between Observers and Observables: Easier to maintain and test.

Simplify asynchronous tasks: If multiple asynchronous executions are required, if more than one level of asynchronous execution is required, the operation and synchronization of Java's thread and future are more complicated, so by using a scheduler, we can easily (without extra work) jump between the background and the main thread, especially when we need to update the UI. It can also avoid the "callback pit" - it makes our code less readable and difficult to follow.
Data transformation/composition: We can combine multiple Observables<T> without affecting the client, making the solution more flexible.
Error handling: When an error occurs in any Observables<T>, it is signaled to the consumer.

From my perspective there is a downside, and even a price to pay, in that developers who are not yet familiar with the concepts still have to follow a learning curve. But you get something extremely valuable out of it. Be reactive for success!
Dependency Injection: Dagger 2

I don’t want to say too much about dependency injection since I’ve already written a whole article about it. I highly recommend you to read it so that we can continue with the following content.

It is worth mentioning that by implementing a dependency injection framework like Dagger 2 we can gain:

Component reuse, because dependent objects can be injected and configured externally.
When injecting objects as collaborators, since the instances of the objects exist in an isolated and decoupled place, we can change the implementation of any object in our code base without making many changes.
Dependencies can be injected into a component: This makes it possible to inject mock implementations of these dependencies into the component, which makes testing easier.

Lambda Expressions: Retrolambda

No one will complain about using Java 8 lambda expressions in your code, and even more so after simplifying it and getting rid of a lot of boilerplate code, as you can see in this code:

However, I have mixed feelings about this, why? We were discussing Retrolambada at @SoundCloud, mainly whether to use it, and the result was:

1. Reasons for support:

Lambda expressions and method references
"try-with-resources" statement developed using karma

2. Reasons for objection:

Unexpected use of Java 8 APIs Very offensive third-party libraries Third-party plugins to be used with Android Gradle

Finally, we decided that it doesn't solve any problems for us: your code looks nice and readable, but it's not something we want to live with, and since all the most powerful IDEs now include code folding options, this need is covered, at least in an acceptable way.

To be honest, although I might use it in a project in my spare time, the main reason for using it here is to try and experience Lambda expressions in Android. It is up to you to use it or not. I am just showing my vision here. Of course, the author of this library deserves my praise for such an amazing work.
Test Method

In terms of testing, not much has changed since the first version of the sample:

Presentation layer: Test the UI with Espresso 2 and Android Instrumentation testing framework.
Domain layer: JUnit + Mockito — It is a standard module of Java.
Data layer: The test combination was changed to Robolectric 3 + JUnit + Mockito. This layer of testing used to exist in a separate Android module. Since there was no built-in unit testing support at the time (the first version of the current sample program), there was no framework like robolectric, which was complicated and required the help of a group of hackers to make it work properly.

Luckily, this is all part of the past, and now everything is available out of the box, so I can move them back into the data module, specifically for its default test path: src/test/java.
Package Organization

I think one of the key factors of a good architecture is code/package organization: the first thing a programmer encounters browsing source code is the package structure. Everything flows from it, everything depends on it.

We can identify two paths for packaging applications into packages:

Package by layer: The items contained in each package are usually not closely related to each other. This leads to low cohesion and modularity of the packages, and high coupling between packages. Therefore, editing a feature requires editing files from different packages. In addition, it is almost impossible to delete a feature in a single operation.
Package by feature: Use packages to represent feature sets. Put all items related to a feature (and only that feature) into one package. This way, packages have high cohesion, high modularity, and low coupling between packages. Closely related items are kept together. They are not scattered throughout the application.

My suggestion is to remove the feature-based packaging, which will bring the following main benefits:

More modularity. Easier code navigation. Minimized scope of features.

It can also be really fun to work with feature teams (like we do at @SoundCloud). Code ownership is easier to organize and modularize. This is a win in growing organizations where many developers share a codebase.

As you can see, my approach looks like packaging by layers: I could make mistakes here (for example, organizing everything under "users"), but I'll forgive myself in this case, because this is an example for learning purposes, and I want to show the main concept of a clear architectural approach. Get the idea, don't copy blindly :-).
What's left to do: Organize your build logic

As we all know, a house is built from the foundation. The same is true for software development, and I would say that from my point of view, the build system (and its organization) is an important part of software architecture.

On the Android platform, we use Gradle, which is actually a platform-agnostic build system with very powerful features. The idea here is to simplify the organization of your application building through some tips and tricks.

Grouping stuff by functionality in separate gradle build files

Therefore, you can insert it into any Gradle build file with "apply from: 'buildsystem/ci.gradle'" to configure it. Don't put everything in one build.gradle file, otherwise you will create a monster, this is the lesson.

Creating a dependency graph

This is great if you want to reuse the same component version between different modules of your project; otherwise, you have to use different versions of component dependencies between different modules. Another point is that you control dependencies in the same place, and things like component version conflicts can be seen at a glance.
Conclusion

Having said so much so far, in one sentence, remember that there is no panacea. But a good software architecture will help the code remain clear and robust, and also keep the code scalable and easy to maintain.

I want to point out a few things. When faced with software problems, treat them as if they should be solved:

Follow SOLID principles <br /> Don’t overthink (don’t over-engineer)
Be pragmatic
<br /> Reduce the dependencies of modules in the (Android) framework as much as possible

<<:  Microsoft releases Android desktop app

>>:  The correct way to use the prompt box in iOS9

Recommend

5 signs that you may have sarcopenia! Be sure to do these two things!

Whether you are at work or at home recently In fa...

The eye of the storm on Jupiter: More than just a big red eye

James Webb Space Telescope Observes Jet Stream St...

WeChat emoticons have been included in the judgment

[[384879]] According to the Legal Daily on March ...

Lays Potato Chips-Brand Marketing Strategy!

As the saying goes, food is the most important th...

Can Internet companies subvert the air purifier market?

For quite a long time, consumers' choices for...

What skills do event planners need to have to get a high salary?

What is the core competitiveness of event plannin...

【APP Promotion Tips】Thoughts on Online Promotion

introduction: When I was setting the topic, I wan...

Astronauts' life in space: both fun and trouble

Compared with the ground, the microgravity enviro...

What happens when you flush the toilet?

© The Independent Leviathan Press: The toilets we...

How UGC establishes content order!

I have said before that we should consider the lo...

How can products be promoted effectively?

Many people write brilliant copy and make beautif...

Why are nutritious goose eggs not often eaten and rarely sold?

Goose eggs, as a nutritious egg, rarely appear on...