Reusing Gradle modules in several applications
Hey guys! I’ve been working as an Android developer for quite some time now and I have to say: today’s apps have clearer architecture and are modular, as compared to the apps of ten years ago. Multi-modularity (or, at least, a couple of Gradle modules in a project) is de facto a modern standard. Any project, as a rule, has the main app module, several feature modules, as well as core/domain/data modules.
These modules are connected in a quite standard way:
When you have one app, it’s not a problem: all the modules co-exist and we don’t really care about the connections between modules, though, of course, we should avoid circular dependencies.
As a matter of fact, we can surely have several apps in one project. To do this, we just need to add another :app
Gradle module, declare the android.application
plugin in it, and — ta da! Android Studio shows another launch option.
For example, we are working on two apps: CatsApp, for cat-lovers, and another one, DogsApp, for dog persons. All the features are the same (we have a screen to display a list of cats and dogs and a screen for detailed information); only the main modules differ. Among other things, different network configurations and themes.xml
files may be declared and DI trees initialise differently in these modules. You can find an example of such a project here.
A more common example is probably the case when a project has its own design system, and we need a demo app to view our UI components, aside from the main app.
Option 2
This is the case when we have two independently developed apps within the same company, and the business requirements dictate that different features were united or all the features of one app were reused in the other.
Here, multi-modularity will save the day because we already have an isolated piece of code that is partially ready for reuse. At the same time, these modules exist in different repos, are not associated with each other, and have different dependency lists of their own.
How do we unite modules from different projects? First of all, we can just set up visibility of a project for the other one, by simply uploading them into neighbouring directories and updating settings.gradle
of the main project by including the other app’s modules into it.
project(':feature-list').projectDir = new File('../app_to_be_reused/feature-list')
include ':feature-details'
project(':feature-details').projectDir = new File('../app_to_be_reused/feature-details')
include ':domain'
project(':domain').projectDir = new File('../app_to_be_reused/domain')
Unfortunately, if we only need :feature-list
, we will also have to add dependencies of that module into our project, i.e. to pull :domain
and :feature-details
.
If we haven’t prepared the modules of the reused project beforehand, it will cause lots of issues even when we just try to import everything we need and synchronise the main project. For instance, if our reusable :feature-1
module has Activities they won’t be able to use themes defined in the main project :cats-app
.
Also, both projects are different Git repositories, so we’ll have to take care about being on the correct branches, while synchronising modules. It is highly possible that the Gradle configuration has changed in the reused project, which is why we need to always check whether we work in the right version of the project. It, in turn, causes problems with CI builds that now have to fetch two projects instead of one and build and test two projects in the same pipeline.
Git submodules can somewhat help us. They allow us to include another repo into the existing one and tag the correct version of the project with dependencies. You can learn more about submodules here. In this case, the dependency diagram will be similar to the one from the previous example.
Anyways, you as the developer of another app will have to dive deep into another team’s code, with the learning curve quite possibly being rather steep. Please see an example of using two projects where one links the other’s modules here.
Option 3
This option is based on publication of a part of the reused app in a separate Android library so that the other app could link it and use it as any other third-party SDK. If the app with dependencies is rather modular, you can try to export the modules you need, and then add entry points for them from the other app. As a rule, we use the maven-publish Gradle plugin for publishing pure Java/Kotlin libraries, but Android Gradle Plugin has recently started supporting it, and we can now export Android modules, and not merely Java libraries.
Here’s an example. Our DogsApp consists of the modules app
, feature-list
, feature-details
and domain
. Feature-list
is responsible for printing out information about and the image of a certain dog. domain
includes general information reused in all the other modules.
At some point, our company, for example, acquires another start-up business that has developed CatsApp. The business requirements now dictate that we support the new app and update its features by adding the same screen for viewing the details of cats, like in DogsApp. Since the apps were developed separately, they have different dependency trees, and we cannot just use the first two approaches, i.e. merge the codebases or link the modules we need from DogsApp into CatsApp. But we can take advantage of library publication. In this case, we need to publish feature-details
.
To do this, we’ll use the maven-publish plugin and add the following code into feature-details
build.gradle
:
android {
...
publishing {
singleVariant('release')
}
}
afterEvaluate {
publishing {
publications {
release(MavenPublication) {
from components.release
}
}
}
}
singleVariant
points that we want to publish only one build option. To make it possible to use both the debugging and release versions of our library, we need to use allVariants()
and components.default
:
...
publishing {
multipleVariants {
allVariants()
}
}
}
afterEvaluate {
publishing {
publications {
allVariants(MavenPublication) {
from components.default
}
}
}
}
Now we can publish our module with the gradle :feature-details:publishToMavenLocal
command. Let’s add a local maven repo to CatsApp so that we could use this dependency:
settings.gradle:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
mavenLocal()
}
}
Now, we can add the dependency to the exported library in cats_app/build.gradle
:
dependencies {
...
implementation 'com.example:feature-details:1.0.0'
}
However, when we try to build CatsApp, build errors will occur because the build system cannot find the :domain
module, so we need to publish it too. In fact, we have to recursively publish all the dependencies that the :feature-details
module uses. For this, delete the code that we’ve added before for the publication of :feature-details
and add the following into the root build.gradle:
subprojects {
apply plugin: "maven-publish"
afterEvaluate {
if (!plugins.hasPlugin("android")) {
if (plugins.hasPlugin("android-library")) {
android {
publishing {
multipleVariants {
allVariants()
}
}
}
}
publishing {
publications {
allVariants(MavenPublication) {
afterEvaluate {
if (plugins.hasPlugin("java")) {
from components.java
} else if (plugins.hasPlugin("android-library")) {
from components.default
}
}
}
}
}
}
}
Subprojects {
means that we want to apply our code to all the submodules. But we don’t want to publish the main module :dogs-app
, so we filter it out by the presence of Android plugin !plugins.hasPlugin("android")
. Please also note that :domain
is a Java library and doesn’t have build options, which is why we don’t need to add this information to it. We will add build options for Android libraries only:
if (plugins.hasPlugin("android-library")) {
android {
Also, since we have both Java and Android modules, we’ll have to use different publication methods. If a Java plugin has been applied to a module, it will be published as a Java library, and in case of the Android library plugin, we’ll publish a set of .aar archives with corresponding build options.
if (plugins.hasPlugin("java")) {
from components.java
} else if (plugins.hasPlugin("android-library")) {
from components.default
}
Now, when we publish our library with the same gradle :feature-details:publishToMavenLocal
command, all the dependencies will be placed into our local maven repository, and the client app CatsApp will be built with no issues. You can find the full code here.
What about you? Have you ever had to merge different apps or reused another team’s Gradle dependencies? Please share the approach used in your company in the comments.
Top comments (3)
Wow, I don't often see a full description of how to publish libraries like this, it's extremely useful! (and almost impossible to work out yourself)👏👏👏
Thank you!
I like this❤️❤️👏👏