background
This article describes how to share code between your secondary projects without code duplication, remote hosting, or version control. Create your own Android Monorepo repository using the Gradle build system and some magic!Copy the code
What is monorepo? In version control systems, monorePO (" mono "stands for" single "and" repo "is short for" repository ") is a software development strategy in which the code for many projects is stored in the same repository.Copy the code
As developers, we can have many (unfinished) side projects, and many times we do the same thing over and over again. Most applications require logging, most applications access network endpoints, most applications load images, and so on.Copy the code
Our side projects usually involve copying and pasting code, then modifying that code in the latest release to further improve it (or creating a library and hosting it). Sometimes I'll go back to an old project and have to copy and paste back my updated features. Other times I forget the great features I added to the last project (such as the rollback of network requests), and the new side project is not as "fancy" in that area.Copy the code
Monorepo can help. When starting a new side project, if you turn everything into a module, then you include the module you want, and then you can get up and running and focus on the new content of this new side project, instead of erecting scaffolding and reinventing your personal wheel every time.Copy the code
Here's what you have when you use Monorepo:Copy the code
We can use some explanatory naming to see what the project structure is. Hopefully seeing both allows you to understand and map to both when you do this for yourself. Here's what our monorepo folder structure looks like, and where the Gradle files involved will be when it's finished:Copy the code
For comparison, this is what a "typical" side project single application project looks like:Copy the code
We can see that modules in a separate side project become shared-libraries in monorepo. Gradle build logic is usually moved to its own "project" in app and moduleN folders. In Gradle, this is called a composite build and is key to the way Monorepo works.Copy the code
Monorepo starts with the root folder, which contains three things: 2) Gradle Build logic - our MonorePO and the build logic of the application (1 folder, Each application has subfolders) - Share build logical modules between applications 3) Shared libraries - Libraries to be shared between projects (1 folder per library)Copy the code
A composite build is just a build that contains other builds. In many ways, composite builds are similar to Gradle multi-project builds, except that they do not contain individual projects but complete builds.Copy the code
Prerequisite: Folder structure
Create a folder named monorepo and within this folder (that is, in the root folder of monorepo) create another folder named mono-libraries and another folder named mono-build-logic. Now we can start populating these folders with Gradle configuration files.Copy the code
If you want to add this Monorepo to version control, make sure you call Git init from the Monorepo folder and not from any subfolders.Copy the code
1) Side project creation
The important tip is to use Monorepo, the idea being to open an instance of the IDE (Android Studio) for each side project (per application). The IDE works using the "Gradle Roots" principle, and we will make each project application a Gradle root. That is, if your MonorePO has two projects and you want to work on both, you should run both instances of the IDE.Copy the code
/tut-app-1/settings.gradle.kts
Edit /monorepo/tut-app-1/settings.gradle. KTS and add an includeBuild method call to the pluginManagement block. This is called a Composite Build by the Gradle Build system and will allow us to separate the Android Build logic from the application we are building. IncludeBuild will reference our mono-build-logic folder. Like this:Copy the code
Next, we'll have our project include our shared-Library-1 (think HTTP or daily logging from Git repositories). This is what you normally do with the include method, and you can see that the app module is included like this. For Monorepo, the included libraries must be slightly different, but this can be easily done if you create a helper method (monoInclude). Like this:Copy the code
include("app") monoInclude("shared-library-1") fun monoInclude(name: String) { include(":$name") project(":$name").projectDir = File(".. /mono-libraries/$name") }Copy the code
We have declared that we want to build monorepo/mono-libraries/shared-library-1 but we haven't created that module yet.Copy the code
/tut-app-1/build.gradle.kts
For the Monorepo setting, there are no abnormal changes to the project build file, except for one. Here, we declare an additional project attribute for compose_Version. This will allow us to reference the compose version number later in all of our application's dependencies. If you take your Monorepo to a new level, you might move this version control to mono-build-Logic.Copy the code
buildscript { extra.apply { set("compose_version", Repositories "1.0.5")} {Google mavenCentral () ()} dependencies {classpath (" com. Android. View the build: gradle: 7.0.4 ") Classpath (" org. Jetbrains. Kotlin: kotlin - gradle - plugin: 1.5.31 ")}}Copy the code
The next step is to declare our application module build file, while delegating most of the work to our single build logic.Copy the code
/tut-app-1/app/build.gradle.kts
This file is usually where your application's build logic resides, but we'll delegate it to the Mono-build-Logic project to reduce duplication.Copy the code
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: the core - KTX: 1.7.0 ") implementation (" androidx. Appcompat: appcompat: 1.4.1 ") Implementation (" com. Google. Android. Material: material: 1.5.0 ") implementation (" androidx.com pose. The UI: UI: $composeVersion ") / / + other dependencies; see git repo debugImplementation("androidx.compose.ui:ui-tooling:$composeVersion") }Copy the code
Please note that we do not declare a dependency on the ID ("com.android.application") plug-in as usual for standalone applications. We'll use our own plug-in, Tut-app-1, which will help reduce duplication; We'll declare a dependency on android plug-ins there (no magic, just mobile!). .Copy the code
To make the Tut-app-1 module dependent on shared-Library-1 (i.e., the logging module in Git repository), we use implementation(Project (nested method calls). Like this:Copy the code
dependencies {
...
implementation(project(":shared-library-1"))
...
}
Copy the code
Note that the dependency block uses: Val implementation by ConfigurationsCopy the code
This is because we have moved the Gradle build logic from the application to the composite build plug-in (to avoid duplication in Monorepo), so the Gradle Kotlin DSL file has no way of knowing at script compile time whether these configurations are available when the script is applied. Therefore, the Kotlin extension (implementation()) for these configurations is not available at script compile time. You must refer to the configuration by name at run time.Copy the code
This is the completion of the /tut-app-1/app/build.gradle. KTS file, and this is the completion of the tut-app-1 setup. The next step is to recreate the build logic we omitted from the file by creating a plug-in in our Mono-build-Logic project.Copy the code
2) Share Gradle build logic
Our build logic project is where we can declare how the build works. (The good news is that this part is one-time, so it's a lot, but we don't need to do it again!) We want to declare that our project uses Android, that it is an application and there are libraries available, and we declare the usual things like the minSDK version and the application version and so on.Copy the code
Consider this build logic folder itself as a project, which enables us to make individual Gradle plug-ins and use them for each of our application project modules. So we'll have a plug-in for Android libraries, a plug-in for Android applications, then application-specific plug-ins, and then we can customize this shared build logic for individual applications.Copy the code
The first thing to do in this build logic project is to create the file structure. We already have a mono-build-Logic folder. Create three sub-folders in this folder and name them Android-plugins, Android-plugins-k, and Tut-app-1-plugins. These will become modules in the project.Copy the code
/mono-build-logic/android-plugins/build.gradle.kts
First, create a new file called build.gradle.kts in the Android-plugins folder. This is the file that declares how to build this module (Android-plugins). For this module, we will use the Groovy language as a plug-in, and the rest of Monorepo will use the Kotlin language entirely. We use Groovy here because it has a more powerful reflection syntax, so we can configure the Android build system without having to rely on Android plug-ins ourselves. The id("groovy-gradle-plugin") enables us to write groovy Gradle plugins. Your file should end up like this:Copy the code
plugins { id("groovy-gradle-plugin") // This enables src/main/groovy } dependencies { Implementation (" com. Android. Tools. Build: gradle: 7.0.4 ")}Copy the code
We now have a project to create groovy plug-ins. The next step is to declare a Groovy plug-in in this module that will set the defaults for all of our Android builds.Copy the code
/mono-build-logic/android-plugins/src/main/groovy/android-module.gradle
Create an Android-module. gradle in the Android-plugins/SRC /main/groovy folder. This will be a script plug-in that forms the basis for each Android module that will be included in any side project (application) in Monorepo.Copy the code
When you have script plug-ins like this, they use their filename as their name when referencing them by ID. For example using this plugin looks like id(" Android-module ") because the file name is android-module.build.kts.Copy the code
As you can see below, we use afterEvalute to check whether the project (module) to which the plug-in is attached is an Android project. If so, we configure the Android closure with all the default values, and we don't want to repeat them in every application.Copy the code
We used the Groovy plug-in here because it has a more powerful reflection syntax, so we can configure the Android build system without relying directly on the Android application or library plug-in (because we want it to work with both).Copy the code
afterEvaluate { project -> if (project.hasProperty("android")) { android { compileSdk 31 defaultConfig { minSdk 28 targetSdk 30 vectorDrawables { useSupportLibrary true } testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } testOptions { unitTests.returnDefaultValues = true } composeOptions { PackagingOptions kotlinCompilerExtensionVersion compose_version} {resources {excludes + = "/ meta-inf / {AL2.0, LGPL2.1}" } } compileOptions { sourceCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11 } kotlinOptions { jvmTarget = "11" useIR = true } buildFeatures { compose true } } } }Copy the code
It all depends on your default Settings here, I would say start with this and migrate/override properties when you find specific differences in an application (e.g. MinSDK).Copy the code
This is/mono - build - logic/android - plugins/SRC/main/groovy/android - module. Gradle file is complete. We now have a reusable plug-in that declares some basic shared Android properties. Next, we'll create another shared plug-in project so that we can declare individual plug-ins that can be used specifically for android libraries or Android application modules.Copy the code
/mono-build-logic/android-plugins-k/build.gradle.kts
Now that we have created the Android-plugins build logic project, we need to create another project, Android-plugins-k. Create a file named build.gradle. KTS in the /mono-build-logic/android-plugins-k/ folder. The only difference is that this module contains the Kotlin script plug-in, while the previous module contains a Groovy script plug-in.Copy the code
This project (module) relies on the Kotlin-DSL plug-in, which will allow us to write the Kotlin scripting plug-in. Note that we also added an API dependency to the Groovy Android-Plugins module. This allows the current module to access the plug-ins of the next module and makes them available to any dependent project.Copy the code
plugins { `kotlin-dsl` // This enables src/main/kotlin } dependencies { api(project(":android-plugins")) Implementation (" org. Jetbrains. Kotlin: kotlin - gradle - plugin: 1.5.31 ") Implementation (" com. Android. Tools. Build: gradle: 7.0.4 ")}Copy the code
Now we have a Kotlin project to create the plug-in. The next step is to declare two Kotlin plug-ins in this module that will set the default values for our Android library and Android application build.Copy the code
/mono-build-logic/android-plugins-k/src/main/kotlin/app-android-module.gradle.kts
Create the/SRC /main/kotlin folder structure and create a file named app-Android-module.gradle.kts inside it. This will be our plug-in for every Android application. We will declare details related to application modules here, but we will not specify details related to any particular applicationCopy the code
This plugin relies on the Android-Module plugin we already created. It also declares dependencies on com.Android. application (because this plug-in is expected to be used for Android applications) and Kotlin-Android (because we use Kotlin in our application).Copy the code
The plug-in then declares two build types for any Android application that relies on it. The plug-in ensures that minIFICATION is turned on for the release type and that the debug version adds "debug" to its application ID (that is, the install package).Copy the code
plugins {
id("android-module")
id("com.android.application")
id("kotlin-android")
}
android {
buildTypes {
getByName("release") {
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
getByName("debug") {
isDebuggable = true
applicationIdSuffix = ".debug"
}
}
}
Copy the code
We now have a reusable Android application plug-in that declares shared Android properties specific to the application module. Next we'll create another shared plug-in, this time for the shared Android library module.Copy the code
/mono-build-logic/android-plugins-k/src/main/kotlin/library-android-module.gradle.kts
In addition to the application plugin, in SRC /main/kotlin, create a file named library-Android-module.gradle.kts. This will be our plug-in for each Android library module. We'll declare details about library modules here, but we won't have any specific library details -- this will be in the shared library itself.Copy the code
This plugin relies on the Android-Module plugin we already created. It also declares dependencies on com.Android. library (because the plug-in is expected to be used for the Android library module) and Kotlin-Android (because we use Kotlin in our application).Copy the code
For library modules, we don't have any additional configuration in this plug-in.Copy the code
plugins {
id("android-module")
id("com.android.library")
id("kotlin-android")
}
Copy the code
We now have a reusable Android library plug-in that declares shared Android properties specific to library modules. Next, we'll create a specific plug-in for the Tut-app-1 side project to rely on.Copy the code
/mono-build-logic/tut-app-1-plugins/build.gradle.kts
Create a new file called build.gradle.kts in the tut-app-1 folder. This is the file that declares how to build this module (Tut-app-1). We will use this module to create an application-specific build script that allows us to separate the build logic into its own separate testable project and simplify side-project-1 Gradle files. Note that we also added implementation dependencies for the Kotlin Android-plugins-k module. This allows the current module to access the plug-in of the latter module (and the Groovy module declared by its API).Copy the code
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 "))}Copy the code
Now we have a build logic project to create an application-specific plug-in. Next, create the plug-in itself for Tut-app-1.Copy the code
/mono-build-logic/tut-app-1-plugins/src/main/kotlin/tut-app-1.gradle.kts
Create a new file in SRC /main/kotlin/, tut-app-1.gradle. KTS. This is where we rely on app-Android-module (because this plugin works with Android application modules).Copy the code
We set the details specific to this side project (application). So we declare what the installed applicationId is and the version of the application.Copy the code
The rest of the Android build logic is handled up in the plug-in: app-Android-Module and Android-Module (which we configured earlier). If you want to know, the only thing you need to declare (coming soon) is the Dependencies block. This will be handled in the side project (application) itself.Copy the code
plugins { id("app-android-module") } android { defaultConfig { applicationId = "com.blundell.tut1" versionCode = 1 VersionName = "1.0.0"}}Copy the code
Now we have an application-specific plug-in that allows us to abstract the complexity of building a project from the building application. The next step is to update the settings.gradle. KTS file so that the project (mono-build-logic) can build each module (project) we just created.Copy the code
/mono-build-logic/settings.gradle.kts
Now let's make all of the above modules that we create in this project recognized and compiled by Gradle. Create settings.gradle.kts in the /mono-build-logic root folder. Once created, synchronize your IDE, which will recognize all declared modules.Copy the code
dependencyResolutionManagement {
repositories {
gradlePluginPortal()
mavenCentral()
google()
}
}
include("android-plugins")
include("android-k-plugins")
include("tut-app-1-plugins")
Copy the code
Next comes the final part, creating shared libraries that can be used by the side project (application).Copy the code
3) the Shared library
Create an Android library project (or move an existing project) and make sure it is created in the /mono-repo/mono-libraries folder. From now on, we'll call it shared-Library-1 (In the GitHub repository example, we have an HTTP and a logging module; They work exactly the same way).Copy the code
/mono-libraries/shared-library-1/build.gradle.kts
We must enable this module to build the system using Gradle. Create a build.gradle. KTS file in shared-library-1.Copy the code
In this case, we want our module to build an Android library, so we rely on our newly created library-Android-Module plug-in. This allows you to inherit targetSDK, testOptions, compileOptions, packingExcludes, and more from our shared build logic plugin.Copy the code
Now the only thing you have to do for each library module is declare the dependencies to use. As follows:Copy the code
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 ")}Copy the code
For each shared library, add a line to the build file to rely on our library-Android-Module plug-in, declare your dependencies, and you're up and running.Copy the code
review
There is now an attached project, tut-app-1, that contains a compact build.gradle. KTS file that simply declares its dependencies and then delegates the rest of the Gradle configuration through the Gradle plug-in.Copy the code
Tut-app-1 relies on the app-Android-Module plug-in.Copy the code
Tut-app-1 relies on our shared library with the following declaration: implementation(project(":shared-library-1"))Copy the code
The shared-Library-1 shared library module matches the simplicity of the application build.gradle.kts file because it only declares its dependencies and then delegates the rest of the Gradle configuration through the Gradle plug-in.Copy the code
Shared-library-1 depends on the library-Android-module.Copy the code
App-android-module declares any build configuration (such as versionCode) specific to your application build.Copy the code
Library-android-module declares any build configuration (such as constant build configuration fields) specific to all shared libraries.Copy the code
Both app-Android-Module and library-Android-Module rely on the Android-Module plug-in.Copy the code
Finally, the Android-Module plug-in declares all shared Gradle configurations, which are often repeated between side projects/applications and modules.Copy the code
conclusion
Although monorepo's initial setup is more in-depth than a single project. Once set up, it gives you a powerful mechanism to share code between projects and separate the responsibility of building configuration from application development.Copy the code
A few things to note when using Monorepo:Copy the code
With Monorepo, the idea is to open one instance of the IDE (Android Studio) per side project (per application). The IDE uses a "Gradle root", each project application is a Gradle root. That is, if your MonorePO has two projects and you want to work on both, you should run both instances of the IDE.Copy the code
Shared libraries are shared between applications. This means that if you change the public API in one of the libraries, other applications that use that API will be broken and need to be fixed. This is not a big deal if you can fix them or ignore old broken projects when you open them. Another solution is when you find that shared libraries are being used and updated a lot between projects; Separating it into your own build and versioning it for release is a good candidate.Copy the code