When developing Android applications, it’s common to build different versions of the same app to target various environments such as development, staging, and production. Android product flavors allow you to define separate configurations for each environment, making it easy to generate multiple APKs with different settings, such as base URLs, versioning, or feature flags.

In this article, we will show you how to integrate branch names dynamically into the version name of your APKs using product flavors. We’ll also walk through setting up a custom Gradle task to automate the build process for signed APKs and explain key parts of the code.

Setting Up Product Flavors

Aside from adding the branch name, we define the BASE_URL and FRONT_URL for each environment, ensuring that different URLs are used in production, development, and staging builds. Here’s how the defaultConfig looks for this setup:

build.gradle (Module:app)
defaultConfig {
    applicationId "com.example.app"
    minSdkVersion 24
    targetSdkVersion 34
    versionCode 135
    versionName "2.6.10"

    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    ndk.abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
    signingConfig signingConfigs.release

    vectorDrawables {
        useSupportLibrary true
    }

    // Define different base URLs and front URLs for various environments
    buildConfigField "String", "PROD_BASE_URL", "\"https://api-prod.example.com\""
    buildConfigField "String", "PROD_FRONT_URL", "\"https://www.example.com\""
    buildConfigField "String", "DEV_BASE_URL", "\"https://api-dev.example.com\""
    buildConfigField "String", "DEV_FRONT_URL", "\"https://dev.example.com\""
    buildConfigField "String", "STAGING_BASE_URL", "\"https://api-staging.example.com\""
    buildConfigField "String", "STAGING_FRONT_URL", "\"https://staging.example.com\""
}

The defaultConfig section defines common settings such as applicationId, SDK versions, and URLs for all environments. Different BASE_URL and FRONT_URL are defined for each flavor using buildConfigField.

Branch Name Integration in Product Flavors

By incorporating the branch name in the APK version name, you can easily identify which branch the APK was built from. This can be particularly useful for tracking releases and managing versions across multiple environments. Below is the updated setup with branch name integration.


def branchName = getGitBranchName()

productFlavors {
    production {
        dimension "environment"
        buildConfigField "String", "BASE_URL", "BuildConfig.PROD_BASE_URL"
        buildConfigField "String", "FRONT_URL", "BuildConfig.PROD_FRONT_URL"
        if (branchName) {
            versionNameSuffix "-prod-${branchName}"
        }
    }
    development {
        dimension "environment"
        buildConfigField "String", "BASE_URL", "BuildConfig.DEV_BASE_URL"
        buildConfigField "String", "FRONT_URL", "BuildConfig.DEV_FRONT_URL"
        if (branchName) {
            versionNameSuffix "-dev-${branchName}"
        } else {
            versionNameSuffix "-dev"
        }
    }
    staging {
        dimension "environment"
        buildConfigField "String", "BASE_URL", "BuildConfig.STAGING_BASE_URL"
        buildConfigField "String", "FRONT_URL", "BuildConfig.STAGING_FRONT_URL"
        if (branchName) {
            versionNameSuffix "-staging-${branchName}"
        } else {
            versionNameSuffix "-staging"
        }
    }
}

In this setup, the getGitBranchName() function dynamically retrieves the current Git branch name. If a branch name is available, it is appended to the versionNameSuffix for each product flavor. This ensures that the APK version reflects the environment (production, development, or staging) and the branch it was built from.

This can be especially useful when debugging or deploying different versions of the app for testing purposes. You can quickly see which branch the APK came from, helping you better manage versioning across environments.

Task to Build Signed APKs with Detailed Explanations

Now that we have product flavors set up with branch name integration, we can create a custom Gradle task to build signed release APKs for all flavors. Below is the code for the task, along with explanations for each part.


task buildSignedReleaseApks {
    group = "build"
    description = "Builds signed release APKs for all flavors"

    // Iterate over all application variants
    android.applicationVariants.all { variant ->
        // Check if the variant is a release build
        if (variant.buildType.name == "release") {
            def flavorName = variant.flavorName.capitalize()
            // Find the build task for the current flavor
            def buildTask = tasks.findByName("assemble${flavorName}Release")
            // Add the build task as a dependency
            if (buildTask) {
                dependsOn buildTask
            }
        }
    }

    doLast {
        // After the build, iterate over all application variants again
        android.applicationVariants.all { variant ->
            if (variant.buildType.name == "release") {
                def flavorName = variant.flavorName.capitalize()
                // Define the path to the APK file that was just built
                def apkFile = file("${buildDir}/outputs/apk/${variant.dirName}/${variant.name}.apk")
                // Define the new path for the APK file, renaming it with the version name
                def newApkFile = file("${buildDir}/outputs/apk/release/${flavorName}-${variant.versionName}.apk")
                // Check if the APK file exists
                if (apkFile.exists()) {
                    // Copy the APK file to the new location and rename it
                    copy {
                        from apkFile
                        into newApkFile.parent
                        rename { newApkFile.name }
                    }
                }
            }
        }
    }
}

Explanation of Key Parts:

  • android.applicationVariants.all: This method iterates over all the build variants (i.e., combinations of build types and product flavors) defined in the project.
  • variant.buildType.name == “release”: This checks if the current variant is a release build.
  • def flavorName = variant.flavorName.capitalize(): The flavorName variable is set to the capitalized version of the flavor’s name (e.g., “Production”, “Development”).
  • tasks.findByName: Finds the Gradle task responsible for assembling the APK for the current flavor. The task name is dynamically generated by combining “assemble” with the flavor name and “Release”.
  • dependsOn buildTask: Adds the buildTask as a dependency to ensure that the APK is built before this task proceeds.
  • apkFile: Specifies the path to the APK file that was just generated.
  • newApkFile: Specifies the new location and name for the APK file, appending the flavor and version name to the filename.
  • copy: Copies the APK file to the new location and renames it according to the defined rules.

Advantages of Using Product Flavors with Branch Name Integration

  • Environment-specific Configurations: You can easily manage different configurations (e.g., URLs, API keys) for each environment.
  • Versioning with Branch Names: By integrating the branch name into the version name, you can clearly identify the source branch for each APK.
  • Automated Builds: The custom task simplifies the process of building signed APKs for all product flavors and renaming them based on the flavor and version.

Product flavors and branch name integration provide a robust solution for managing multiple environments and streamlining the release process in Android projects. By using the custom task to build signed APKs, developers can automate their workflow, improve version tracking, and reduce manual effort.

For more insights into Android development best practices, check out Building an Android App with Gemini AI.

Did you like this article?
You can subscribe to my newsletter below and get updates about my new articles.

Shares:
Leave a Reply

Your email address will not be published. Required fields are marked *