Trella Technical Education #1 – Android Multi-Module Multi-Apps Structure.

Our mission is to make trucking efficient, reliable and convenient. We aim to ease the hassle of repetitive tasks and make our clients focus only on the important issues.To provide a convenient experience we use the mobile as part of our channels to engage and notify our clients in realtime. We largely use Android as it dominates a larger percentage of our users. In light of this, We have decided to share some of our struggles and difficulties in our tech blog. There are limited resources elaborating on the tech elements of establishing these platforms and running them smoothly.

Let’s get started. 💫

This is part 1 of a 3 part series.

Part 1 will focus on the development of workflow and how to improve developers’ productivity on large scale projects. This will include implementing Continuous Integration (CI) pipeline to automate publishing Android Library using Github Actions.

Requirements 📄

  • GitFlow.
  • Basic Gradle scripting knowledge.

Motivation

Working on large scale projects has some interesting challenges.

A common obstacle is setting the project structure and coordination between the project’s Apps and Modules, and how it reflects on the development lifecycle.

For the project we are working on we have the following: Five different Android Apps, every app on its own project

( Some have common features and others do not ).

  1. Five Modules shared across the five apps.
  2. Three of the five modules depend on the other two modules.
  3. The Five Apps depend on the Five Modules. Check the diagram below visualizing the structure.

Within the structure shown below, we had a GitHub repository for each module as it’s shared across multiple apps.

Every app has its own repository, and the development workflow was the following:-

  1. We clone every module to our development machines.
  2. We include them on every app as local modules with its directory path on the machine so settings.gradle. The file was git ignored as it would change for everyone working on the projects.
  3. On making changes to modules or apps, we push them to their corresponding remote repositories, and we use GitFlow so we have to be updated with the latest version of the develop branch

Our Challenge 👨🏼‍💻

We all know the pain of creating a test build after finishing a feature. Conducting a manual build is really difficult as it may not as may have to do t more than one time, and in doing so triggers your inner desire to automate the process.

Based on this issue we decided to use a Continuous Integration (CI) pipeline for this process, This requires all of our local modules to have a remote version as a Gradle dependency.

This also requires removing all of our local modules integration for all of our apps, which will affect our development workflow**, however we will address this later in the next article.**

Changing Local Modules to Gradle Dependencies

Publishing an Android library present two key challenges:

  • Some of the modules have product flavors (Production/Staging) which are common to have.
  • We want to include a snapshot version of the libraries if anyone wants to make changes and test its result on the apps before proceeding with Pull Request Review, which will be built automatically on git push to any branch except develop/master.

Creating Android Library with Product Flavors and Snapshot Versions with Github Actions & Github Packages

Github Actions & Packages meet our requirements as Github Actions offer great support with Gradle, And are easily workable with the server setup boilerplate.

Github Packages is also a great place to host maven dependencies and provide private access options as those libraries will be used internally within the company’s projects.

Let’s start with Building 🏗️

Any Android library could be built into an aar version which is used to publish on Github Packages.to build the aar version if it doesn’t exist or after any changes to the library before publishing we use.

./gradlew assemble

and the output could be found in this directory after a successful build by making sure to select the release variant before building.

"${rootProject.rootDir}//module-name/build/outputs/aar/module-name-release.aar"

then we proceed to Github Packages configurations :

Step 1: Generate a Personal Access Token for GitHub

Inside your GitHub account:

**Settings -> Developer Settings -> Personal Access Tokens -> Generate new token**

Make sure you select the following scopes (“ write: packages”, “ read: packages”) and Generate a token

After Generating, copy the new personal access token as It will not appear again! The only option will be to regenerate a key or create a new key.

Step 2: Store your GitHub — Personal Access Token details

Create a github.properties file within your root Android project.

In case of a public repository, make sure you add this file to .gitignore to keep the token private.

Add properties gpr.usr=GITHUB_USERID and gpr.key=PERSONAL_ACCESS_TOKEN

