Blogs Different ways of Managing Gradle Dependencies
Post
Cancel

Different ways of Managing Gradle Dependencies

Preview Image

Multi-module Android projects are now the recommended way to take advantage of performance improvements with Android Gradle Plugin 3+. However, as we add more modules to our project, we quickly run into the issue of dependency management.

Let’s Dig Deeper to the Different ways of managing Gradle dependencies:

Manual Management

This is the way most of us have been managing dependencies, but it requires a lot of manual changes whenever you upgrade a library to ensure that versions are updated correctly.

Let’s say we have two modules, module1 and module2. So, we have to duplicate the configuration in both modules like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ==> module1/build.gradle
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation "com.android.support:support-annotations:27.1.1"
implementation "com.android.support:appcompat-v7:27.1.1"
implementation "com.squareup.retrofit2:retrofit:2.3.0"
implementation "com.squareup.retrofit2:adapter-rxjava2:2.3.0"
implementation "io.reactivex.rxjava2:rxjava:2.1.10"

// ==> module2/build.gradle
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation "com.android.support:support-annotations:27.1.1"
implementation "com.android.support:appcompat-v7:27.1.1"
implementation "com.squareup.retrofit2:retrofit:2.3.0"
implementation "com.squareup.retrofit2:adapter-rxjava2:2.3.0"
implementation "io.reactivex.rxjava2:rxjava:2.1.10"

This is a lot of duplicated configuration that is hard to manage upgrades with, especially when you have a lot of modules like the above example.

Using Gradle Extra Properties “ Google’s Recommendation ”

This is Google’s recommended way of doing this as seen in the Android documentation. It is also used in lots of Android projects. This method is great for upgrading libraries like the support library. Every support library dependency has the same version number, so only having to change this in one place.

