Make a monorepo for your Android projects

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, complete builds 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:

  1. 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.
  2. 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!

Next Post

What Is Instagram Shadow Banned & How To Avoid It?

When you are hoping to improve a next on Instagram, it can be frustrating when your posts aren’t showing up any place. Instagram people depend upon the take a look at web site and hashtags for social advancement, but in some cases your engagement may well just cease responding or […]
What Is Instagram Shadow Banned & How To Avoid It?

You May Like