Part 3: Modular architecture

Taras Morskyi
AndroidPub
Published in
12 min readFeb 19, 2019

Previously on Medium

Part 1 of this series introduces Reactive architecture with MVI pattern to make your project able to use whole power of RxJava instead of wrapping network requests in it.

Part 2 is about introducing MVVM provided by Google in the way which allows us to keep all advantages of Reactive architecture with MVI introduced in previous part. With this solution ViewModel becomes an Observer instead of Activity/Fragment(View), but it keeps the same way of View sending events.

TL;DR

Modular architecture makes your project easily scalable and it builds much faster because Gradle doesn’t rebuild all modules which where not changed.

Split the jobs

Splitting project into modules is one more way to organize your code. Usually, code is splitted into packages to separate one architecture part from another and this solution is great for small projects, because code organization is always welcomed if you don’t want to have pain in your bottoms in the future. However, when project becomes bigger and bigger this organization style starts look like a mess with constantly growing amount of files and packages. Moreover, longer builds come up and every small changes force you to wait a lot of time to see results of your work.

Gradle has great features to deal with long builds and to organize code even better. This features allow you to split project into smaller modules in a few different ways. One of these ways is the most useful for everyday use, fits for every project and can be combined with other splitting methods (like dynamic features) is splitting project into libraries.

Usually libraries are considered as tools for automating everyday used features in projects (loading images, network requests etc.), but at the same time libraries can be used for separating some code from parent module. Besides creating libraries for automation, it is possible to create libraries per feature. In this way it will look like a split into additional layer of packages with extra build.gradle file for every package. In addition, child module doesn’t know anything about parent module and doesn’t have any communication with it by default.

Build faster. Please, I’m begging you

Before starting organizing modules in project, it’s important to understand how exactly Gradle builds project in both incremental (when small changes where done) and full build.

Scheme 1

Understanding how full build works will be much easier to start with. First of all, Gradle has to build every library added to build.gradle to make them ready for main part. Only when all libraries are built Gradle is ready to build main part of the project with your own code.

Scheme 2

Next, incremental build is not much different besides that libraries are already ready to make it possible for Gradle to move right to building main part of project. It means Gradle doesn’t rebuild libraries on incremental build.

Scheme 3

Finally, with multimodular project Gradle works in exactly same way as it does with regular one, because every regular project is multimodular under the hood as external libraries are separate modules. So, what is the difference in multimodular architecture can be?

The main difference in build process with Gradle for multimodular project to regular one is possibility to have multiple layers of modules. Actually, it will have multiple layers, that’s for sure. What exactly does it mean? It means Gradle gets possibility to build even smaller parts of project on incremental build if it’s possible.

The complete process for incremental builds, as it was mentioned before, is exactly the same as for project without multimodular architecture, it builds only modules where changes were made. However, it has to rebuild every parent module of every module with changes, because besides parent modules don’t have changes directly in themselves, they depend on their child modules where changes were made. Let’s see the example:

This example projects scheme contains the following modules:

  • Base module which is com.android.application and represents an application. It is a main module, from where all branches represented as modules, are growing
  • Login modulechild module of base. Let’s assume it implements LoginActivity with everything required for it except communication to remote repository (more about it later)
  • Registration module child module of login. At the same way as login it implements RegistrationActivity
  • Remote modulechild module of base. It represents communication with a remote server.

Finally, structures’ parts explanation is ready and we can move to build process. First of all, build process starts from checking in which module changes were made and at this situation it is login module. After discovering by Gradle that login module was changed, it starts rebuilding it. Next, it does not rebuild registration module, because it wasn’t changed and its cached version gets included to login module in build process. When login module is ready, Gradle starts rebuilding base module because it is a parent module of login and its dependency was changed. During build process of base module, Gradle includes in it remote module as its dependency from cache, because it wasn’t changed, as well as new, rebuilt version of login.

