September 20, 2019

Avoid repetitive dependency declarations with Gradle Kotlin DSL

Improve how you define dependencies throughout your Android project.

Avoid repetitive dependency declarations with Gradle Kotlin DSL

Improve how you define dependencies throughout your Android project.

Gradle recently introduced a Kotlin DSL to replace the ever-so-popular Groovy DSL which (almost) no one loves. The main benefit when working with build files in the new system, believe it or not, is code completion. Backed by a powerful language like Kotlin, it’s an absolute pleasure dealing with build files now. So, how can Kotlin DSL make our lives easier? Well, let’s start with where we were at before.

The Old Way

With the Groovy DSL, we’d typically have our dependencies littered around the app. If we’re only using a single module in our project, no big deal. Any update to a single dependency is applied to all of our code base.

androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

Cool, what if we have a multi-module app? Now we have ${modules.size()} the dependencies to have to keep track of! Should we just stick with a search and replace? No! … Regex? No! … Magic? .. 🤔.. No!

In Gradle, we’re able to create predefined scripts which we can reuse(yay!). This means that we’re able to define all our dependencies in a single location. Let’s say we reuse the gradle/ directory within our project and create a dependencies.gradle file within there. We can define dependency versions in one object, and libraries in another, with some nesting for organization.

def versions = [  
  runner          : '1.1.0',  
  espresso        : '2.0.0-beta2'  
  supportV4       : '28.0.0'
]

ext.libraries = [  
  deviceTests : [    
    espresso      : "androidx...:${versions.espresso}",      
    runner        : "androidx...:${versions.runner}"  
  ],  
  supportV4       : "com.android....:${versions.supportV4}"
]

Now, we just need to make this file “visible” within each module’s build.gradle. We can do that by using apply from!

apply from: ‘../gradle/dependencies.gradle’

This now makes a life a smidge easier in that we now have a single source of truth for all of our dependencies. There’s now only one file to update when it’s time to upgrade all-the-things.

...
apply plugin: 'kotlin-kapt'
apply from: ‘../gradle/dependencies.gradle’

android {
  ...
}

dependencies {
  androidTestImplementation libraries.deviceTests.runner
  androidTestImplementation libraries.deviceTests.espresso
  implementation libraries.supportV4
}

How can we make this better?

Code completion! It’s not something that’s too bad .. until we repeat the same process in every new module and make sure we didn't misspell anything which, if you’re anything like me, you will. Side note, shout out to the person who invented spell-check.

Second, we may forget to apply the dependencies file. Why cant Gradle just know it exists! That is .. unless you’re fancy and have a build.gradle template for some auto-copy-pasta … but still. Let’s try and avoid attempting to build and subsequent wall of red text.

Third, defining every dependency .. line by line by line by line by line. This ends up just being a copy-pasta job from other modules. Not terrible, but not great. Example: forgetting to define kapt for moshi-codegen and finding out the hard way that the code ain’t too hot. Let’s always strive for hot code 🔥.

Cue Gradle Kotlin DSL 🎉

Using the Kotlin DSL will help us achieve all these goals. We will get you to hot code 🔥 status. Read on.

Single Source of Truth

We can achieve a similar Groovy DSL setup with having a shared dependencies file by using the buildSrc/ directory in our project. There are plenty of posts about this topic, so I won’t go into the thick of it. The outcome of that process should be the same as The Old Way, a single source of truth for all dependencies.

The other main benefit is that any gradle.kts file defined in buildSrc/ is automatically recognized by all of our converted module build.gradle.kts files. This means that any new module will automatically pick-up what’s defined in that directory .. so no more having to use apply from for the dependencies.gradle.kts file! 🎉Win!

Code Completion

If the conversion went well, all of our Gradle files are now in Kotlin giving us the full power of the IDE. It will treat our build.gradle.kts as a regular Kotlin classes, giving us code completion out of the box! 🎉 Second Win!

Stop Repeating Yourself

Defining the same set of dependencies per each module is a common occurrence. Let’s say we want to introduce rxJava to our project. We probably also want to include rxAndroid and rxKotlin too.

implementation(Libraries.Threading.rxJava)
implementation(Libraries.Threading.rxAndroid)
implementation(Libraries.Threading.rxKotlin)

We know that these three dependencies will probably be defined together 99.995% of the time for each module. Why can’t we just group all these three together and only call on one function to add them? .. Well that is exactly what we will do using Kotlin method extensions.

Under the Kotlin DSL hood, the implementation function is an extension function on DependencyHandler. That method then calls on DependencyHandler.add(String, Any). That’s the same for testImplementation, androidTestImplementation, api , etc. We can use that same idea within our Dependencies.gradle.kts file.

So, let’s look at our current setup:

object Libraries {

  object Threading {

    object Versions {
      ...
    }

    const val rxJava = "io...:rxjava:${Versions.rxJava}"
    const val rxAndroid = "io...:rxjava:${Versions.rxAndroid}"
    const val rxKotlin = "io...:rxjava:${Versions.rxKotlin}"
  }

}

Within Threading, We can add an extension function on DependencyHandler to only add all Rx dependencies.

object Libraries {
  
  object Threading {

    object Versions {
      ...
    }

    const val rxJava = "io...:rxjava:${Versions.rxJava}"
    const val rxAndroid = "io...:rxjava:${Versions.rxAndroid}"
    const val rxKotlin = "io...:rxjava:${Versions.rxKotlin}"
    
    DependencyHandler.implementRx() {
      add("implementation", rxJava)
      add("implementation", rxAndroid)
      add("implementation", rxAndroid)
    }
  }
}

This then allows us to call implementRx() within the dependencies block in our build.gradle.kts.

import Libraries.Threading.implementRx

dependencies {
  implementRx()
}

We can even go a step further, and have an extension function which calls on other extension functions we’ve already defined.

object Threading {
  // ...
  DependencyHandler.implementRx() {
    // ...
  }
  DependencyHandler.implementSomethingElse() {
    // ...
  }
  DependencyHandler.implementThreading() {
    implementRx()
    implementSomethingElse()
  }
}

// - - - - - - - - - - - - - - -

import Libraries.Threading.implementThreading

dependenies {
  implementThreading()
}

Using this trick brought down a 28 line dependency block to 9 lines for a project I’m working on. <doge>Wow. Win Three. Such Efficient! 🎉</doge>

Upgrading Dependencies

The process of upgrading dependencies sounds manual and tedious, and thats because it usually is. Having to this means tracking down the latest version through a GitHub README, GitHub Releases, or even browsing through Maven. There is a nifty Gradle plugin which does all that legwork for you so you don’t have to! Go check out jmfayard/buildSrcVersions for more information!

The End

With these extension functions and the use of Kotlin, it’s less hassle and simpler to define dependencies. It also helps lessen the learning curve for new engineers learning how to setup their project. Kotlin’s great language features make implementing this straight forward! No need write a Groovy gradle plugin if we have the power of Kotlin powering our build scripts!

Thanks for reading! Happy coding!