Table of Contents
This put up describes how you can share code concerning your facet jobs devoid of code duplication, distant web hosting or versioning. Produce your own Android monorepo repository using the Gradle build system and some wizardry!
What is a monorepo?
In edition manage methods, a monorepo (“mono” which means ‘single’ and “repo” staying brief for ‘repository’) is a software package improvement technique where code for a lot of assignments is stored in the similar repository.
https://en.wikipedia.org/wiki/Monorepo
As developers, we can have several (unfinished) aspect tasks and a whole lot of the time we are executing the very same points over and in excess of yet again. Most applications require logging, most apps hit network endpoints, most apps load images and so forth etc.
Our aspect assignments usually contain copy and pasting all around code, then in the most current version modifying that code to boost it much more (or producing a library and internet hosting it). From time to time I’ll then go back to an outdated venture and have to duplicate paste again my up-to-date features. Other instances I’ll overlook the wonderful function I extra in the very last undertaking, (such as exponential again off for community requests) and the new facet project will not be as “fancy” in that spot.
A monorepo can support. When starting up a new aspect undertaking, if you make every little thing a module, you include the modules you want and you are then up and working and concentrating on what is new about this new facet undertaking, in its place of scaffolding and reinventing your own wheel just about every time.
Without the need of even more ado, let us get begun.
I believe it’s most effective to commence by wanting at the conclusion, from what we want to attain, set that expectation and then work in the direction of it. All code is accessible in the Git Repo listed here. This is the illustration format from the GitHub Repo. It’s what you are going to have when you use a monorepo:
/monorepo /mono-libraries << Shared app(s) code /logging /http /mono-build-logic << Shared build code /android-plugins /android-k-plugins /tut-app-1-plugins /tut-app-2-plugins /tut-app-1 << Specific app code /app /libraries /tut-app-2 << Specific app code /app /libraries
Now you understand how a real world example will look. We can take a look what the project structure will be with some explanatory naming. Hopefully seeing both will let you understand and map between to the two when doing it for yourself. This is what our monorepo folder structure will look like, as well as the location of the Gradle files involved, when it’s complete:
/monorepo /mono-libraries << Shared app(s) code /shared-library-1 /shared-library-2 /shared-library-N build.gradle.kts /mono-build-logic << Shared build code settings.gradle.kts /shared-plugins build.gradle.kts /src/main/kotlin/plugin-shared-android.gradle.kts /side-project-1-plugins /side-project-2-plugins /side-project-N-plugins build.gradle.kts /src/main/kotlin/plugin-side-project-N-app.gradle.kts /side-project-1 << Specific app code /side-project-2 << Specific app code /side-project-N << Specific app code build.gradle.kts settings.gradle.kts /libraries /app build.gradle.kts
For comparison this is what a ‘typical’ side project single app project would look like:
/side-project
/app
build.gradle.kts
/module1
/module2
/moduleN
build.gradle.kts
build.gradle.kts
settings.gradle.kts
We can see that modules
in a solo side-project becomes shared-libraries
in a monorepo. The Gradle build logic usually in app
& moduleN
folders moves to its own ‘project’. In Gradle speak this is known as a Composite Build, and is the crux of how your monorepo will work.
A monorepo starts with a root folder, the root folder holds 3 things:
1) Side Projects
– each of the apps (side projects) you have worked/working on (1 folder each)
2) Gradle builds logic
– the build logic for our monorepo & apps (1 folder with subfolders for each app)
– shared build logic modules between apps
3) Shared Libraries
-the libraries you want to share between projects (1 folder for each library)
A composite build is simply a build that includes other builds. In many ways a composite build is similar to a Gradle multi-project build, except that instead of including single
projects
, completebuilds
are included.https://docs.gradle.org/current/userguide/composite_builds.html
The rest of this blog post is quite a long explanation (we’re going to be creating/modifying 12 Gradle build files in total!) but stick with it and at the end you will have a monorepo that you can use again and again for every side project. Alternatively, if you just want to get going fork the GitHub repo, scroll to the bottom, read the conclusion and you can be up and running straight away, skipping the step by step understanding. 🙂
Prerequisite: Folder Structure
Create a folder called monorepo
, Inside this folder (i.e. in the root monorepo
folder) create another folder called mono-libraries
and another called mono-build-logic
. Now we can start to fill out these folders with Gradle configuration files.
/monorepo /mono-libraries /mono-build-logic
If you want to add this monorepo to version control make sure you call git init
from the monorepo
folder and not from any of the subfolders.
1) Side Project creation
Use the Android Studio wizard to start a new project (or do it however you usually create a new project). Move this project inside of your folder called monorepo
and this is where we will start from. From now on we’ll call this tut-app-
1.
/monorepo /mono-libraries /mono-build-logic /tut-app-1 /app /libs gradlew build.gradle ...etc
IMPORTANT NOTE
With a monorepo, the idea is to have one instance of the IDE (Android Studio) open per side-project (per app). The IDE works with “gradle roots” and we’re going to make each side project app a Gradle root. i.e. if your monorepo has two projects and you want to work on both, then you should have two instances of your IDE running.
(You technically can work on both in the same IDE instance, but only one will be on the classpath and therefore only one will be compiling/have IDE assistance. I would see this as a good thing, as you are not compiling code you are not working on saving yourself compile time.)
/tut-app-1/settings.gradle.kts
Edit /monorepo/tut-app-1/settings.gradle.kts and inside the pluginManagement
block add an includeBuild
method call, this is called a Composite Build by the Gradle build system and will allow us to separate our Android build logic from the Android apps we are making. The includeBuild
will reference our mono-build-logic
folder. Like so:
pluginManagement
repositories
gradlePluginPortal()
google()
mavenCentral()
includeBuild("../mono-build-logic")
Next we’ll have our project include our shared-library-1
(think http
or logging
from the Git repo). This is what you’d do normally with the include
method, and you can see the app
module is included like that. For a monorepo the libraries have to be included slightly differently, but if you create a helper method (monoInclude
) it can easily be achieved. Like so:
include("app")
monoInclude("shared-library-1")
fun monoInclude(name: String)
include(":$name")
project(":$name").projectDir = File("../mono-libraries/$name")
We’ve declared that we want to build monorepo/mono-libraries/shared-library-1
but we haven’t created that module yet. That happens later on (below).
That’s the /monorepo/app/settings.gradle file done. Next is to declare the tut-app-1
‘s project build file.
/tut-app-1/build.gradle.kts
The project build file does not have any changes out of the ordinary for a monorepo setup, except for one. Here we will declare a project extra property for the compose_version
. This will allow us to reference the version number for compose in all dependencies in our app later on. If you where to take your monorepo to the next level, you’d likely move this versioning into the mono-build-logic
.
buildscript
extra.apply
set("compose_version", "1.0.5")
repositories
google()
mavenCentral()
dependencies
classpath("com.android.tools.build:gradle:7.0.4")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31")
That’s the /tut-app-1/build.gradle.kts file done. Next is to declare our app
module build file, whilst delegating most the work to our mono-build-logic
.
/tut-app-1/app/build.gradle.kts
This file is usually where your app’s build logic is, however we are going to be delegating that to the mono-build-logic
project with the idea of reducing duplication and repetition, didn’t I just say that?
Your file ends up looking like this:
plugins
id("tut-app-1")
dependencies
val implementation by configurations
val debugImplementation by configurations
implementation(project(":logging"))
val composeVersion = rootProject.extra["compose_version"]
implementation("androidx.core:core-ktx:1.7.0")
implementation("androidx.appcompat:appcompat:1.4.1")
implementation("com.google.android.material:material:1.5.0")
implementation("androidx.compose.ui:ui:$composeVersion")
// + other dependencies see git repo
debugImplementation("androidx.compose.ui:ui-tooling:$composeVersion")
Notice that we don’t declare a dependency on the id("com.android.application")
plugin like a usual stand alone application would. We’re going to use our own plugin tut-app-1
this will help reduce duplication and we will declare the dependency on the android plugin in there (no magic, just movement!).
To have the tut-app-1
module depend on the shared-library-1
(i.e. logging
module in the Git repo), we use the implementation(project(
nested method call. Like so:
dependencies
...
implementation(project(":shared-library-1"))
...
Notice that the dependencies
block uses:
val implementation by configurations
This is because we have moved the Gradle build logic from the app to the composite build plugins (to avoid duplication in the monorepo, keep reading it is shown next) and as such, the Gradle Kotlin DSL File can’t know at script compilation time that these configurations will be available when the script is applied. Hence the Kotlin extensions (implementation()
) for these configuration not being available at script compilation time. You have to reference the configurations by name at runtime.
That’s the /tut-app-1/app/build.gradle.kts file done and that is also the tut-app-1
setup complete as well. Next is to recreate the build logic we have omitted from this file, by creating plugins in our mono-build-logic
project.
/monorepo /mono-libraries /mono-build-logic < UP NEXT /tut-app-1 < COMPLETE build.gradle.kts settings.gradle.kts /app build.gradle.kts /src
2) Sharing Gradle Build Logic
Our build logic project is where we can declare how our build runs. (the good news is this section is a one off, so its a lot but we won’t need to do it again!) We want to declare that our project uses Android, that it’s an app and has libraries available, and we declare the usual things like minSDK version and application version etc.
Think of this build logic folder as a project in itself (because it is), it gives us the capability to make individual Gradle plugins and have each of our app project modules use these plugins. As such we will have a plugin for Android Libraries, a plugin for Android Apps and then app specific plugins that can then customise this shared build logic for the individual app. Makes sense? Doesn’t make sense? Read on to find out more.
First thing for this build logic project is to create the file structure. We already have a mono-build-logic
folder. Inside that folder make three sub-folders, naming them android-plugins
, android-plugins-k
, tut-app-1-plugins
. These are going to be the modules in this project.
/monorepo /mono-libraries /mono-build-logic /android-plugins /android-plugins-k /tut-app-1-plugins /tut-app-1
/mono-build-logic/android-plugins/build.gradle.kts
First up, create a new file in the android-plugins
folder called build.gradle.kts
. This is the file for declaring how this module (android-plugins
) is built. For this module we are going to use the Groovy language for the plugin, the rest of the monorepo is all in the Kotlin language. We use Groovy here as it has a much more powerful reflection syntax, so we can configure the Android build system without having to depend on the Android plugins ourself. id("groovy-gradle-plugin")
enables us to write Groovy Gradle plugins. Your file should end up looking like this:
plugins
id("groovy-gradle-plugin") // This enables src/main/groovy
dependencies
implementation("com.android.tools.build:gradle:7.0.4")
That’s the /mono-build-logic/android-plugins/build.gradle.kts file done. Now we have a project to create groovy plugins in. Next is to declare a Groovy plugin in this module that will setup the defaults of all our Android builds.
/mono-build-logic/android-plugins/src/main/groovy/android-module.gradle
Create a android-module.gradle
in the android-plugins/src/main/groovy
folder (you need to create that path). This is going to be a script plugin that forms the basis of every Android module in any side project (app) that’ll be included in the monorepo.
When you have script plugins like this, they use their filename as the name when referencing them by id
. ie. using this plugin would look like id("android-module")
because the filename is android-module.build.kts
.
As you can see below, we’re using afterEvalute
to check if the project (module) this plugin is being attached to is an Android project. If it is we configure the android closure with all the defaults we don’t want to keep duplicating in every app.
We are using a Groovy plugin here as it has a much more powerful reflection syntax, so we can configure the Android build system without having to depend on the Android app or library plugins directly (because we want it to work for both).
afterEvaluate { project ->
if (challenge.hasProperty("android")) {
android
compileSdk 31
defaultConfig
minSdk 28
targetSdk 30
vectorDrawables
useSupportLibrary legitimate
testInstrumentationRunner "androidx.take a look at.runner.AndroidJUnitRunner"
testOptions
unitTests.returnDefaultValues = legitimate
composeOptions
kotlinCompilerExtensionVersion compose_model
packagingOptions
sources
excludes += "/META-INF/AL2.,LGPL2.1"
compileOptions
sourceCompatibility JavaVersion.Edition_11
targetCompatibility JavaVersion.Version_11
kotlinOptions
jvmTarget = "11"
useIR = correct
buildFeatures
compose genuine
}
}
It’s fully up to you what defaults you have listed here, I would say start out with this and migrate / overwrite attributes as you obtain a distinct differentiation in a specific application (this kind of as minSDK).
That’s the /mono-develop-logic/android-plugins/src/main/groovy/android-module.gradle file done. We now have a reusable plugin out there that declares some rudimentary shared android attributes. Future we will develop yet another shared plugin job, so that we can declare specific plugins that can be applied especially for an android library or android software module.
/mono-make-logic/android-plugins-k/build.gradle.kts
We’ve built the android-plugins
make logic venture, now we require to develop a different challenge android-plugins-k
. Produce a file termed establish.gradle.kts
within of the /mono-develop-logic/android-plugins-k/
folder. The only variance will be this module properties Kotlin script plugins and the previous module housed a Groovy script plugin.
This venture (module) is dependent on the kotlin-dsl
plugin and this will let us to generate Kotlin script plugins. See we have also added an api
dependency on our Groovy android-plugins
module. This allows the present module to obtain the plugins of the latter module and to make them offered to any dependent jobs.
plugins
`kotlin-dsl` // This enables src/main/kotlin
dependencies
api(venture(":android-plugins"))
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31")
implementation("com.android.applications.construct:gradle:7..4")
That is the /mono-establish-logic/android-plugins-k/create.gradle.kts file done. Now we have a Kotlin job to make plugins in. Following is to declare two Kotlin plugins in this module that will set up the defaults for our Android library & Android app builds.
/mono-build-logic/android-plugins-k/src/principal/kotlin/app-android-module.gradle.kts
Create the /src/main/kotlin
folder framework and within generate a file known as application-android-module.gradle.kts
. This will be our plugin that we use for every Android app. We will declare particulars to do with application modules below, but we will not have details to do with any just one certain application nevertheless, that will come subsequent.
This plugin depends on the android-module
plugin we have previously designed. It also declares dependencies on com.android.application
(since this plugin is predicted to be applied for android apps), and kotlin-android
(since we are working with Kotlin in our applications).
The plugin then declares two construct types for any android application that will count on it. The plugin guarantees the release develop sort has minification turned on and the debug make has “debug” additional to its software id (i.e. put in offer).
plugins
id("android-module")
id("com.android.application")
id("kotlin-android")
android
buildTypes
getByName("release")
isMinifyEnabled = correct
proguardFiles(getDefaultProguardFile("proguard-android-improve.txt"), "proguard-policies.pro")
getByName("debug")
isDebuggable = true
applicationIdSuffix = ".debug"
Which is the /mono-establish-logic/android-plugins-k/src/primary/kotlin/app-android-module.gradle file performed. We now have a reusable android application plugin offered that declares application module unique shared android properties. Following we will build another shared plugin, this time for shared android library modules.
/mono-make-logic/android-plugins-k/src/major/kotlin/library-android-module.gradle.kts
Alongside the app plugin, within src/most important/kotlin
, produce a file identified as library-android-module.gradle.kts
. This will be our plugin that we use for each individual Android library module. We will declare details to do with library modules below, but we will not have specifics to do any just one unique library – that would be in the shared library alone.
This plugin depends on the android-module
plugin we have now made. It also declares dependencies on com.android.library
(since this plugin is envisioned to be made use of for android library modules), and kotlin-android
(simply because we are applying Kotlin in our apps).
We really don't have any other added configuration in this plugin for the library modules.
plugins
id("android-module")
id("com.android.library")
id("kotlin-android")
Which is the /mono-build-logic/android-plugins-k/src/principal/kotlin/library-android-module.gradle file done. We now have a reusable android library plugin obtainable that declares library module particular shared android properties. Next we will make a precise plugin for our tut-app-1
facet project to depend on.
/mono-create-logic /android-plugins << COMPLETE android-module.gradle /android-plugins-k << COMPLETE app-android-module.gradle.kts library-android-module.gradle.kts /tut-app-1-plugins << UP NEXT
/mono-build-logic/tut-app-1-plugins/build.gradle.kts
Create a new file in the tut-app-1
folder called build.gradle.kts
. This is the file for declaring how this module (tut-app-1) is built. We’re going to use this module to create an app specific build script, allowing us to separate the build logic into its own separate testable project and simplifying the side-project-1
‘s Gradle file. Notice we have also added an implementation
dependency on our Kotlin
android-plugins-k
module. This allows the current module to access the plugins of the latter module (and its api
declared groovy module).
plugins
`kotlin-dsl` // This enables src/main/kotlin
dependencies
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31")
implementation("com.android.tools.build:gradle:7.0.4")
implementation(project(":android-k-plugins"))
That’s the /mono-build-logic/tut-app-1-plugins/build.gradle.kts file done. Now we have a build logic project to create an app specific plugin in. Next is to create the plugin itself for tut-app-1
.
/mono-build-logic/tut-app-1-plugins/src/main/kotlin/tut-app-1.gradle.kts
Create a new file tut-app-1.gradle.kts
inside of src/main/kotlin/
, here is where we will depend on the app-android-module
(because this plugin is for Android app modules).
We set up the details that are specific to this side-project (app). Therefore we declare what the installation applicationId is and the app version.
The rest of the Android build logic is handled up stream in the plugins: app-android-module
and android-module
(which we configured previously). If you are wondering, the only thing left to declare (coming up soon) is the dependencies
block. This will be handled in the side project (app) itself.
plugins
id("app-android-module")
android
defaultConfig
applicationId = "com.blundell.tut1"
versionCode = 1
versionName = "1.0.0"
That’s the /mono-build-logic/tut-app-1-plugins/src/main/kotlin/tut-app-1.gradle.kts file done. Now we have an app specific plugin that allows us to abstract the complexity of building the project from building the app. Next is to update the settings.gradle.kts
file to enable this project (mono-build-logic
) to build each of the modules (projects) we have just created.
/mono-build-logic/settings.gradle.kts
Now let’s enable all the above modules we have create to be recognised and compiled by Gradle in this project. Create a settings.gradle.kts
in the /mono-build-logic
root folder. Once created, sync your IDE and it will recognise all the modules declared.
dependencyResolutionManagement
repositories
gradlePluginPortal()
mavenCentral()
google()
include("android-plugins")
include("android-k-plugins")
include("tut-app-1-plugins")
That’s the /mono-build-logic/settings.gradle.kts file done and that is also the mono-build-logic
setup complete as well. Next is the final part, creating shared libraries that will be available to your side projects (apps).
/monorepo /mono-shared-libraries << UP NEXT /mono-build-logic << COMPLETE settings.gradle.kts /android-plugins build.gradle.kts /src/main/groovy/android-module.gradle /android-plugins-k build.gradle.kts /src/main/kotlin/app-android-module.gradle.kts /src/main/kotlin/library-android-module.gradle.kts /side-project-1-plugins /side-project-2-plugins /side-project-N-plugins build.gradle.kts /src/main/kotlin/tut-app-1.gradle.kts /tut-app-1 << COMPLETE
3) Sharing libraries
Use the Android Studio wizard to create an Android Library project (or move one that you already have), ensure it is created in the /mono-repo/mono-libraries
folder. From now on we’ll call this shared-library-1
(in the GitHub repo example we have a http
and a logging
module they work exactly the same way).
/monorepo /mono-shared-libraries << COMPLETE /shared-library-1
/mono-libraries/shared-library-1/build.gradle.kts
I’m sure you get the pattern by now. First up we have to enable this module to use the Gradle build system. Create a build.gradle.kts
file inside of shared-library-1
.
Here we’re wanting to have our module build an Android Library therefore we depend on our newly created library-android-module
plugin. This allows the targetSDK, testOptions, compileOptions, packingExcludes etc etc to be inherited from our shared-build-logic
plugins.
The only thing left to do for every library module now is to declare what dependencies you want to use. As below:
plugins
id("library-android-module")
dependencies
val implementation by configurations
val testImplementation by configurations
implementation("androidx.core:core-ktx:1.7.0")
// + other dependencies see git repo
implementation("com.squareup.okhttp3:okhttp:4.9.1")
testImplementation("junit:junit:latest.release")
That’s it! That’s the monorepo/mono-libraries/logging/build.gradle.kts
file written, and I hope you can see that that is also the template for any libraries you want to create or add in the future. Fore each shared library, it is a one line addition to the build file to depend on our library-android-module
plugin, declare your dependencies and you are up and running.
/monorepo /mono-shared-libraries << COMPLETE /shared-library-1 build.gradle.kts /src/main/ /mono-build-logic << COMPLETE /tut-app-1 << COMPLETE
Recap
You now have a side project tut-app-1
with a concise build.gradle.kts
file that only declares its dependencies
and then delegates via Gradle plugins for the rest of the Gradle configuration.
tut-app-1
depends on the app-android-module
plugin.
tut-app-1
depends on our shared libraries using a declaration such as: implementation(project(":shared-library-1"))
The shared-library-1
shared library module, matches the simplicity of the app build.gradle.kts
file, in that it only declares its dependencies
and then delegates via Gradle plugins for the rest of the Gradle configuration.
shared-library-1
depends on library-android-module
.
app-android-module
declares any build configuration specific to your application build (such as the versionCode
).
library-android-module
declares any build configuration specific to all shared libraries (such as a constant build config field).
app-android-module
and library-android-module
both depend on the android-module
plugin.
Finally the android-module
plugin declares all shared Gradle configuration that is typically duplicated between side-projects / apps and modules.
Conclusion
Whilst the initial setup of a monorepo is more in-depth than a single project. Once it is setup, it gives you a powerful mechanism for sharing code between projects and for separating the responsibilities of build configuration from app development.
A couple of things to be wary of with monorepo’s:
- With a monorepo, the idea is to have one instance of the IDE (Android Studio) open per side-project (per app). The IDE works with “gradle roots” and each side project app is a Gradle root. i.e. if your monorepo has two projects and you want to work on both, then you should have two instances of your IDE running.
- Your shared libraries are shared between apps. This means if you change the public API in one of your libraries, the other apps using that API will be broken and will need fixing. This isn’t so much of a problem if you can fix projects as you open them or ignore your old broken projects. An alternative solution is, when you find a shared library is used and updated a lot between projects it is a good candidate to be separated into its own build and versioned for release.
I hope you’ve enjoyed this walk through, all code is available on the GitHub repo and any feedback is always appreciated!