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.
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'
}
In my next article, we will examine what improvements we can make in development processes with Kotlin.
Top comments (0)