Let’s sum up what Gradle had to do to build whole project. Because remote and registration modules weren’t changed it didn’t rebuild them. It means Gradle had to rebuild only login and base modules which are only 50% of modules. In this small example nobody will feel the difference, but imagine yourself a big project with 20 modules where every module contains a lot of code. In situation where you make changes in only one module (or two, but not all of them) which is not a common dependency for most layers (like a layer with all your Model classes can be) Gradle will rebuild only a few modules instead rebuilding all of them. With a project this size build time can be decreased even twice or more.

Tell me what do I have to do

As any child module doesn’t know anything about its parent module it can’t interact with other parts of a project by default. It means parent module has to describe all events which its child module requires and provide to it.

What if child module is located further than the first line of dependencies from base module? Who, out of all parents has to implement all events for this module? It depends on requirements you have for this module. Every parent can provide required implementation. Mainly, requirement and possibility to reuse this module with different behaviors is the only importance here. If module will not be reused in the same project ever, then all required implementations can be provided even from base module which has a connection to the whole project. But, if you need (or consider a possibility) to reuse module then the best option is to implement all required events throughout all parent modules up to the modules which contain required connections for every method independently. For example, if registration module (scheme 3) requires description about network request to perform registration then you have two options:

Gif 1
  • Base module implements all events required by registration module, because it has a connection with remote module to describe network request
Gif 2
  • Login module implements all events required by registration module by adding “bridge” events to itself for redirecting them to base modules’ implementation. To complete events ,required by registration module, base module implements events required by login module and communicates with remote module.

In real life, the second approach has one minor problem: a lot of developers would not want to create all this connections because those are only redirectors. On the other side, first approach has a gap in communication logic, because base module communicates directly with registration module and login module doesn’t know anything about this connection. You decide, which approach is better for you.

Important

Gradle has two keywords for importing dependencies:

  • Implementationdependencies added with this keyword are visible only in that exact module where build.gradle is located
  • Api — the only difference from implementation is visibility. Dependencies added with api keyword are visible to parent modules.

With this keywords you can organize your dependencies to limit visibility for parent modules. Also with api keyword you can add external dependencies only in common module to minimize amount of libraries in parent build.gradle.

Story behind

To understand how to use some tool you have to read a lot of posts and documentations before actually starting using it. For this case I also read a lot of posts, watched some presentations on Youtube and even talked with Java Backend developers who already digged into Jigsaw and used microservices in their projects. After talking with backend developers I understood one interesting thing, which was never mentioned in posts and presentations created by Android developers, is independence of every module. Independence of every module means that modules shouldn’t have some godlike module, usually called ‘core’, which contains, for example, network requests or database layer.

All of articles I found about modularization on Android represent diamond shaped dependency scheme: all modules which got separated are children of base module and are depending on core module

Scheme 4

In this way you get three problems:

  • Every Feature module knows everything about network and database layer.
  • If you change something in network or database request for Feature 1 Gradle has to rebuild whole project because all your Features have changes in their dependency.
  • You are getting limited in reusability of your module and you cannot limit network or database requests in specific version of your app, for example in Instant App.

In most posts I found also one more problem: everybody creates very flat scheme with only one layer of Features. The only one layer of features forces your base module to make all decisions.

After long brainstorming about creation of modular architecture which will give me all bonuses I want, I finally got everything.

Scheme 5

As you could understand, we don’t need core module, which contains all actions which most Features have to do. As well as backend developers separate microservice(server part) from database I create separate modules: for network requests and for database. In this way Feature modules don’t communicate directly with database or server, but they tell parent module through interface what they want to get. With this kind of separation you can easily create limited version of your application and Feature modules don’t know anything from where data is coming. Also, it becomes possible to reuse your modules and create multiple layers of dependencies to provide different functionalities into inner modules. For example, you can have gallery module, as well as I do in sample github project, with Fragment in it which is used in multiple modules. I use gallery module in previewGallery and main modules and they decide what gallery gets when it makes some action. In my sample user can click on post from gallery and main module shows dialog to like a post, but previewGallery doesn’t show any dialog, because user is not logged in and doesn’t have a possibility to like a post.

