Common Gradle to share across modules in a multi-module Android project

I have more than 10 submodules for an Android Application. Most of the modules use the same gradle configuration and dependencies. So, I have created a base-module.gradle file at the root of the project as below:

base-module.gradle at the project level (inside the project directory. E.g. myapp/base-module.gradle):

apply from: '../config/ext.gradle'
apply from: '../jacoco/modules.gradle'

android {
    compileSdkVersion ver.compileSdkVersion
    buildToolsVersion ver.buildToolsVersion

    defaultConfig {
        minSdkVersion ver.minSdkVersion
        targetSdkVersion ver.targetSdkVersion

        vectorDrawables.useSupportLibrary = true
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

    flavorDimensions = ["distribution", "environment"]

    productFlavors {
        appTypeName {
            dimension "distribution"
        }

        anotherAppTypeName {
            dimension "distribution"
        }

        oneMoreAppTypeName {
            dimension "distribution"
        }

        experimental {
            dimension "environment"
            resourceConfigurations += ['en', 'xxhdpi']
        }

        alpha {
            dimension "environment"
        }

        beta {
            dimension "environment"
        }

        rcandi {
            dimension "environment"
        }

        uat {
            dimension "environment"
        }

    }

    buildFeatures {
        dataBinding = true
        buildConfig true
    }

    if (project.hasProperty('experimentalBuild')) {
        splits.abi.enable = false
        splits.density.enable = false
        aaptOptions.cruncherEnabled = false
    }

    kotlinOptions {
        jvmTarget = ver.sourceCompatibility
    }

    compileOptions {
        sourceCompatibility ver.sourceCompatibility
        targetCompatibility ver.sourceCompatibility
    }

    sourceSets {
        test {
            java.srcDirs += "../core/src/test/java"
        }
    }
}

dependencies {
    implementation project(':core')
    implementation project(':app-java-sdk')
    implementation project(':models')
    implementation libs.kotlin.stdlib
    implementation libs.androidx.recyclerview
    implementation libs.androidx.cardview
    implementation libs.androidx.constraintlayout

    implementation libs.androidx.material
    implementation(libs.retrofit) {
        exclude module: 'okhttp'
    }
    implementation libs.okhttp
    implementation libs.retrofit.converter.gson
    implementation libs.retrofit.adapter.rxjava2
    implementation libs.legacy.support.v4
    implementation libs.rxjava

    implementation libs.navigation.fragment.ktx
    implementation libs.navigation.ui.ktx
    implementation libs.koin.android
    implementation libs.sandwich.retrofit
    implementation libs.realm.kotlin.base
    // Test
    testImplementation libs.junit4
    testImplementation libs.junit.jupiter.api
    testImplementation libs.mockk
    testImplementation libs.assertj.core.old
    testImplementation libs.mockito.core
    testImplementation libs.kotlinx.coroutines.test

    implementation libs.lifecycle.common.java8
    testImplementation project(':core')

}

Now, a particular feature module would re-use the above gradle configuration as below (inside the module directory. e.g., myapp/feature_feed/build.gradle):

plugins {
    alias libs.plugins.android.library
    alias libs.plugins.kotlin.parcelize
    alias libs.plugins.kotlin.gradle.plugin
    alias libs.plugins.kotlin.kapt
    alias libs.plugins.navigation.safeargs.gradle.plugin
    alias libs.plugins.realm.plugin.kotlin
}

apply {
    from("$rootDir/base-module.gradle")
}

android {
    namespace 'com.my.app.feature_feed'
}

dependencies {
    implementation project(':thc_recyclerview')
    implementation project(':templates')
    implementation libs.multisnaprecyclerview
}

It is working fine. I want to understand pros and cons of the above approach, and the best practices to achieve the similar purpose.

The con is, that it is highly discouraged to use legacy script plugins (the ones you use with apply-from), due to many quirks with them you should not risk to hit.

Better use convention plugins, for example in buildSrc or an included build like gradle/build-logic, for example implemented as precompiled script plugin.

1 Like

Thanks @Vampire for the reply. I need your help to understand:

  1. What are those quirks/issues because of which we should avoid legacy script plugins?
  2. How the convention plugins solve the issues we get from the legacy script plugins?

Thanks in advance.

I don’t have them all in mind, just don’t use them, it is not without reason that they are highly discouraged.

For example there are pretty confusing classloader quirks when using them. And you cannot use the plugins { ... } block to apply plugins which you should always use. And when using Kotlin DSL (highly recommended), then in legacy script plugins you do not get type-safe accessors generated. To just name a few.

“convention plugin” does not solve anything, a “convention plugin” is just a plugin that applies your conventions, no matter how it is implemented, be it legacy script plugin, binary plugin in any JVM language, precompiled script plugin, …

If you actually meant how precompiled script plugins solve it, well, they benefit from the learnings with legacy script plugins which are way older, so how they solve the issues is simply by not causing those issues.