Configuring multi-module projects

Vladimir Genovich
ЮMoney
Published in
5 min readAug 26, 2020

Background

Sometimes, when I procrastinate, I like to clean the table, re-arrange my stuff, tidy up the room. Basically, what I’m doing is putting the environment in order: it brings in new energy and sets me up for work. When it comes to programming, it’s pretty much the same, except I clean the project: carry out refactorings, create various tools, and do my best to make life easier for my colleagues and myself.

Some time ago, we in the Android team decided to make one of our projects, the Wallet, multi-modular. This led to both a number of advantages and problems, one of which is the need to configure each module from scratch. Of course, you can just copy the configuration from module to module, but if we wanted to change something, we would have to go over all the modules.

I don’t like that, the team doesn’t like that, and here are the iterations that we’ve taken to simplify our lives and make configurations easier to maintain.

Iteration I — moving library versions

Actually, this was already in the project before me, and you may already know this approach. I often see developers using it.

The approach is to move the versions of the libraries into separate global properties of the project, so they become available throughout the project, which helps to reuse them. This is usually done in the build.gradle file at the project level, but sometimes these variables are taken out into a separate .gradle file and included in the main build.gradle.

You have most likely already seen such code in the project. There is no magic in it; it’s just one of the Gradle extensions called ExtraPropertiesExtension. In short, it’s just Map<String, Object>, available by the name ext in the project object, and everything else (working as if with an object, configuration blocks, and so on) is just Gradle magic. Example from .gradle and .gradle.kts.

What I like about this approach is that it’s extremely simple and helps keep the versions consistent. Still, it has its disadvantages: you need to make sure developers use versions from this set, and it doesn’t really simplify the creation of new modules since you still have to copy a lot of things.

By the way, a similar effect can be achieved using gradle.properties instead of ExtraPropertiesExtension, but be careful: your versions can be overridden when building with -P flags, and if you refer to a variable simply by name in groovy-scripts, then gradle.properties will override them as well. See an example with gradle.properties and overrides here.

Iteration II — project.subprojects

My curiosity coupled with my unwillingness to copy the code and deal with the configuration of each module, led me to the next step: I remembered that in the root build.gradle there is a block generated by default, allprojects.

I checked the documentation and found out you can pass a block of code into it that will configure this project as well as all nested projects. This is not quite what I needed, so I scrolled further and found subprojects, a method for configuring all nested projects at once. I had to add a few checks, and these are the results.

Now, for any module with a connected com.android.application or com.android.library plugin, we can configure anything: applied plugins, plugin configurations, dependencies.

All this would be fine if not for a couple of problems: if we want to override some parameters specified in such a block, it won’t be possible because the block configuration occurs before the general configuration is applied (thanks to afterEvaluate). Also, if we want to not apply this automatic configuration in individual modules, then there will be many additional checks in the subprojects block. So I started thinking further.

Iteration III — buildSrc and plugin

Up to this point, I had heard about buildSrc several times and saw examples in which buildSrc was used as an alternative to the first step in this article. I also heard about gradle plugins, so I started digging in this direction. Everything turned out to be much simpler: Gradle has documentation for developing custom plugins that details, well, everything.

After reading the thing, I made a plugin that can configure everything that needs to be configured with the ability to change if necessary.

Now the configuration of the new project looks like apply plugin: ⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠⁠’ru.yandex.money.module’, and that’s it. You can make your own additions to the android or dependencies block, you can add plugins or customize them, but the main thing is that the new module is configured in one line, its configuration is always relevant, and the product developer no longer needs to think about setting it up.

Regarding the disadvantages, I would note that this solution requires additional time and study of the material but, from my point of view, it’s well worth it. If you want to move the plugin out as a separate project in the future, then I would not recommend setting up dependencies between modules in the plugin.

An important point: if you’re using the android gradle plugin below 4.0, some things are very difficult to do in kotlin scripts: at least the android block is easier to configure in groovy scripts. There is a problem with the fact that some types are not available for compilation, but groovy is dynamically typed, so that doesn’t matter to him :)

Next — standalone plugin or monorepo

Of course, the third step is not the end. There is no limit to perfection, so we have some options for where to go next.

The first option is the standalone plugin for gradle. After the third step, that’s not going to be that difficult: you need to create a separate project, transfer the code there, and set up the publication.

Pros: the plugin can be shared between several projects, which will simplify your life not just in one project, but in the entire ecosystem.

Cons: versioning, i.e when updating a plugin, you will have to update and check its functionality in several projects at once, and this can take some time. By the way, my colleagues from the back-end development have a great solution for that, and the key word is modernizer, but they can tell you all about that better than me.

Monorepo sounds big, but I have no experience with it, only a few thoughts: one project, like buildSrc, can be used in several other projects at once, and this could help solve the issue with versioning. If by any chance you have experience with monorepo, please do share it in the comments so that we can learn something about it.

To summarize

In a new project, do the third step right away — buildSrc and plugin — it will be easier for everyone, plus I have already provided the code. Use the second step, project.subprojects, for connecting common modules to each other.

If you have anything to add or object to, leave a comment or find me on social networks.

--

--