However, if you’ll look at my project you can find a lack of perfection in its scheme. It contains module called dataModel which contains all models required in the whole project. Also, most modules depend on thismodule what creates exactly what I didn’t like in all other posts about modularity.

Why did I do it? Well, as you see, this module contains only models and doesn’t have any logic in it. Does it fit as an excuse for my imperfection? No, it doesn’t. I know the solution which will make all modules independent, but I’m not sure if it’s redundancy or required sacrifice to make it closer to perfection. I would like to get some feedback from other developers, the only option I had so far is that it’s too much, so I have to make up and live with occasional long builds.

How can you solve it? You can move data models into modules which require them, but also you will have to repeat this model in every module from first to last in whole chain of action. Let’s get back to my sample. In the situation when gallery module which is in main module wants to load posts you will have to create model Post in gallery, main and remote. In addition to this amount of models you have to create converters from gallery.Post to main.Post etc. Right, you can get rid of main.Post if you use api keyword in Gradle to add your module, but it doesn’t help a lot.

One more thing, if you looked already in my project then you could find ui-core module. Almost as well as dataModel it is added in most modules, but the only difference is the purpose of this module. I don’t plan to change it because it contains only Base classes for my architecture and these classes are ready to be used.

How to do it

First of all, to start working with module it has to be created. To do it in Android Studio open File -> New -> New module… where you can choose different kinds of modules. Android Library is used for the most basic purposes. If dynamic feature is required you can make it dynamic and even combine it with libraries. There is a lot of topics about dynamic features on the internet.

When new module is created it is ready to be added to its parent module.

implementation project(':ui-login')

After telling parent module about its new child, child is ready to be implemented. Implementation looks almost the same as usual with patterns of your choice. For the purpose of this example patterns from part 2 will be used.

To create fully functional ui-login module with only one activity which collects all required data to login and after receiving response about login success it asks parent module to open Splash (but remember that parent module decides what exactly has to be done):

  • LoginActivity — activity with login form which collects required data from ui and sends it to ViewModel
  • LoginActivityModule — class needed for dependency injection everything with Dagger2
  • LoginUiModel/LoginViewEvent/LoginViewModelEvent — events explained in part 2
  • LoginViewModel — ViewModel of activity
  • LoginInteractor/LoginInteractorImpl — interface and implementation of interactor which is responsible for redirection to RepoEvents and processing data received from it.
  • LoginRepoEvents — interface injected into Interactor which describes events directed to load/save data from/into Repository required by module.
  • LoginUiEvents — interfece injected into Activity/Fragment which describes Ui events, such as opening Dialog or starting another Activity

Most of this classes where described in previous parts (part 1 and part 2). For the purpose of this article it’s required to describe only 2 classes:

  • LoginRepoEvents contains only 1 declared method in this example — login(). This component receives collected data in ui and returns Completable. Return type as Completable (or Observable/Maybe/Single) means it continues reactive chain from Interactor.
  • LoginUiEvents contains 2 declared methods which don’t return anything, because such kind of events have to be just done and current Activity/Fragment doesn’t have to know what actually happened. Both of this events receive Activity/Fragment instance as they require its Context to do the job which is not available in implementation.

Next, when all events required by module are described, parent module can implement it. As a parent module is base module it has a connection to every required module for these events.

Due to availability to access local module from base module an implementation of LoginRepoEvents has injected LocalRepository interface which declares functionality of local module. An interface and implementation of LocalRepository are located in local module and are available only to base module. Because local module is connected only to base module, all events have to go through it and are easy to find by every team member.

An implementation of LoginUiEvents is very simple, it just starts every required Activity because it has direct contact to all of them.

Conclusion

Modularized architecture is very useful to use in a team where every member can be responsible for his own module and does not make changes in other modules. With this approach you are not limited to any pattern. For example you can use coroutines, as well as RxJava, it is not important. However, I can recommend you to use Dependency Inject to inject connections into modules.

--

--