Still, we are working on the same example of having two modules which are module1 and module2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ==> Root-level build.gradle
ext {
    versions = [
    support_lib: "27.0.2",
    retrofit: "2.3.0",
    rxjava: "2.1.9"
    ]
    libs = [
    support_annotations: "com.android.support:support-annotations:${versions.support_lib}",
    support_appcompat_v7: "com.android.support:appcompat-v7:${versions.support_lib}",
    retrofit :"com.squareup.retrofit2:retrofit:${versions.retrofit}",
    retrofit_rxjava_adapter: "com.squareup.retrofit2:adapter-rxjava2:${versions.retrofit}",
    rxjava: "io.reactivex.rxjava2:rxjava:${versions.rxjava}"
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// ==> module1/build.gradle
implementation libs.support_annotations
implementation libs.support_appcompat_v7
implementation libs.retrofit
implementation libs.retrofit_rxjava_adapter
implementation libs.rxjava

// ==> module2/build.gradle
implementation libs.support_annotations
implementation libs.support_appcompat_v7
implementation libs.retrofit
implementation libs.retrofit_rxjava_adapter
implementation libs.rxjava

This is a huge step ahead of manual management, but IDE support is lacking.

Using buildSrc for custom logic in Gradle Dependencies

Recently, Gradle announced Kotlin language support for writing build scripts. You may be wondering, why would you use Kotlin for writing Gradle scripts?

First of all, Kotlin is a statically typed language (Groovy is dynamically typed), which allows for conveniences like autocompletion, better refactoring tools, and source-code navigation. You can work in script files just you would with Kotlin classes, with all support of the Android Studio you’re used to. Moreover, autocompletion will prevent you from making typos. Secondly, it’s practical to work with a single language across your app and your building system ;).

If somebody asked about the Gradle feature that everybody should know about, I would probably point them towards buildSrc. It is a magic Gradle Java/Groovy project inside your repository, available as a library to all of your build.gradle files. You can create a buildSrc module with Kotlin code to manage dependencies and get IDE completion support.

When you run Gradle, it checks for the existence of a directory called buildSrc. Gradle then automatically compiles, tests this code, and puts it in the classpath of your build script. You don’t need to provide any further instruction.

Getting started is easy :

  • Let us create a folder named buildSrc in the root folder of your project (the same folder that contains your settings.gradle):
1
$ mkdir buildSrc
  • After that, we must rebuild/sync the project. Then, inside the buildSrc folder, you have to create a file called build.gradle.kts and writes inside it the following block of code:
1
2
3
4
==> buildSrc/build.gradle.kts**
plugins {
    `kotlin-dsl`
}
  • Thus, we must rebuild/sync the project again until the folder hierarchy be like the below image:

  • The project will automatically have both the java and groovy plugins applied to it. We can simply create a standard src/main/java folder and start writing code:
1
$ mkdir -p buildSrc/src/main/java
  • Now we can create Two Kotlin Classes one for Dependencies.kt and the other for Versions.kt of these Dependencies like the below image:

  • Versions.kt Here we writes all the versions that our dependencies are going to use in all our modules which are module1 and module2.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package dependencies

object Versions {

    val versionName = "1.0"
    val versionCode = 1

    object SupportAndroidLibs {
        const val gradlePlugin = "3.0.1"
        const val servicesPlugin = "3.2.0"
        const val compileSdk = 28
        const val minSdk = 21
        const val targetSdk = 28
        const val buildTools = "27.0.3"
        const val supportLibrary = "27.1.1"
        const val multiDex = "1.0.3"
        const val constraintLayout = "1.1.0"
        const val androidArcComponents = "1.1.1"
    }

    object Testing {
        const val mockito = "2.10.0"
        const val espresso = "3.0.1"
        const val runner = "1.0.1"
        const val junit = "4.12"
        const val junitPlatform = "1.0.0"
        const val spek = "1.1.5"
    }

    object Kotlin {
        const val std = "1.2.40"
    }

    object Google {
        const val playServices = "12.0.1"
        const val firebase = "12.0.1"
        const val dagger = "2.14.1"
    }

    object Libraries {
        // RxJava and RxAndroid
        const val rxAndroid = "2.0.1"
        const val rxJava = "2.1.9"
        const val rxRelay = "2.0.0"
        const val rxIdler = "0.9.0"

        // OkHttp and Retrofit
        const val retrofit = "2.3.0"
        const val okHttp = "3.9.1"

        // Room for Database
        const val room = "1.1.1"

        // Paging Library
        const val paging = "1.0.0"

        // Gson
        const val gson = "2.8.2"

        // Timber
        const val timber = "1.5.1"

        // ButterKnife
        const val butterknife = "8.8.1"

        //LeakCanary
        const val leakCanary = "1.5.4"

        // Glide
        const val glide = "4.7.1"

    }
}
  • Dependencies.kt: Here we writes all the dependencies that we are going to use in all our modules which are module1 and module2 by using the arrayOf mechanism. Since, we grouped the dependencies that are similar to each other and make them in one array/group to make it easier when we are going to use them in the build.gradle module file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
package dependencies

object Dependencies {

    // All The Support Android Libraries are grouped in this supportAndroidLibs array
    val supportAndroidLibs = arrayOf(
        "com.android.support:support-annotations:${Versions.SupportAndroidLibs.supportLibrary}",
        "com.android.support:appcompat-v7:${Versions.SupportAndroidLibs.supportLibrary}",
        "com.android.support:cardview-v7:${Versions.SupportAndroidLibs.supportLibrary}",
        "com.android.support:recyclerview-v7:${Versions.SupportAndroidLibs.supportLibrary}",
        "com.android.support:design:${Versions.SupportAndroidLibs.supportLibrary}",
        "com.android.support:customtabs:${Versions.SupportAndroidLibs.supportLibrary}",
        "com.android.support:multidex:${Versions.SupportAndroidLibs.multiDex}",
        "com.android.support.constraint:constraint-layout:${Versions.SupportAndroidLibs.constraintLayout}"
    )

    // The same here for the Android Architecture Components Libraries
    val androidArchComponents = arrayOf(
        "android.arch.lifecycle:runtime:${Versions.SupportAndroidLibs.androidArcComponents}",
        "android.arch.lifecycle:extensions:${Versions.SupportAndroidLibs.androidArcComponents}",
        "android.arch.lifecycle:reactivestreams:${Versions.SupportAndroidLibs.androidArcComponents}"
    )

    // The same here for Kotlin Libraries
    val kotlin = arrayOf(
        "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.Kotlin.std}"
    )

    // The same here for Google Libraries
    val google = arrayOf(
        "com.google.android.gms:play-services-analytics:${Versions.Google.playServices}",
        "com.google.firebase:firebase-core:${Versions.Google.firebase}",
        "com.google.dagger:dagger:${Versions.Google.dagger}",
        "com.google.dagger:dagger-android:${Versions.Google.dagger}",
        "com.google.dagger:dagger-android-support:${Versions.Google.dagger}"
    )

    // And here, all the libraries that we are going to use in our two modules which are `module1` and `module2`
    val libraries = arrayOf(
        // RxJava, RxAndroid and RxRelay
        "io.reactivex.rxjava2:rxandroid:${Versions.Libraries.rxAndroid}",
        "io.reactivex.rxjava2:rxjava:${Versions.Libraries.rxJava}",
        "com.jakewharton.rxrelay2:rxrelay:${Versions.Libraries.rxRelay}",

        // OkHttp and Retrofit
        "com.squareup.okhttp3:okhttp:${Versions.Libraries.okHttp}",
        "com.squareup.okhttp3:logging-interceptor:${Versions.Libraries.okHttp}",
        "com.squareup.okhttp3:okhttp-urlconnection:${Versions.Libraries.okHttp}",
        "com.squareup.retrofit2:retrofit:${Versions.Libraries.retrofit}",
        "com.squareup.retrofit2:converter-gson:${Versions.Libraries.retrofit}",
        "com.squareup.retrofit2:adapter-rxjava2:${Versions.Libraries.retrofit}",
        "com.squareup.retrofit2:retrofit-mock:${Versions.Libraries.retrofit}",

        // Room
        "android.arch.persistence.room:runtime:${Versions.Libraries.room}",
        "android.arch.persistence.room:rxjava2:${Versions.Libraries.room}",

        // Paging
        "android.arch.paging:runtime:${Versions.Libraries.paging}",

        // Gson
        "com.squareup.retrofit2:converter-gson:${Versions.Libraries.retrofit}",

        // TimberKotlin
        "com.github.ajalt:timberkt:${Versions.Libraries.timber}",

        // LeakCanary
        "com.squareup.leakcanary:leakcanary-android:${Versions.Libraries.leakCanary}",

        // Glide
        "com.github.bumptech.glide:glide:${Versions.Libraries.glide}"
    )

    val annotations = arrayOf(

        // Android Architecture Components
        "android.arch.lifecycle:compiler:${Versions.SupportAndroidLibs.androidArcComponents}",

        // Room
        "android.arch.persistence.room:compiler:${Versions.Libraries.room}",

        // ButterKnife
        "com.jakewharton:butterknife-compiler:${Versions.Libraries.butterknife}",

        // Dagger
        "com.google.dagger:dagger-compiler:${Versions.Google.dagger}",
        "com.google.dagger:dagger-android-processor:${Versions.Google.dagger}",

        // Glide
        "com.github.bumptech.glide:compiler:${Versions.Libraries.glide}"
    )

}
  • This grouping mechanism shouldn’t be the same in all the projects. It’s according to each project. So, you shouldn’t obey the same structure in your application. Just group the libraries as you want and according to the methodology you want.

  • After we have done with syncing Gradle, we can now access any of the values in module1 or module2. The result looks very similar to what “ext” looked like, but we have autocomplete and click support (to take you to the definition).

1
2
3
4
5
6
7
8
9
10
11
// ==> module1/build.gradle
implementation Dependencies.supportAndroidLibs
implementation Dependencies.androidArchComponents
implementation Dependencies.google
implementation Dependencies.libraries

// ==> module2/build.gradle**
implementation Dependencies.supportAndroidLibs
implementation Dependencies.androidArchComponents
implementation Dependencies.google
implementation Dependencies.libraries

I highly recommend the Using buildSrc for custom logic in Gradle Dependencies option. It may not seem like it’s that big of a deal, but managing Gradle dependencies is a pain, and having autocomplete and click support are something wonderful. No more switching back and forth between files manually!

This post is licensed under CC BY 4.0 by the author.