Auto-Increment Build Numbers in Android: A Complete Guide with Custom APK Naming

Learn how to automatically increment version codes, customize output filenames, and access build information in your Android app using Gradle KTS and BuildConfig

The Problem

Manual version management in Android development is tedious and error-prone. Every time you build an APK or AAB, you need to:

  • Remember to increment versionCode
  • Manually rename output files with version numbers
  • Keep track of build numbers across debug and release builds
  • Update version info displayed in your app's settings

This tutorial shows you how to automate all of this using Gradle KTS (Kotlin DSL).

What You'll Build

By the end of this guide, you'll have:

  1. Auto-incrementing version codes that update on every build
  2. Custom APK/AAB names like MyApp-v1.4-release-185.apk
  3. BuildConfig fields to display version info in your app
  4. Persistent version tracking using a version.properties file

Step 1: Create the Version Properties File

Create a file named version.properties in your project's root directory (same level as your build.gradle.kts):

# Auto-managed version file
# This file is automatically updated when assembling builds
VERSION_CODE=7

Tip: Add this file to Git so version numbers stay consistent across your team.


Step 2: Set Up Gradle Helper Functions

Add these functions to your app/build.gradle.kts file (outside the android {} block):

// Helper function to get Git commit hash
fun getGitHash(): String {
    return try {
        Runtime.getRuntime()
            .exec("git rev-parse --short HEAD")
            .inputStream
            .bufferedReader()
            .readText()
            .trim()
    } catch (e: Exception) {
        "unknown"
    }
}

// Get the version properties file
fun getVersionPropertiesFile(): File {
    return rootProject.file("version.properties")
}

// Read current version code
fun readVersionCode(): Int {
    val versionFile = getVersionPropertiesFile()
    if (!versionFile.exists()) {
        versionFile.writeText("VERSION_CODE=7\n")
        return 7
    }
    val props = Properties()
    versionFile.inputStream().use { stream -> props.load(stream) }
    return props.getProperty("VERSION_CODE", "7").toInt()
}

// Increment and save version code
fun incrementAndSaveVersionCode(): Int {
    val versionFile = getVersionPropertiesFile()
    val currentVersion = readVersionCode()
    val newVersion = currentVersion + 1

    versionFile.writeText("# Auto-managed version file\n")
    versionFile.appendText("# This file is automatically updated when assembling builds\n")
    versionFile.appendText("VERSION_CODE=$newVersion\n")

    println("✓ Version code incremented: $currentVersion → $newVersion")
    return newVersion
}

Don't forget to import at the top:

import java.util.Properties

Step 3: Configure Auto-Increment on Release Builds

Add this code before the android {} block. This logic ensures the version code only increments when you create a release build.

// Auto-increment version code on RELEASE assemble/bundle tasks only
val taskRequests = gradle.startParameter.taskNames
val isAssembleOrBundleReleaseBuild = taskRequests.any { taskName ->
    taskName.contains("assembleRelease", ignoreCase = true) ||
    taskName.contains("bundleRelease", ignoreCase = true)
}

if (isAssembleOrBundleReleaseBuild) {
    // Increment version code for release builds only
    incrementAndSaveVersionCode()
    println("📦 Building RELEASE with incremented version code: ${readVersionCode()}")
} else {
    println("📦 Building with version code: ${readVersionCode()} (no increment for debug builds)")
}

This detects when you run ./gradlew assembleRelease or bundleRelease and automatically increments the version code before the build starts. Debug builds will use the existing version code without changing it.


Step 4: Configure BuildConfig Fields

Inside your android { defaultConfig {} } block, add these BuildConfig fields:

android {
    defaultConfig {
        applicationId = "com.yourapp.package"
        minSdk = 26
        targetSdk = 34
        versionCode = readVersionCode() // ← Auto-managed
        versionName = "1.4"

        // Expose version info to your app code
        buildConfigField("String", "VERSION_NAME", "\"${versionName}\"")
        buildConfigField("int", "VERSION_CODE", "${versionCode}")
        buildConfigField("String", "BUILD_TYPE", "\"${name}\"")
        buildConfigField("long", "BUILD_TIME", "${System.currentTimeMillis()}L")
        buildConfigField("String", "GIT_HASH", "\"${getGitHash()}\"")
    }
}

Important: Enable BuildConfig in your buildFeatures:

buildFeatures {
    buildConfig = true
}

Step 5: Customize APK and AAB Output Names

Add this code after the android {} block to customize output filenames:

