Android App Architecture Ground Up

Per-Erik Bergman
AndroidPub
Published in
13 min readApr 5, 2017

I have been jumping back and forth regarding the best architecture for android apps for some time now, and have talked to friends, listen to lectures, reading articles and tested a lot of different approaches. This is my attempt to explain why I have chosen the one I am currently using. And why I think it is a good approach on architecture for Android Apps.

My more then 20 years of programming experience tells me that following the S.O.L.I.D principles is the way to go. With that in mind I have looked over different patterns and structures and I believe I have found a good approach to a good solid architecture.

First of all, I hate when I have to read a long blog post just to get to the point so I will start with the conclusion and then explain why.

Conclusion

I think the three layer architecture is a good foundation. I keep the domain model in the Data Access Layer together with the Repository pattern for managing them. The Data Access Layer should only have the core models of the domain and the CRUD methods surrounding them, it might be that we have different apps working within the same domain but have different purpose. So no other logic in this layer than the models and the way to manage them.

In the Business Logic Layer I put my use cases that is the logic that interacts with one or more of the core models.

In the Presentation Layer I use the View-Model-Presenter-ViewMode pattern since that is the simplest display pattern that follow the S.O.L.I.D principles.

The final architecture would look similar to this image below.

Android App Architecture

The implementation details that helps me solve this architecture are data binding for the Presentation Layer and RxJava for the Business Logic Layer and the Data Access Layer.

This is my current preferred architecture. I say current because I try to have an open mind that there might be a better way and I will continue looking at different architectures in the search for a better option. So let us look into why I have chosen this. Let us have a quick look at the foundation, the S.O.L.I.D Principles.

S.O.L.I.D Principles

The idea behind these principles is that, when they are applied together, they will make it easier for a programmer to create a system that is very cohesive, decoupled and it will be easy to maintain, extend and test.

Single responsibility principle

a class should have only a single responsibility (i.e. only one potential change in the software’s specification should be able to affect the specification of the class)

This is the most important principle of them all so it is just fitting that it is the first one.

I define a single responsibility in a way that I can with a simple sentence say what the class is about. This sentence should be as specific and narrow as possible.

Example 1: An adapter should convert domain objects into visual components.

Example 2: A ViewModel should deliver formated data to the view.

Example 3: A Repository should handle the lifecycle of one type of domain model.

Bad example: An adapter should download and convert domain objects into visual components.

Open/closed principle

software entities … should be open for extension, but closed for modification.

This means that we should write our code so it is extendable by adding new classes not by modifying existing classes.

A common way of explaining this principle is the area calculation example. Say we have a several rectangle objects and we want to calculate and summarise the total area. The rectangle class looks like this:

public class Rectangle {

private float mWidth;
private float mHeight;

public Rectangle(final float Width, final float height) {
mWidth = Width;
mHeight = height;
}

public float getWidth() {
return mWidth;
}

public float getHeight() {
return mHeight;
}
}

Now if we should calculate the area a function for that could look like this:

public class AreaCalculation {

public static float calculate(Rectangle... rectangles) {
float sum = 0;
for (Rectangle rectangle : rectangles) {
sum += rectangle.getHeight() * rectangle.getWidth();
}

return sum;
}

}

If we need to add another shape say a circle like this:

public class Circle {

private float mRadious;

public Circle(final float radious) {
mRadious = radious;
}

public float getRadious() {
return mRadious;
}

}

We need to modify the AreaCalculation class like this:

public class AreaCalculation {

public static float calculate(Object... shapes) {
float sum = 0;
for (Object o : shapes) {
if (o instanceof Rectangle) {
Rectangle rectangle = (Rectangle) o;
sum += rectangle.getHeight() * rectangle.getWidth();
} else if (o instanceof Circle) {
Circle circle = (Circle) o;
sum += circle.getRadious() * circle.getRadious() * Math.PI;
}
}

return sum;
}

}

This means the AreaCalculation is not closed for modification. But if we add a shape interface and update the shapes like this:

public interface Shape {
public float getArea();
}
public class Rectangle implements Shape {

private float mWidth;
private float mHeight;

public Rectangle(final float Width, final float height) {
mWidth = Width;
mHeight = height;
}

public float getWidth() {
return mWidth;
}

public float getHeight() {
return mHeight;
}

public float getArea() {
return mWidth * mHeight;
}
}
public class Circle implements Shape {

private float mRadious;

public Circle(final float radious) {
mRadious = radious;
}

public float getRadious() {
return mRadious;
}

public float getArea() {
return (float) (mRadious * mRadious * Math.PI);
}
}

We can easily change our AreaCalculation class to be closed for modification like this:

public class AreaCalculation {

public static float calculate(Shape... shapes) {
float sum = 0;
for (Shape shape : shapes) {
sum += shape.getArea();
}

return sum;
}

}

Now we have a system that is open for extension by adding new classes and closed for modification since we don’t need to modify any existing code.

