When defining the dependencies for a Gradle project, such as an Android app, there are situations that call for duplicated information. A common case is that libraries or plugins can often come with multiple dependencies, which are usually released together with matching versions. Maintaining these dependencies requires the version to be duplicated and then kept up-to-date as new versions are released. Another case is that it is common to have a multi-module project, where we may want a dependency to be added to multiple modules. Similarly to the case with versions, the dependency definition would be duplicated for each module and then kept in-sync moving forward. It is clear that in both cases we may want to define the dependencies in one place and then access this location from wherever the dependency definition is needed.
The Groovy way
There is a mechanism for sharing your dependency configuration using Groovy, by adding some extra properties in our root
build.gradle
file. This is
Google’s recommended solution
and can be nicely seen in practice within the
SdkSearch app by Jake Wharton
, which works in a similar way to the following Gradle script snippets.
→ /build.gradle
buildscript {
ext.versions [
'kotlin': '1.3.10',
'dagger': '2.19'
]
ext.deps [
'kotlin': [
'stdlib': "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}",
'test': "org.jetbrains.kotlin:kotlin-test-common:${versions.kotlin}"
],
'dagger': [
'runtime': "com.google.dagger:dagger:${versions.dagger}",
'compiler': "com.google.dagger:dagger-compiler:${versions.dagger}",
'androidRuntime': "com.google.dagger:dagger-android:${versions.dagger}",
'androidCompiler': "com.google.dagger:dagger-android-processor:${versions.dagger}",
]
]
}
→ /app/build.gradle
dependencies {
implementation deps.kotlin.stdlib
implementation deps.dagger.runtime
implementation deps.dagger.androidRuntime
kapt deps.dagger.compiler
kapt deps.dagger.androidCompiler
testImplementation deps.kotlin.test
}
Sharing the configuration via the root
build.gradle
file achieves the goal of having a single location to maintain. Only needing to update dependency versions in a single file is a great time-saver as well 🚀. Unfortunately from practice,
when using the Groovy Gradle DSL auto-complete is often missing or incomplete in this situation, making it awkward to use. On top of this,
Gradle 5.0
is on the horizon which includes the
production-ready Kotlin Gradle DSL. When we convert the above solution to Kotlin it isn’t as nice to use in Kotlin as it is in Groovy. The map syntax can be achieved using
mapOf
, but reading the data to pass to
implementation
requires some casting to fit with the statically typed language. 😢
Kotlin to the rescue
As many developers have found with their main app source code, Kotlin can help with defining our dependency configurations as well, even if we have Groovy-based Gradle scripts.
When ran, Gradle will check for a
buildSrc
directory in the root of the project, placing any code that is found into the classpath of our build. This is really handy and allows us to put Kotlin code into
buildSrc
and
then reference it from our Groovy scripts. To get started we just need a little bit of structure.
→ /buildSrc/build.gradle.kts
plugins {
`kotlin-dsl`
}
The Kotlin DSL plugin needs to be specified for
buildSrc
.
→ /buildSrc/src/main/java/com/ourapp/Versions.kt
Our source files need to be placed within the standard sources directory structure. To use inner objects, for now we need to include the sources within a package, e.g. ‘com.ourapp’.
package com.ourapp
object Versions {
object Gradle {
const val KTLINT = "0.29.0"
}
const val DAGGER = "2.19"
const val KOTLIN = "1.3.10"
}
→ /buildSrc/src/main/java/com/ourapp/Libs.kt
package com.ourapp
object Libs {
object Gradle {
const val KTLINT = "com.github.shyiko:ktlint:${Versions.Gradle.KTLINT}"
}
object Dagger {
const val RUNTIME = "com.google.dagger:dagger:${Versions.DAGGER}"
const val COMPILER = "com.google.dagger:dagger-compiler:${Versions.DAGGER}"
const val ANDROID_RUNTIME = "com.google.dagger:dagger-android:${Versions.DAGGER}"
const val ANDROID_COMPILER = "com.google.dagger:dagger-android-processor:${Versions.DAGGER}"
}
object Kotlin {
const val STDLIB = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.KOTLIN}"
const val TEST = "org.jetbrains.kotlin:kotlin-test-common:${Versions.KOTLIN}"
}
}
→ /app/build.gradle