Part 3: Modular architecture
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.
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.
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.
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 amain module
, from where all branches represented asmodules
, are growing - Login module —
child 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 module —
child 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:
- Base module implements all
events
required by registration module, because it has a connection with remote module to describe network request
- Login module implements all
events
required by registration module by adding “bridge”events
to itself for redirecting them to base modules’ implementation. To completeevents
,required by registration module, base module implementsevents
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:
- Implementation —
dependencies
added with this keyword are visible only in that exactmodule
wherebuild.gradle
is located - Api — the only difference from implementation is visibility.
Dependencies
added with api keyword are visible toparent 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
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 theirdependency
. - 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.
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 bymodule
. - 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
.