Replace GITHUB_USERID with personal / organization Github User ID and PERSONAL_ACCESS_TOKEN with the token generated in #Step 1.

The final file should look like this

gpr.usr=GITHUB_USERID
gpr.key=PERSONAL_ACCESS_TOKEN

Step 3: Update build.gradle Inside the Library Module

Add the following code to build.gradle inside the library module

def githubProperties = new Properties()
githubProperties.load(new FileInputStream(rootProject.file("github.properties"))) //Set env variable GPR_USER & GPR_API_KEY if not adding a properties file

repositories {
        maven {
               name = "GitHubPackages"
        /** Configure path of your package repository on Github
         ** Replace GITHUB_USERID with your/organisation Github userID
         ** and REPOSITORY with the repository name on GitHub
        */
 url = uri("<https://maven.pkg.github.com/GITHUB_USERID/REPOSITORY>")
credentials {
        /** Create github.properties in root project folder file with
         ** gpr.usr=GITHUB_USERID-app & gpr.key=PERSONAL_ACCESS_TOKEN
         ** Set env variable GPR_USER & GPR_API_KEY if not adding a properties file**/

         username = githubProperties['gpr.usr'] ?: System.getenv("GPR_USER")
         password = githubProperties['gpr.key'] ?: System.getenv("GPR_API_KEY")
      }
    }
  }
}

What we have done in the previous step is access the properties that we have added in github.properties, and then used it as a credential to be able to access the library’s repository Github Packages using our personal access token.

Step 4: Creating Gradle Publishing Script

We will go step by step through the regular publish script until we customize it, so we may add support for product flavours and snapshot versioning

  • We will add the Maven Gradle Publishing plugin
apply plugin: 'maven-publish' // Apply this plugin at the top of your library build.gradle

and then add the publish script

def getVersionName = { ->
 return "1.0.2" // Replace with version Name
}

def getArtifactId = { ->
 return "sampleAndroidLibrary" // Replace with library name ID
}