androidComponents {
    onVariants { variant ->
        variant.outputs.forEach { output ->
            val versionName = android.defaultConfig.versionName ?: "unknown"
            val versionCode = android.defaultConfig.versionCode ?: 0
            val buildType = variant.buildType ?: "unknown"

            // Custom APK name: MyApp-v1.4-debug-185.apk
            val outputFileName = "MyApp-v${versionName}-${buildType}-${versionCode}"
            (output as com.android.build.api.variant.impl.VariantOutputImpl)
                .outputFileName.set("${outputFileName}.apk")
        }
    }

    // For AAB (Bundle) files
    onVariants { variant ->
        val versionName = android.defaultConfig.versionName ?: "unknown"
        val versionCode = android.defaultConfig.versionCode ?: 0
        val buildType = variant.buildType ?: "unknown"

        tasks.register("rename${variant.name.replaceFirstChar { it.uppercase() }}Bundle") {
            val bundleTask = tasks.named("bundle${variant.name.replaceFirstChar { it.uppercase() }}")
            dependsOn(bundleTask)

            doLast {
                val buildDir = layout.buildDirectory.get().asFile
                val bundleDir = File(buildDir, "outputs/bundle/${variant.name}")

                bundleDir.listFiles { file -> file.extension == "aab" }?.forEach { aabFile ->
                    val newName = "MyApp-v${versionName}-${buildType}-${versionCode}.aab"
                    val newFile = File(aabFile.parentFile, newName)
                    if (aabFile.exists() && !newFile.exists()) {
                        aabFile.renameTo(newFile)
                        println("✓ Renamed bundle to: ${newFile.name}")
                    }
                }
            }
        }
    }
}
---

## Step 6: Create a Build Script (`make.sh`)

To simplify the build process, you can create a shell script named `make.sh` in your project's root directory. This script will provide easy commands for building, cleaning, and installing your app.

Create the `make.sh` file and make it executable:
```bash
touch make.sh
chmod +x make.sh

Add the following content to make.sh:

#!/bin/bash

set -e

function usage() {
    cat <<EOL
Usage: $0 [option]

Options:
  debug      - Assemble the debug build and copy it to the builds/ directory.
  release    - Assemble the release build and copy it to the builds/ directory.
  both       - Assemble both debug and release builds and copy them to the builds/ directory.
  show       - List all .apk and .aab files in the build output directories.
  copy       - Copy all .apk and .aab files to the builds/ directory without building.
  clean      - Run 'gradle clean'.
  install    - Install the debug APK on a connected device.
  uninstall  - Uninstall the app from a connected device.
  help       - Show this usage information.

If no arguments are given, this help information is displayed.
EOL
}

function assemble() {
    mkdir -p builds
    if [[ "$1" == "debug" ]]; then
      echo "Assembling Debug build..."
      ./gradlew assembleDebug
      echo "Finding and copying Debug APK..."
      DEBUG_APK_FILE=$(find app/build/outputs/apk/debug -name "*.apk" | head -n 1)
      if [ -n "$DEBUG_APK_FILE" ]; then
          cp "$DEBUG_APK_FILE" "builds/"
          echo "Debug APK copied to builds/ directory."
      else
          echo "Debug APK not found."
      fi
    elif [[ "$1" == "release" ]]; then
      echo "Assembling Release build..."
      ./gradlew assembleRelease
      echo "Finding and copying Release AAB..."
      RELEASE_AAB_FILE=$(find app/build/outputs/bundle/release -name "*.aab" | head -n 1)
       if [ -n "$RELEASE_AAB_FILE" ]; then
          cp "$RELEASE_AAB_FILE" "builds/"
          echo "Release AAB copied to builds/ directory."
      else
          echo "Release AAB not found."
      fi
    else
        echo "Building both debug and release variants..."
        ./gradlew assembleDebug assembleRelease
        echo "Copying artifacts to builds/ directory..."
        find app/build/outputs/apk/debug -name "*.apk" -exec cp {} builds/ \;
        find app/build/outputs/bundle/release -name "*.aab" -exec cp {} builds/ \;
        echo "Both versions built and copied. Output in builds/:"
        ls -lh builds/
    fi
}

function show_builds() {
    echo "Showing found APKs and AABs:"
    find app/build/outputs -name "*.apk" -o -name "*.aab"
}

function copy_builds() {
    echo "Copying all builds to builds/ folder..."
    mkdir -p builds
    find app/build/outputs/apk -name "*.apk" -exec cp {} builds/ \;
    find app/build/outputs/bundle -name "*.aab" -exec cp {} builds/ \;
    echo "Copied files:"
    ls -lh builds/
}

function clean_only() {
    echo "Running gradle clean..."
    ./gradlew clean
}

function install_app() {
    echo "Installing debug APK on connected device..."
    APK_FILE=$(find app/build/outputs/apk/debug -name "*.apk" | head -n 1)
    if [ -z "$APK_FILE" ]; then
        echo "No debug APK found. Please build the debug APK first with './make.sh debug'."
        exit 1
    fi
    echo "Found APK: $APK_FILE. Installing..."
    adb install -r "$APK_FILE"
    echo "Installation complete."
}

function uninstall_app() {
    echo "Uninstalling app from connected device..."
    # IMPORTANT: Replace with your app's actual package name
    adb uninstall com.yourdev.easycctv
}

if [ $# -eq 0 ]; then
    usage
    exit 0
fi

case "$1" in
    debug)
        assemble "debug"
        ;;
    release)
        assemble "release"
        ;;
    both)
        assemble "both"
        ;;
    show)
        show_builds
        ;;
    copy)
        copy_builds
        ;;
    clean)
        clean_only
        ;;
    install)
        install_app
        ;;
    uninstall)
        uninstall_app
        ;;
    help)
        usage
        ;;
    *)
        echo "Unknown argument: $1"
        usage
        exit 1
        ;;
esac

Important: Remember to replace com.yourdev.easycctv in the uninstall_app function with your app's actual package name.


Step 7: Use BuildConfig in Your App

}


---

## Step 6: Use BuildConfig in Your App

Now you can access version info anywhere in your Kotlin code:

```kotlin
import com.yourapp.package.BuildConfig

