DEV Community 👩‍💻👨‍💻

Cover image for Android Build.Gradle Explained 🌟
Hakkı Cengiz
Hakkı Cengiz

Posted on

Android Build.Gradle Explained 🌟

Hello friends,

Gradle is a build system that automates the development stages of an Android application.

While writing code for the java platform in the Gradle system, we use the Groovy programming language, which is a very simple language. Groovy has a much simpler language than Java. You can take a look at the github repo where I explained these differences.

What Is This Groovy?

Groovy is a powerful, type-on-demand, dynamic language with static typing and static compilation for the Java platform that aims to increase developer productivity through a concise, simple, and easy-to-learn syntax.

With this information we learned in Groovy, let's take a closer look at how we can build our build.gradle in Android Studio in a professional way.

Build.gradle allows us to make changes in the development processes of an application we developed through Android Studio, to customize the build we receive (for the simplest example, to produce different .apk for pro and free versions), with multiple environments (debug, ent, test, beta, prod.. ) and allows us to work comfortably with the team.

Build Gradle (App) Tools

Its general structure is as follows:

`plugins {
id 'com.android.application'

}
import java.util.regex.Pattern
android {

compileSdk 32
defaultConfig {
applicationId "com.hakki.uygulamaadi"
minSdk 21
targetSdk 32
resConfigs "en", "tr"
...
}
}
buildTypes {}
compileOptions {}
dependencies {
implementation 'androidx.appcompat:appcompat:1.4.1'
...
}`


Now let's see what we have in our build.gradle settings.

signingConfigs: The desired keys are stored when publishing the application. Release is added to the version.

buildTypes: The part where we add and edit application development processes.

buildConfigField: Allows sending a constant value while getting the build.
When Build is received, the BuildConfig.java class is created. To access this constant from a class, it is accessed as BuildConfig.INFO. Note: Since you haven't received a build yet, it will appear red as if there is an error.

productFlavors: Used when you want to collect multiple projects under one roof. It comes as "main" by default and is not included in the build.gradle. FlavorDimensions are defined beforehand.

manifestPlaceholders: Changes the application icon.
resValue: Allows to change a resource value according to the application.
For example: The application name is mentioned in several places (it can be a common splash or activity), we can add dynamism by assigning the value here.

variantFilter: The more buildTypes we add, the more selectable development environments we add for each application. eg. we add processes such as test, debug, beta, ent, release to our todo application. And for every application, there are cases where processes don't work that way. We're removing it using variantFilter so it doesn't unnecessarily populate the development environment list.

Applications and Development Processes

If I need to briefly explain the other crucial points:
Gathering all applications in a project provides convenience in terms of change and management. However, it also brings some minor improvements that need to be added.
These;

  • When we get Build, preventing all classes in the projects from being loaded in the .apk of each application,
  • Defining a common folder and uploading common classes and resource files in each .apk,
  • Performing only Release (live) version specific operations,

You can take a look at the sample build.gradle codes below, along with the solutions I developed for these problems.

one android project to rule them (apps) all.

plugins {
    id 'com.android.application'
}

import java.util.regex.Matcher
import java.util.regex.Pattern

/*
sonarqube {
    properties {
        property "sonar.projectName", "LargeProjectExample"
        property "sonar.projectKey", "LargeProjectExample"
        property "sonar.host.url", "http://localhost:9000"
        property "sonar.language", "java"
        property "sonar.login", "admin"
        property "sonar.password", "******"
    }
}
*/