publishing {
 publications {
   bar(MavenPublication) {
      groupId 'com.sample.library' // Replace with group ID
      artifactId getArtifactId()
      version getVersionName()
     artifact("${rootProject.rootDir}//module-name/build/outputs/aar/module-name-release.aar")
    }
 }

The final resulting file will look like this

apply plugin: 'maven-publish' // Apply this plugin at the top of your library build.gradle

def getVersionName = { ->
 return "1.0.2" // Replace with version Name
}

def getArtifactId = { ->
 return "sampleAndroidLibrary" // Replace with library name ID
}

def githubProperties = new Properties()
githubProperties.load(new FileInputStream(rootProject.file("github.properties"))) //Set env variable GPR_USER & GPR_API_KEY if not adding a properties file

publishing {
 publications {
   bar(MavenPublication) {
      groupId 'com.sample.library' // Replace with group ID
      artifactId getArtifactId()
      version getVersionName()
     artifact("${rootProject.rootDir}//library-name/build/outputs/aar/library-name-release.aar")
    }
 }

repositories {
        maven {
               name = "GitHubPackages"
        /** Configure path of your package repository on Github
         ** Replace GITHUB_USERID with your/organisation Github userID
         ** and REPOSITORY with the repository name on GitHub
        */
 url = uri("<https://maven.pkg.github.com/GITHUB_USERID/REPOSITORY>")
credentials {
        /** Create github.properties in root project folder file with
         ** gpr.usr=GITHUB_USERID-app & gpr.key=PERSONAL_ACCESS_TOKEN
         ** Set env variable GPR_USER & GPR_API_KEY if not adding a properties file**/

         username = githubProperties['gpr.usr'] ?: System.getenv("GPR_USER")
         password = githubProperties['gpr.key'] ?: System.getenv("GPR_API_KEY")
      }
    }
  }
}

We added the maven-publishplugin that is provided out of the box for Android libraries to help us publish maven dependencies easier.

Next is library’s groupId which is the Package name identifier in Projects.

Then artifactId, which is the library name.

Lastly, the version which is the library version.

By executing the following code in the terminal.

./gradlew publish

Alternatively, Go to the Gradle tab in Android Studio > YOUR_PROJECT_NAME(root) -> Publish.

Once the task is successfully published you should be able to see the Package under the Packages tab of the GitHub Account

Step 5: Auto-Increment (Dynamically) Android Library Version on Publishing

Of course, we will not manually update the version with every new change but rather automate the Patch version (the latest number in the version number eg “2” in “1.0.2”) and then let the Major, Minor version to be updated manually. This happens with the following steps:

def versionPropsFile = file('version.properties')
            def value = 0
            Properties versionProps = new Properties()
            if (!versionPropsFile.exists()) {
                versionProps['VERSION_PATCH'] = "0"
                versionProps.store(versionPropsFile.newWriter(), null)
            }
            def runTasks = gradle.startParameter.taskNames
            if ('publish' in runTasks) {
                value = 1
            }

            def mVersionName = ""
            if (versionPropsFile.canRead()) {
                versionProps.load(new FileInputStream(versionPropsFile))
                versionProps['VERSION_PATCH'] = (versionProps['VERSION_PATCH'].toInteger() + value).toString()
                versionProps.store(versionPropsFile.newWriter(), null)
                // 1: change major and minor version here
                mVersionName = "1.0.${versionProps['VERSION_PATCH']}"
                println mVersionName
            } else {
                throw new FileNotFoundException("Could not read version.properties!")
            }

            def getVersionName = { ->
                return "${mVersionName}"
            }

What this script does that it looks for a file in the project named version.properties, and if it doesn’t exist, we will create one with a default value VERSION_PATCH = "0" to then store the file in our project with

 versionProps.store(versionPropsFile.newWriter(), null)
  • We check to make sure the publish task (we will come to it later) is being run now. If it’s running we make a variable value to have 1 (the value that we will add to our current version patch), we do this as this script is always executed with every Gradle build (regardless if it’s a publish task or not). By doing this we confirm that we are currently publishing the library.
  • The rest of the script is that we read the version.properties (which should be created after this step), and get the current version patch value and add our value variable to it to assign the output result to another variable mVersionName which we reuse in the getVersionName function. Finally, we use it for the publish maven task as our desired new version.
  • We will add this script inside our publish task, the final build.gradle file should look like this
apply plugin: 'maven-publish' // Apply this plugin at the top of your library build.gradle
def getArtifactId = { ->
 return "sampleAndroidLibrary" // Replace with library name ID
}

def githubProperties = new Properties()
githubProperties.load(new FileInputStream(rootProject.file("github.properties"))) //Set env variable GPR_USER & GPR_API_KEY if not adding a properties file

publishing {
 publications {
   bar(MavenPublication) {
def versionPropsFile = file('version.properties')
            def value = 0
            Properties versionProps = new Properties()
            if (!versionPropsFile.exists()) {
                versionProps['VERSION_PATCH'] = "0"
                versionProps.store(versionPropsFile.newWriter(), null)
            }
            def runTasks = gradle.startParameter.taskNames
            if ('publish' in runTasks) {
                value = 1
            }

            def mVersionName = ""
            if (versionPropsFile.canRead()) {
                versionProps.load(new FileInputStream(versionPropsFile))
                versionProps['VERSION_PATCH'] = (versionProps['VERSION_PATCH'].toInteger() + value).toString()
                versionProps.store(versionPropsFile.newWriter(), null)
                // 1: change major and minor version here
                mVersionName = "1.0.${versionProps['VERSION_PATCH']}"
                println mVersionName
            } else {
                throw new FileNotFoundException("Could not read version.properties!")
            }

            def getVersionName = { ->
                return "${mVersionName}"
            }
      groupId 'com.sample.library' // Replace with group ID
      artifactId getArtifactId()
      version getVersionName()
     artifact("${rootProject.rootDir}//module-name/build/outputs/aar/module-name-release.aar")
    }
 }

repositories {
        maven {
               name = "GitHubPackages"
        /** Configure path of your package repository on Github
         ** Replace GITHUB_USERID with your/organisation Github userID
         ** and REPOSITORY with the repository name on GitHub
        */
 url = uri("<https://maven.pkg.github.com/GITHUB_USERID/REPOSITORY>")
credentials {
        /** Create github.properties in root project folder file with
         ** gpr.usr=GITHUB_USERID-app & gpr.key=PERSONAL_ACCESS_TOKEN
         ** Set env variable GPR_USER & GPR_API_KEY if not adding a properties file**/

         username = githubProperties['gpr.usr'] ?: System.getenv("GPR_USER")
         password = githubProperties['gpr.key'] ?: System.getenv("GPR_API_KEY")
      }
    }
  }
}

Step 6:Enabling Snapshot Versioning for Android Library with Github Packages

As we discussed we will need to enable a snapshot versioning for our library which is implemented using the same concept as auto-increment versioning.

Instead of storing a version number, we shall store a boolean indicating if this build is a snapshot, or not with the snapshot version.

The question presented is how will we change the boolean value? By creating a Gradle task which we shall run before any publishing, and set the boolean to be true if we want a snapshot version and false if we want a stable version.

task setSnapshot {
    doLast {
        def versionPropsFile = file('version.properties')
        Properties versionProps = new Properties()
        if (!versionPropsFile.exists()) {
            versionProps['IS_SNAPSHOT'] = isSnapshot
            versionProps['SNAPSHOT_VERSION_PATCH'] = "0"
            versionProps['VERSION_PATCH'] = "0"
            versionProps.store(versionPropsFile.newWriter(), null)
        }
        if (versionPropsFile.canRead()) {
            versionProps.load(new FileInputStream(versionPropsFile))
            versionProps['IS_SNAPSHOT'] = isSnapshot
            versionProps.store(versionPropsFile.newWriter(), null)
        }
    }
}

Sounds familiar right?

The only changes between this task and the dynamic versioning are that we introduced the IS_SNAPSHOT, SNAPSHOT_VERSION_PATCH properties in our version.properties file.

We use a value isSnapshot, but it wasn’t defined, and its value will be sent through a terminal command to trigger this Gradle task which we run before running the publish task.

We do however need to update our versioning logic, so we make use of the snapshot versioning option.

 def versionPropsFile = file('version.properties')
            def value = 0
            Properties versionProps = new Properties()
            if (!versionPropsFile.exists()) {
                versionProps['IS_SNAPSHOT'] = false
                versionProps['SNAPSHOT_VERSION_PATCH'] = "0"
                versionProps['VERSION_PATCH'] = "0"
                versionProps.store(snapshotPropsFile.newWriter(), null)
            }
            if (versionPropsFile.canRead()) {
                versionProps.load(new FileInputStream(versionPropsFile))
            }

            def runTasks = gradle.startParameter.taskNames
            if ('publish' in runTasks) {
                value = 1
            }

            def mVersionName = ""
            def mArtifact = ""

            if (versionPropsFile.canRead()) {
                versionProps.load(new FileInputStream(versionPropsFile))
                def isSnapshot = versionProps['IS_SNAPSHOT']
                if (isSnapshot != null && isSnapshot.toBoolean()) {
                    versionProps['SNAPSHOT_VERSION_PATCH'] = (versionProps['SNAPSHOT_VERSION_PATCH'].toInteger() + value).toString()
                } else
                    versionProps['VERSION_PATCH'] = (versionProps['VERSION_PATCH'].toInteger() + value).toString()
                versionProps.store(versionPropsFile.newWriter(), null)
                if (isSnapshot != null && isSnapshot.toBoolean()) {
                    // 1: change major and minor version here
                    mVersionName = "1.0.${versionProps['SNAPSHOT_VERSION_PATCH']}"
                    mArtifact = "snapshot"
                } else {
                    // 1: change major and minor version here
                    mVersionName = "1.0.${versionProps['VERSION_PATCH']}"
                    mArtifact = "sampleAndroidLibrary"

                }
                println mVersionName
            } else {
                throw new FileNotFoundException("Could not read version.properties!")
            }

            def getVersionName = { ->
                return "${mVersionName}"
            }
            def getArtifactId = { ->
                return "${mArtifact}"
            }

You will notice here that we check if the IS_SNAPSHOT is true then we change the version value to SNAPSHOT_VERSION_PATCH.

We then set our artifact value as snapshot so we could distinguish the snapshot version from a stable version.

In case of displaying false we repeat our previous logic in #Step 5.

Let’s try to make a snapshot version of the library by running these commands in terminal

./gradlew -PisSnapshot=true setSnapshot --stacktrace
./gradlew publish

After a successful build we should see the following in the Github packages tab:

Step 7: Create Product Flavors for Android Library with Github Packages.

If your library has product flavors, for example, Production and Staging flavors and you want to create different versions for each one;

We will first check it’s binaries (aka aar files).

By running the same ./gradlew assemble task we will have an aar file generated for every flavor of our library and their directories will show.

"${rootProject.rootDir}//library-name/build/outputs/aar/library-name-production-release.aar"
"${rootProject.rootDir}//library-name/build/outputs/aar/library-name-staging-release.aar"

As soon as we have the aar files then we could publish them easily using a Maven Plugin and the coding scheme would present this

publishing {
    publications {
        production(MavenPublication) {
            .....
            groupId 'com.sample.library'
            artifactId getArtifactId()
            version getVersionName()
            artifact("${rootProject.rootDir}//library-name/build/outputs/aar/library-name-production-release.aar")

        }
        staging(MavenPublication) {
            .....
            groupId 'com.sample.library'
            artifactId getArtifactId()
             version getVersionName()
            artifact("${rootProject.rootDir}//library-name/build/outputs/aar/library-name-staging-release.aar")
        }
    }

As simple as that, we integrate our dynamic & snapshot versioning script with these and see the final build.gradle file

apply plugin: 'maven-publish' // Apply this plugin at the top of your library build.gradle
def githubProperties = new Properties()
githubProperties.load(new FileInputStream(rootProject.file("github.properties"))) //Set env variable GPR_USER & GPR_API_KEY if not adding a properties file

task setSnapshot {
    doLast {
        def versionPropsFile = file('version.properties')
        Properties versionProps = new Properties()
        if (!versionPropsFile.exists()) {
            versionProps['IS_SNAPSHOT'] = isSnapshot
            versionProps['SNAPSHOT_VERSION_PATCH'] = "0"
            versionProps['VERSION_PATCH'] = "0"
            versionProps.store(versionPropsFile.newWriter(), null)
        }
        if (versionPropsFile.canRead()) {
            versionProps.load(new FileInputStream(versionPropsFile))
            versionProps['IS_SNAPSHOT'] = isSnapshot
            versionProps.store(versionPropsFile.newWriter(), null)
        }
    }
}

publishing {
 publications {
   production(MavenPublication) {
            def versionPropsFile = file('version.properties')
            def value = 0
            Properties versionProps = new Properties()
            if (!versionPropsFile.exists()) {
                versionProps['IS_SNAPSHOT'] = false
                versionProps['SNAPSHOT_VERSION_PATCH'] = "0"
                versionProps['VERSION_PATCH'] = "0"
                versionProps.store(snapshotPropsFile.newWriter(), null)
            }
            if (versionPropsFile.canRead()) {
                versionProps.load(new FileInputStream(versionPropsFile))
            }

            def runTasks = gradle.startParameter.taskNames
            if ('publish' in runTasks) {
                value = 1
            }

            def mVersionName = ""
            def mArtifact = ""

            if (versionPropsFile.canRead()) {
                versionProps.load(new FileInputStream(versionPropsFile))
                def isSnapshot = versionProps['IS_SNAPSHOT']
                if (isSnapshot != null && isSnapshot.toBoolean()) {
                    versionProps['SNAPSHOT_VERSION_PATCH'] = (versionProps['SNAPSHOT_VERSION_PATCH'].toInteger() + value).toString()
                } else
                    versionProps['VERSION_PATCH'] = (versionProps['VERSION_PATCH'].toInteger() + value).toString()
                versionProps.store(versionPropsFile.newWriter(), null)
                if (isSnapshot != null && isSnapshot.toBoolean()) {
                    // 1: change major and minor version here
                    mVersionName = "1.0.${versionProps['SNAPSHOT_VERSION_PATCH']}"
                    mArtifact = "snapshot-production"
                } else {
                    // 1: change major and minor version here
                    mVersionName = "1.0.${versionProps['VERSION_PATCH']}"
                    mArtifact = "name-production"

                }
                println mVersionName
            } else {
                throw new FileNotFoundException("Could not read version.properties!")
            }

            def getVersionName = { ->
                return "${mVersionName}"
            }
            def getArtifactId = { ->
                return "${mArtifact}"
            }
      groupId 'com.sample.library' // Replace with group ID
      artifactId getArtifactId()
      version getVersionName()
      artifact("${rootProject.rootDir}//library-name/build/outputs/aar/library-name-production-release.aar")
    }
    staging(MavenPublication) {
                def versionPropsFile = file('version.properties')
                def value = 0
                Properties versionProps = new Properties()
                if (!versionPropsFile.exists()) {
                    versionProps['IS_SNAPSHOT'] = false
                    versionProps['SNAPSHOT_VERSION_PATCH'] = "0"
                    versionProps['VERSION_PATCH'] = "0"
                    versionProps.store(snapshotPropsFile.newWriter(), null)
                }
                if (versionPropsFile.canRead()) {
                    versionProps.load(new FileInputStream(versionPropsFile))
                }

                def runTasks = gradle.startParameter.taskNames
                if ('publish' in runTasks) {
                    value = 1
                }

                def mVersionName = ""
                def mArtifact = ""

                if (versionPropsFile.canRead()) {
                    versionProps.load(new FileInputStream(versionPropsFile))
                    def isSnapshot = versionProps['IS_SNAPSHOT']
                    if (isSnapshot != null && isSnapshot.toBoolean()) {
                        versionProps['SNAPSHOT_VERSION_PATCH'] = (versionProps['SNAPSHOT_VERSION_PATCH'].toInteger() + value).toString()
                    } else
                        versionProps['VERSION_PATCH'] = (versionProps['VERSION_PATCH'].toInteger() + value).toString()
                    versionProps.store(versionPropsFile.newWriter(), null)
                    if (isSnapshot != null && isSnapshot.toBoolean()) {
                        // 1: change major and minor version here
                        mVersionName = "1.0.${versionProps['SNAPSHOT_VERSION_PATCH']}"
                        mArtifact = "snapshot-staging"
                    } else {
                        // 1: change major and minor version here
                        mVersionName = "1.0.${versionProps['VERSION_PATCH']}"
                        mArtifact = "name-staging"

                    }
                    println mVersionName
                } else {
                    throw new FileNotFoundException("Could not read version.properties!")
                }

                def getVersionName = { ->
                    return "${mVersionName}"
                }
                def getArtifactId = { ->
                    return "${mArtifact}"
                }
          groupId 'com.sample.library' // Replace with group ID
          artifactId getArtifactId()
          version getVersionName()
          artifact("${rootProject.rootDir}//library-name/build/outputs/aar/library-name-staging-release.aar")
        }
 }

repositories {
        maven {
               name = "GitHubPackages"
        /** Configure path of your package repository on Github
         ** Replace GITHUB_USERID with your/organisation Github userID
         ** and REPOSITORY with the repository name on GitHub
        */
 url = uri("<https://maven.pkg.github.com/GITHUB_USERID/REPOSITORY>")
credentials {
        /** Create github.properties in root project folder file with
         ** gpr.usr=GITHUB_USERID-app & gpr.key=PERSONAL_ACCESS_TOKEN
         ** Set env variable GPR_USER & GPR_API_KEY if not adding a properties file**/

         username = githubProperties['gpr.usr'] ?: System.getenv("GPR_USER")
         password = githubProperties['gpr.key'] ?: System.getenv("GPR_API_KEY")
      }
    }
  }
}

By running the Gradle sync, and then running the below commands we publish a snapshot production/staging version

./gradlew -PisSnapshot=true setSnapshot --stacktrace ./gradlew publish 

VOILA! We have 4 different variants of our library that we could use based on various cases

Step 8: Custom Gradle Project Configuration for build variants

Our library’s version would look like this

//Stable
'com.sample.library:name-production:1.0.2'
'com.sample.library:name-staging:1.0.2'
//snapshot
'com.sample.library:snapshot-production:1.0.2'
'com.sample.library:snapshot-staging:1.0.2'

Let’s assume that we need to have the snapshot versions only on a debug variant, and the stable versions on release variants only. For that we use the following:

dependencies {
debugImplementation 'com.sample.library:snapshot-production:1.0.2'
releaseImplementation 'com.sample.library:name-production:1.0.2'
}

If your project doesn’t have different flavors such as the library then there is no need for changes, but if your project also has Production & Staging flavors, you only use the production versions. Also if we wanted to add a staging version we could do something like this

dependencies {
debugImplementation 'com.sample.library:snapshot-production:1.0.2'
releaseImplementation 'com.sample.library:name-production:1.0.2'
debugImplementation 'com.sample.library:snapshot-staging:1.0.2'
releaseImplementation 'com.sample.library:name-staging:1.0.2'

}

Unfortunately, Gradle will present an error because of duplicates, so let’s assume that we are on the debug variant now. Gradle will then sync and find those two exact same versions

debugImplementation 'com.sample.library:snapshot-production:1.0.2'
debugImplementation 'com.sample.library:snapshot-staging:1.0.2'

It won’t know which one to choose, so it will throw a duplicate exception. We solve this by generating two Build Variants (Release, Debug) and two Product Flavors (Production, Staging), and by calculating the scenarios we will have these combinations

 productionRelease
 productionDebug
 stagingRelease
 stagingDebug

We then want to implement a specific library configuration on each scenario. For example, in production release in our app we want production & release version library to read ‘com.sample.library:name-production:1.0.2’

Another example is stagingDebug, we want the staging flavor & snapshot (debug) version library to read ‘com.sample.library:snapshot-staging:1.0.2’ and so on…

We know that Gradle supports Flavor-based implementations e.g. productionImplementation also builds variant implementations e.g. debugImplementation.

However, our goal is to have something like productionDebugImplementation which combines both scenarios together. Luckily we could do this easily in our app build.gradle file by defining these custom configurations

configurations {
    productionReleaseImplementation
    productionDebugImplementation
    stagingReleaseImplementation
    stagingDebugImplementation
}

We don’t have to inform Gradle anything about these configurations as it’s already aware of these combinations by default. We just need to declare them and then we can use them instead of the regular implementation.

Using the new configurations of our implementation for the transactions module would be displayed like this

productionReleaseImplementation 'com.sample.library:name-production:1.0.2'
productionDebugImplementation 'com.sample.library:snapshot-production:1.0.2'
stagingReleaseImplementation 'com.sample.library:name-staging:1.0.2'
stagingDebugImplementation  'com.sample.library:snapshot-staging:1.0.2'

Sync your project and everything would be working like a charm 🔥

That’s it for now.

This is part 1 of a series of 3 articles. In the next segment, we will focus on the development workflow, and improve developers productivity with large-scale projects. In the last article, we will present how we could implement a Continuous Integration (CI) pipeline to automate publishing the Android Library using Github Actions.

Lastly, none of this is possible without some good music, here’s a link to my playlist:

https://open.spotify.com/playlist/4wxECzEZHxVXlnsNKkgclu?si=YfYZAnBCTvivsnVrS2lkkQ 🎸 Happy coding and stay tuned 👋🏼

Ahmed Elshaer | Senior Android Engineer at Trella