class SettingsScreen : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // Display version info
        val versionText = "Version ${BuildConfig.VERSION_NAME}\nBuild ${BuildConfig.VERSION_CODE}"
        val buildTime = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
            .format(Date(BuildConfig.BUILD_TIME))
        val gitHash = BuildConfig.GIT_HASH
        
        // Use in your UI
        textView.text = """
            Version: ${BuildConfig.VERSION_NAME}
            Build: ${BuildConfig.VERSION_CODE}
            Type: ${BuildConfig.BUILD_TYPE}
            Commit: ${BuildConfig.GIT_HASH}
            Built: $buildTime
        """.trimIndent()
    }
}

Example in Jetpack Compose:

@Composable
fun AboutScreen() {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Version ${BuildConfig.VERSION_NAME}",
            style = MaterialTheme.typography.titleMedium
        )
        Text(
            text = "Build #${BuildConfig.VERSION_CODE}",
            style = MaterialTheme.typography.bodyMedium,
            color = MaterialTheme.colorScheme.onSurfaceVariant
        )
        Text(
            text = "Commit: ${BuildConfig.GIT_HASH}",
            style = MaterialTheme.typography.bodySmall
        )
    }
}

How It Works

  1. First Release Build: The system reads VERSION_CODE=7 from version.properties.
  2. Build Detection: When you run ./make.sh release (which calls ./gradlew assembleRelease), Gradle detects it's a release build.
  3. Auto-Increment: The version code increments: 7 → 8.
  4. File Update: version.properties is updated with VERSION_CODE=8.
  5. Build Proceeds: Your AAB is built with version code 8.
  6. Custom Naming: The output file is renamed to EasyCCTV-v1.4-release-8.aab.
  7. Debug Build: If you run ./make.sh debug, the version code remains 8 and is not incremented. The output is EasyCCTV-v1.4-debug-8.apk.
  8. Next Release Build: The next time you run a release build, the version will become 9.

Testing Your Setup

  1. Clean build:

    ./make.sh clean
    
  2. Build release AAB:

    ./make.sh release
    
  3. Check the console output:

    ✓ Version code incremented: 185 → 186
    📦 Building RELEASE with incremented version code: 186
    
  4. Find your APK:

    app/build/outputs/apk/debug/MyApp-v1.4-debug-186.apk
    
  5. Check the version file:

    VERSION_CODE=186
    
  6. Build debug APK:

    ./make.sh debug
    
  7. Check the console output (no increment):

    📦 Building with version code: 186 (no increment for debug builds)
    
  8. Find your APK:

    builds/MyApp-v1.4-debug-186.apk
    builds/MyApp-v1.4-release-186.aab
    

Pro Tips

Don't Increment on Sync or Debug

The auto-increment only happens during actual release builds (assembleRelease/bundleRelease), not during Gradle sync or debug builds. This prevents unnecessary version bumps for development builds.

Version Control

Commit version.properties to Git so your team shares the same version numbers:

git add version.properties
git commit -m "Update version to 186"

Reset Version Code

To reset or manually set the version code, just edit version.properties:

VERSION_CODE=100

Build Variants

This works for all build variants:

  • assembleDebugMyApp-v1.4-debug-186.apk
  • assembleReleaseMyApp-v1.4-release-186.apk
  • bundleReleaseMyApp-v1.4-release-186.aab

Common Issues & Solutions

Issue: BuildConfig not found

Solution: Make sure you have buildConfig = true in buildFeatures:

buildFeatures {
    buildConfig = true
}

Issue: Version doesn't increment

Solution: Ensure you're running a release build command (./make.sh release or ./gradlew assembleRelease), not just a debug build or a Gradle sync.

Issue: Git hash shows "unknown"

Solution: Make sure you're in a Git repository and Git is installed on your system.


Conclusion

You now have a professional version management system that:

  • ✅ Automatically increments build numbers
  • ✅ Creates descriptively named APK/AAB files
  • ✅ Provides version info accessible in your app
  • ✅ Works seamlessly with CI/CD pipelines
  • ✅ Tracks versions across your development team

No more manual version management! 🎉


Related Topics

  • Android Gradle Plugin API
  • Semantic Versioning for Android
  • CI/CD with GitHub Actions for Android
  • Automated Release Management
  • Gradle Build Optimization

Last updated: December 2024 | Works with Android Gradle Plugin 8.0+

Need an Android Developer or a full-stack website developer?

I specialize in Kotlin, Jetpack Compose, and Material Design 3. For websites, I use modern web technologies to create responsive and user-friendly experiences. Check out my portfolio or get in touch to discuss your project.