Liskov substitution principle

objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.

Continue with the same context of shapes this means that if we have a new class called Square that inherit the Rectangle. This new class can replace the Rectangle and the area calculation will still be intact and work with no alteration to the program.

A really bad example would be if we have a sub-class called Sound that inherit the Rectangle and every time getArea() is called it plays a sound. This would most definitely change the correctness of the program.

Interface segregation principle

many client-specific interfaces are better than one general-purpose interface.

This means that we should separate the different functions into smaller interfaces so we don’t have one big giant interface.

So if we are implementing a software for an app that should be able to load and save data from our backend and also upload files to dropbox. It should not look like this:

public interface API {
public Data load();

public void save(Data data);

public void upload(File file);
}

But rather look like this:

public interface DataAPI {
public Data load();

public void save(Data data);
}
public interface FileAPI {
public void upload(File file);
}

This makes it way easier to reuse of change implementation for different parts of the apps API.

Dependency inversion principle

one should depend upon abstractions, not concretions.

Our Presenter class should not depend on a concrete implementation for fetching data from network but rather depend on an abstraction that could be a Repository class. This means that the Presenter should not know about the implementation details in the Repository class, it should not know if the models are fetched from local database or a remote server.

Bottom-Up or Data First

My thought on how to develop an application is from the bottom up, aka data first, meaning I’ll start with the foundation as in the Data Layer Access and work my way up thru the Business Logic Layer towards the Presentation Layer. The reason for this is pretty simple, to completely implement a class I need all it’s dependencies to be implemented first otherwise I have to implement a mocked dependency meaning a lot of temporary code just to be able to finish the implementation of the class. So by start with the classes with the least number of dependencies first will make me implement less code. My order is usually something like this: Domain Model, Repository, Use Cases, Presenter, ViewModel and finally the View.

Say that the API isn’t done yet, then we need to fake the Repository, but that is all we need to fake to do the rest of the application.

File Structure

The file structure is also really important there is a lot of blog out there about why one specific structure should be followed. The two most talked about is the package-by-layer and the package-by-feature. Personally I believe that a combination of them both are the best way.

I like to group the files by probability of reuse. I have three groups of probability; high, medium, low.

Data Access Layer

The first group is the data it is the part of the app that is most likely to be reused because there might be several apps using the same data set in different ways, so that is the “high” group also known as the Data Access Layer.

Business Logic Layer

The second group would be the use cases. There might be different apps with the same use cases but different UI/UX an example could be an app with different flavours or a phone vs. tablet version. So this goes into the “medium” group also known as the Business Logic Layer.

Presentation Layer

The last group is the part with the least probability to be reused, the UI/UX. So this goes into the “low” group also known as the Presentation Layer.

Group by feature not by layer

As soon as we go into one of these three layers we should switch from package-by-layer into package-by-feature.

This means that I will have everything that belongs to the same feature (domain model/screen) in the same package.

This gives us high modularity and cohesion and low coupling, it also scale good.

An example of grouped by feature looks like this:

01 .
02 |-- build.gradle
03 |-- settings.gradle
04 `-- src
05 |-- app
06 | `-- Application.java
07 |-- screen
08 | |-- auth
09 | | |-- AuthView.java
10 | | `-- AuthViewModel.java
11 | |-- list
12 | | |-- ListView.java
13 | | `-- ListViewModel.java
14 | `-- item
15 | |-- ItemView.java
16 | `-- ItemViewModel.java
17 |-- model
18 | |-- AuthModel.java
19 | |-- ListModel.java
20 | `-- ItemModel.java
21 |-- network
22 `-- database

Data Access Layer

As I said I’d like to work my way up starting with the Data Access Layer so let’s start there and move up.

File Structure

First of all, the grouping inside this layer would be like this:

01 .
02 |-- build.gradle
03 |-- settings.gradle
04 `-- src
05 `-- com.library.archive
06 |-- model
07 | |-- author
08 | | |-- Author.java
09 | | `-- AuthorRepository.java
10 | `-- book
11 | |-- Book.java
12 | `-- BookRepository.java
13 `-- network

These repositories might share common network code, that code should be move up as you see in the example above.

Domain Model

I like my domain models to be as stupid as possible, as close to a POJO as I can get. I write “as close as” because I do think that it is okey to add annotations etc. to the class to help with serialisation. Like this example below:

public class Entity {    @SerializedName("id")
private long mId;
@SerializedName("name")
private String mName;

public long getId() {
return mId;
}

public void setId(final long id) {
mId = id;
}
public String getName() {
return mName;
}

public void setName(final String name) {
mName = name;
}
}

Repository Pattern

I chose to work with the repository pattern because I think it is easy to implement and it is also easy to explain to others. The Repository Pattern’s methods are the CRUD (create, read, update and delete). There are several different interpretations of this pattern some to simple and some way to complex. I do think this is a good middle way. I use RxJava when implementing this pattern because it makes it easier. When moving up towards the use cases.

