Posted by: Satya Pavan Kantamani
Introduction
Since the start of using Android Studio, Gradle has become a central place for managing many things like dependencies, config data, flavours, etc. And maintaining the dependencies across multi-module projects has become a challenge over time. Let’s see in detail how the evolution of dependencies management across multi-module projects happened and the problems faced. Please check out my previous post on Kotlin DSL for a better understanding
The core part of this post is how we can achieve better dependencies management using buildSrc and Kotlin DSL
Problem
If you want to skip the evolution process move to the usage of buidSrc
with Kotlin DSL section else continue reading.
Let’s take a multi-module app to understand the problem. Manually declaring versions along with dependencies across multi modules is our first approach. That would look something like below
At first module build.gradle file we declare dependencies
implementation "androidx.appcompat:appcompat:1.0.0" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.41" xxxxxx
In the second module build.gradle fiile we declare same dependencies
implementation "androidx.appcompat:appcompat:1.0.0" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.41" xxxxxxxx
As we are defining dependency versions at each build.gradle file this has created problems like conflicting versions if not upgraded correctly. It’s very hard to manage the version upgrades of dependencies and config data. Whenever there is a change in dependency if we have more modules there was a lot of manual process of going to each and every module and checking whether the dependency is exiting or not if found updating and then moving to next.
Configure project-wide properties
Then we moved to set project-wide properties using Gradle Extra properties. For this approach, we used to define the config data and dependency versions at the project or root-level build.gradle file
ext { compileSdkVersion = 28 versions = [ supportLibrary = "1.0.0", kotlinVersion = "1.3.41" ]deps = [ appcompatLib: "androidx.appcompat:appcompat:${versions.supportLibrary}", kotlinStdLib: "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlinVersion}" ] }
Across modules simply use
compileSdkVersion ext.compileSdkVersionimplementation deps.appcompatLib implementation deps.kotlinStdLib
This has solved the issue of upgrading versions of dependencies keeping them in one central place. However, there is no navigation support to check the versions or auto-suggestions. So let’s move to the core part of using buildSrc
with Kotlin DSL
What is buildSrc?
buildSrc
is a directory at the project root level which contains build info. We can use this directory to enable kotlin-dsl
and write logic related to custom configuration and share them across the project. It was one of the most used approaches in recent days because of its testability,
The directory buildSrc
is treated as an included build. Upon discovery of the directory, Gradle automatically compiles and tests this code and puts it in the classpath of your build script. For multi-project builds there can be only one buildSrc
directory, which has to sit in the root project directory. buildSrc
should be preferred over script plugins as it is easier to maintain, refactor and test the code — Gradle Docs
Let’s see how to create a buildSrc
directory
Right Click on Root Project → New → Directory → buildSrc
Enable Kotlin DSL in buildSrc
As Kotlin DSL is adopted from the parent Kotlin language most of its syntax would be similar. For this in buildSrc
first, create an empty file build.gradle.kts and then enable the option of ‘kotlin-dsl’ in buildSrc
import org.gradle.kotlin.dsl.`kotlin-dsl` | |
plugins { | |
`kotlin-dsl` | |
} | |
repositories { | |
mavenCentral() | |
} |
Click on sync now to finish the set-up. Once it’s done now we can create our Kotlin DSL scripts for custom build logic. Here let’s do configuration build data and dependency management set-up.
Step 1 :
Create a src
directory structure in buildSrc
directoryv
Job Offers
Step 2
In this directory, we can create our Kotlin files. Let’s create an object class called Versions.kt
where we define all the versions related to plugins, dependencies, etc
object Versions { | |
const val gradlePlugin = "4.2.1" | |
const val kotlin = "1.5.0" | |
const val timber = "4.7.1" | |
const val appCompat = "1.3.0" | |
const val material = "1.3.0" | |
const val constraintLayout = "1.1.3" | |
const val jUnit = "4.12" | |
} |
Step 3
Let’s create a new file Dependencies.kt
to define all our plugins, dependencies, etc
The file will be looking as below
/** | |
* To define plugins | |
*/ | |
object BuildPlugins { | |
val android by lazy { "com.android.tools.build:gradle:${Versions.gradlePlugin}" } | |
val kotlin by lazy { "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}" } | |
} | |
/** | |
* To define dependencies | |
*/ | |
object Deps { | |
val appCompat by lazy { "androidx.appcompat:appcompat:${Versions.appCompat}" } | |
val timber by lazy { "com.jakewharton.timber:timber:${Versions.timber}" } | |
val kotlin by lazy { "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.kotlin}" } | |
val materialDesign by lazy { "com.google.android.material:material:${Versions.material}" } | |
val constraintLayout by lazy { "androidx.constraintlayout:constraintlayout:${Versions.constraintLayout}" } | |
val junit by lazy { "junit:junit:${Versions.jUnit}" } | |
} |
While creating a file we can have auto-suggestions poping from Versions.kt
as shown below. So we can easily navigate between file to check the specific values
Step 4
We can also create a ConfigData.kt
for any build configuration related info
object ConfigData { const val compileSdkVersion = 30 const val buildToolsVersion = "30.0.3" const val minSdkVersion = 21 const val targetSdkVersion = 30 const val versionCode = 1 const val versionName = "1.0" }
Step 5
Finally using the data defined in the above classes in the module-level build.gradle.kts file
plugins { | |
id("com.android.application") | |
kotlin("android") | |
} | |
android { | |
compileSdkVersion(ConfigData.compileSdkVersion) | |
buildToolsVersion(ConfigData.buildToolsVersion) | |
defaultConfig { | |
applicationId = "com.sample.dsl" | |
minSdkVersion(ConfigData.minSdkVersion) | |
targetSdkVersion(ConfigData.targetSdkVersion) | |
versionCode = ConfigData.versionCode | |
versionName = ConfigData.versionName | |
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" | |
} | |
buildTypes { | |
getByName("release") { | |
isMinifyEnabled = false | |
proguardFiles( | |
getDefaultProguardFile("proguard-android-optimize.txt"), | |
"proguard-rules.pro" | |
) | |
} | |
} | |
compileOptions { | |
sourceCompatibility = JavaVersion.VERSION_1_8 | |
targetCompatibility = JavaVersion.VERSION_1_8 | |
} | |
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> { | |
kotlinOptions { | |
jvmTarget = "1.8" | |
} | |
} | |
} | |
dependencies { | |
implementation(Deps.kotlin) | |
implementation(Deps.appCompat) | |
implementation(Deps.materialDesign) | |
implementation(Deps.timber) | |
implementation(Deps.constraintLayout) | |
testImplementation(Deps.junit) | |
} |
The final directory structure would be something like below
buildSrc
├── src
│ └── main
│ ─└── kotlin
│ ──└── Dependencies.kt
├── build.gradle.kts
Please check out the GitHub repo android_sample_kotlin_dsl for the sample codebase.
Note: Though we can do this declaration of versions, dependencies, etc in a single class it would be better to have segregation of classes so that the code will be clean
Summary
buildSrc + Kotlin DSL
is the best option for dependency management. As it’s a class-level declaration it can be easily tested. The auto-suggestion support and code navigation would help in saving time. Maintain separate classes for each purpose. This approach could be easy for better reusability and easy maintenance.
Don’t forget to clap if you found this article helpful. Thank you for reading…
References
- Gradle Docs
- GitHub repo android_sample_kotlin_dsl
Continue Reading Android Stuff
Understand How View Renders in Android
The Life Cycle of a View in Android