android {
    useLibrary 'org.apache.http.legacy'
    compileSdk 32

    defaultConfig {
        applicationId "com.largeproject.example"
        minSdk 21
        targetSdk 32
        versionCode 1
        versionName "1.0"
        resConfigs "en", "tr"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"    
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    signingConfigs {
        release {
            keyAlias 'key0'
            keyPassword '******'
            storeFile file('27tkey.jks')
            storePassword '******'
        }
    }

    buildTypes {
        // Development Environments
        release {
            signingConfig signingConfigs.release
            // zipAlignEnabled true      //
            //shrinkResources true   //res
            minifyEnabled true    //java code
            // multiDexEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            debuggable false
        }
        debug {
            //applicationIdSuffix ".debug"
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            debuggable true
        }
        dev {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            debuggable true
            /* DEV ORTAMI */
            buildConfigField "String", "INFO", '"someinfodev"'
        }
        plat {
            /* PLATFORM ORTAMI */
            buildConfigField "String", "INFO", '"someinfoplat"'

        }
        prod {
            /* PROD ORTAMI */
            buildConfigField "String", "INFO", '"someinfoprod"'

        }
    }

    flavorDimensions "APP"
    productFlavors {

        largeProject {
            dimension "APP"

            // CHANGE
            String appName = "Large Project"
            String appVersionName = "4.1.55"
            Integer appVersionCode = 255
            String appIdSuffix = ".largeproject"
            // CHANGE

            manifestPlaceholders = [appName: appName, appIcon: "@drawable/ic_second"]
            resValue("string", "app_name", appName)
            versionName appVersionName
            versionCode appVersionCode
            applicationIdSuffix appIdSuffix
        }

        largeProject2 {
            dimension "APP"

            String appName = "Another Large Project"
            String appVersionName = "4.1.55"
            Integer appVersionCode = 255
            String appIdSuffix = ".largeProject2"

            manifestPlaceholders = [appName: appName, appIcon: "@drawable/ic_second"]
            resValue("string", "app_name", appName)
            versionName appVersionName
            versionCode appVersionCode
            applicationIdSuffix appIdSuffix

            buildConfigField 'String', 'merhabaa', '"selam"'
        }

    }

    // Ignore Operations. Note: "==" operator is better than ".contains".
    variantFilter { variant ->
        String app = variant.flavors*.name.get(0).toLowerCase()
        String buildType = variant.buildType.name.toLowerCase()
        // println("app : " + app + " buildType : " + buildType)

        if (app == "largeproject" && buildType == "ent") {
            variant.setIgnore(true)
        }
    }

    // Changing the Default Media Storage Location Groovy
    List<String> resBuildTypes = new ArrayList<String>()
    List<String> resBuildType  = new ArrayList<String>()
    resBuildTypes.add("src/main/res/common")

    productFlavors.all { flavor  ->
        String folder = "src/main/res/customers/" + flavor.name
        resBuildTypes.add(folder)
        resBuildType.add(flavor.name)
    }

    //println resBuildTypes // output: [src/main/res/common, src/main/res/customers/largeProject, src/main/res/customers/largeProject2]
    //println resBuildType  //output: [largeProject, largeProject2]

    sourceSets {
        main.res.srcDirs  = resBuildTypes
        // also you can change manifest file for every customer
        //main.manifest.srcFile = "src/main/res/"

        for (String var : resBuildType) {
            "$var" {
                setRoot "src/main/res/" + var
            }
        }
    }

    buildFeatures {
        buildFeatures.dataBinding = true
        buildFeatures.viewBinding = true
    }
}


// Build task that allows us to get the received project request information.
def projectName
def projectBuildType
task getCurrentFlavor() {
    Gradle gradle = getGradle()
    String tskReqStr = gradle.getStartParameter().getTaskRequests().toString()

    Pattern pattern

    // take buildTypes to String
    String buildTypes = ""
    android.buildTypes.all {
        type ->
            //println(type.name) Capitalize
            String output = type.name.substring(0, 1).toUpperCase() + type.name.substring(1)
            buildTypes += output + "|"
    }
    buildTypes = buildTypes.substring(0, buildTypes.length() - 1) //delete last | character
    //println buildTypes

    if (tskReqStr.contains("assemble"))
        pattern = Pattern.compile("assemble(\\w+)($buildTypes)") // Addition Required for Every BuildType Created. Dev|Ent|Tst,
    else
        pattern = Pattern.compile("generate(\\w+)($buildTypes)") // $buildTypes fulfills this request by automating.

    Matcher matcher = pattern.matcher(tskReqStr)

    if (matcher.find()) {
        //projectVariant = matcher.group().toLowerCase()
        projectName = matcher.group(1).toLowerCase() // Changing to 2 will return build type, 1 provides product flavor
        projectBuildType = matcher.group(2).toLowerCase() // Changing to 2 will return build type, 1 provides product flavor

    } else {
        println "NO MATCH FOUND"
    }
}

task buildVariantTasks(type: Copy) {
    dependsOn getCurrentFlavor
    println "flavor name is " + projectName
    println "build type is "  + projectBuildType

    if(projectName!=null && projectBuildType!=null) {

        // Separating Classes and Resourses
        // To identify location of Common (Main) Classes, simply change the part that says "common".

        String appID = android.defaultConfig.applicationId
        String appIdSlashes = "src/main/java/" + appID.replaceAll("\\.", "/") + "/"
        android.sourceSets.main {
            java.srcDirs = [appIdSlashes + "common", appIdSlashes + projectName]
            res.srcDirs  = ['src/main/res/common', 'src/main/res/'+ projectName]
            manifest.srcFile "src/main/res/customers/" + projectName + "/AndroidManifest.xml"
        }

        android.applicationVariants.all { variant ->

            // Output APK Name & appVer - Specify a Common BuildConfig to Add to Each Product
            String buildType = variant.buildType.name
            String flavorName = variant.getFlavorName()
            String buildTime = new Date().format("yyMMddHHmm", TimeZone.getTimeZone("Asia/Istanbul"))

            variant.outputs.all {
                buildConfigField 'String', 'INFO', "\"${flavorName}.${buildType}\""
                buildConfigField 'String', 'appVer', "\"${versionName}_${buildTime}\""
                outputFileName = getFileName(flavorName, versionName, buildTime, buildType)
            }

            // Action By Product Specific Defined BuildConfigField, If Product Is Defined ...
            /*variant.productFlavors.each { flavor ->
                if (variant.getFlavorName() == projectName) {

                    flavor.buildConfigFields.each { key, value ->
                        if(key == "INFO") {
                            println value.type
                            println value.name
                            println value.value
                        }
                    }
                }
            }*/

            // Release Actions for Release -> Allatori is here.
            if(projectBuildType.contains("release") && flavorName == projectName) {

                // You can add here compression tools...


            }

        }


    }
}
preBuild.dependsOn buildVariantTasks


static String getFileName(flavorName, versionName, buildTime, buildType) {
    return flavorName + "_" + versionName + "_" + buildTime + "_" + buildType + ".apk"
}

// Example of defining version for dependencies
ext {
    appCombat = '1.4.1'
    material = '1.5.0'
    constrain = '2.1.3'
    junit = '4.13.2'
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')

    implementation 'androidx.appcompat:appcompat:' + appCombat
    implementation 'com.google.android.material:material:'+ material
    implementation 'androidx.constraintlayout:constraintlayout:'+constrain
    implementation 'androidx.appcompat:appcompat:1.4.1'
    implementation 'com.google.android.material:material:1.5.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
    testImplementation 'junit:junit:'+junit
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
Enter fullscreen mode Exit fullscreen mode

In my next article, we will examine what improvements we can make in development processes with Kotlin.

Source:
Gradle
What it this Groovy?
Github Build.Gradle

Top comments (0)

Join us at DEV Want to join the conversation?
 

It's easy! Become a DEV member to follow this post, comment, and more.