public interface Repository<T, K> {

Observable<List<T>> get();

Observable<T> getById(K id);

Observable<T> create(T entity);

Observable<T> update(T entity);

Observable<T> delete(T entity);

}

The generic parameters are T; the model’s data type and K; the model’s id type.

Implementing a repository for the Entity model we start with an interface.

public interface EntityRepository extends Repository<Entity, Long> {

Observable<List<Entity>> get();

Observable<Entity> getById(long id);

Observable<Entity> create(Entity entity);

Observable<Entity> update(Entity entity);

Observable<Entity> delete(Entity entity);

/* Entity Specific Methods */

Observable<List<Entity>> getNameContaining(String str);
}

The reason we do this as an interface if because there are lot of times where you need a local mocked repository for testing the rest of your code. Maybe the backend API isn’t ready yet or internet is down or… any other of many reasons.

You might have noticed that I don’t have a generic search method like this:

Observable<List<Entity>> get(Specification specification);

The reason is that it is not an easy task to create this kind of methods. The possibly implementations can be in memory, SQL, Firebase or a network API or any other kind of solution.

The idea with the repository pattern is to hide the implementation details from the rest of the app. So imagen to implement a generic method where you can do generic searches towards any of the different kind of solutions. It is not an easy task. So I rather implement a specific method like the one below, that also give us a clearer api.

Observable<List<Entity>> getNameContaining(String str);

A valid and really common senario is that the app development starts simultaneously with the backend development, meaning no api available from start. Implementing an in memory repository with a couple of hard coded Entity items added will give you a lot of help continue working while waiting for the backend. This in-memory repository can later on be used during testing.

Business Logic Layer

The business logic is the use cases of the app. Here is where the more complex operation might occur.

Use Case

A use case can be as simple as fetching all objects of a type to merging several objects into a new one and save it. In the spirit of keeping it simple I don’t use any special patterns here many people promote the use of Interactor Pattern but I think it is a simpler to just do this with RxJava and you will get the same result.

An example of fetching all users could look like this:

public class GetAllUsersUseCase {

private static GetAllUsersUseCase sInstance;

private UserRepository mUserRepository;

private GetAllUsersUseCase(UserRepository repository) {
mRepository = repository;
}

public Observable<List<User>> execute() {
return mRepository.get()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}

public static GetAllUsersUseCase getInstance(UserRepository repository) {
if (sInstance == null) {
sInstance = new GetAllUsersUseCase(repository);
}

return sInstance;
}
}

Presentation Layer

UI Design Pattern

I think that the MVP-VM (model-view-presenter-view-model) is the most clean and proper presentation pattern. I will try to explain why by starting with the MVC pattern and move towards the MVP-VM pattern.

Model-View-Controller

The oldest most known pattern is probably the MVC. There are several take on the implementation but I chosen the one I think is the most common one.

The view listen to the changes in the model and update it self accordingly. All user inputs goes into the controller that acts and update the models.

Model-View-Controller

The first issue here is when working on Android the entry point is not the controller it is the Activity or the Fragment, some say that the Activity/Fragment is the controller but for me they are both the view. So to make the MVC work on Android we need to adjust the pattern a bit. We need to move the entry point to the view, this also means that we get an unwanted dependency from the view to the controller.

Model-View-Controller adjusted for Android

An other issue here is that since the view is listening to events directly from the model the controller isn’t really in control, something else could update the model, leading to an update in the view and the controller don’t now about it. So let us give back the control to the controller.

Model-View-Presenter

By removing the dependency from the view to the model we get a cleaner diagram as shown below.

Model-View-Presenter

We can now also draw it a bit nicer to more visually show the layers in this pattern.

Model-View-Presenter

There are still some issues with this pattern and the biggest one it that we have a bi-directional dependency between the view and the presenter. Let’s solve that issue.

Model-View-ViewModel

To solve this bi-directional dependency the MVVM pattern was created, by letting the view listen to events from the controller class the controller class don’t need to know anything about the view.

Model-View-ViewModel

You probably figured out by that red in my diagrams means somethings is wrong.

The MVVM patterns breaks the single responsibility principle by letting the ViewModel taking care of both the formatting of the data to be shown in the view and handling the input for updating the ViewModel.

Model-View-Presenter-ViewModel

By splitting up the ViewModel into two classes with their own responsibility we get the MVP-VM pattern. The ViewModel now only deliver formatted data for the view. The Presenter only react on user input to update the ViewModel with data from the model.

Model-View-Presenter-ViewModel

So visually it might look a bit more complex than the MVVM pattern but to quote dear old Albert.

Everything should be made as simple as possible, but not simpler. — Albert Einstein

My own interpretation of this would be that we should make the architecture as simple as possible, but not so simple that it messes up the implementation.

--

--