With Instant Apps, Google is trying heavily to push people to modularise their apps in Android, so that instant apps (hopefully) take off. Let’s take a look at how we can use Gradle to help us achieve this.
To begin with, we’re just gonna create a new Android project and add the retrofit dependency into our project. Create a new blank application and in the app build.gradle file, inside the dependencies closure, add the following:
Now sync, and you should now be able to use retrofit inside this module. Cool. Now imagine you have more modules, and you need the same library over multiple modules. Do we just copy/paste this everywhere we need it? Thankfully, Gradle’s DSL is Groovy (hopefully Kotlin soon), which we can make use of to clean this up.
Here’s how it’ll work: The project’s build.gradle file will contain extension closures which we’ll use to specify our targetSdk, compileSdk, buildTools, etc. In here we’ll also define where the libraries that we want to use in any of our modules are. Finally we’ll add functions that we’ll call in our separate module’s gradle files to actually add the library.
First in your project gradle file, scroll to the allProjects {} function. In here we have a repositories function. Underneath that (but still in allProjects), add the following:
In the ext function, we’re just defining some variables that are consistent through the various module’s gradle files. Now head over to your app gradle file and inside the android {} function, where you used to have compileSdkVersion = 26, change this to compileSdkVersion(compileSdk). Same for buildToolsVersion, minSdkVersion, and targetSdkVersion.
Re-sync and nothing should have changed. But we can now do this across all our modules. Now let’s take a look at those dependencies.
Head back to the project gradle, and head back into the allProjects {} function. Underneath the ext function that we just added, add a variable to define the retrofit version. Underneath that, add another ext function, defining a new variable ‘retrofit’ which is an array. In the array, add everything that you need for retrofit to work.
Now we have a variable will includes everything we need for retrofit. The final thing we need is some way of actually using this variable in our app gradle file. To do this we’ll define a new function that takes one of these variables and for each code in the array, syncs the library. Add the following in the project gradle in the allProjects function, which will allow us to add the library internal to the module.
You’ll notice here we check if the code starts with a # or *, and do different things based on them, and if it doesn’t begin with either, we add it through implementation. This is so that we can still add annotation processors and still maintain control. So for example if the thing you’re adding is an annotation processor, instead of: “android.arch.persistence.room:compiler:$lifecycleVer”, which is an annotation processor, just do “*android.arch.persistence.room:compiler:$lifecycleVer”.
Now head back to the app gradle, and at root level, add the following line:
Delete everything to do with retrofit from the dependencies function and resync. Fingers crossed, everything will be fine, and you’ll be able to use retrofit within that module. Yay! Now of course, you probably want more than just to be able to add libraries internally to the module, so below are functions to do just that, including for tests.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ext.internalProject = { code -> | |
dependencies { | |
implementation project(":$code") | |
} | |
} | |
ext.exposedProject = { code -> | |
dependencies { | |
api project(":$code") | |
} | |
} | |
ext.internalLib = { codes -> | |
dependencies { | |
codes.each { | |
if (it.startsWith("#")) { | |
compileOnly it.substring(1) | |
} else if (it.startsWith("*")) { | |
kapt it.substring(1) | |
} else { | |
implementation it | |
} | |
} | |
} | |
} | |
ext.exposedLib = { codes -> | |
dependencies { | |
codes.each { | |
if (it.startsWith("#")) { | |
compileOnly it.substring(1) | |
} else if (it.startsWith("*")) { | |
kapt it.substring(1) | |
} else { | |
api it | |
} | |
} | |
} | |
} | |
ext.test = { codes -> | |
dependencies { | |
codes.each { | |
if (it.startsWith("*")) { | |
kaptTest it.substring(1) | |
} else { | |
testImplementation it | |
} | |
} | |
} | |
} | |
ext.androidTest = { codes -> | |
dependencies { | |
codes.each { | |
if (it.startsWith("*")) { | |
kaptAndroidTest it.substring(1) | |
} else { | |
androidTestImplementation it | |
} | |
} | |
} | |
} |
Now we can add more modules, and more gradle files, and have a single source of truth for our libraries and project settings throughout!
Below are the final app and project gradle files, using test and androidTest functions as well as internalLib.
Project build.gradle
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Top-level build file where you can add configuration options common to all sub-projects/modules. | |
// This is the project gradle file | |
buildscript { | |
ext.kotlin_version = '1.2.10' | |
repositories { | |
google() | |
jcenter() | |
} | |
dependencies { | |
classpath 'com.android.tools.build:gradle:3.0.1' | |
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" | |
} | |
} | |
allprojects { | |
repositories { | |
google() | |
jcenter() | |
} | |
ext { | |
targetSdk = 26 | |
compileSdk = 26 | |
buildTools = '27.0.1' | |
minSdk = 21 | |
} | |
def supportLibraryVer = '27.0.1' | |
def retrofitVer = '2.3.0' | |
def rxAndroidVer = '2.0.1' | |
def rxKotlinVer = '2.2.0' | |
def gsonVer = '2.8.2' | |
def constraintLayoutVer = '1.0.2' | |
def junitVer = '4.12' | |
def espressoVer = '2.2.2' | |
ext { | |
supportv4 = ["com.android.support:support-v4:$supportLibraryVer"] | |
appcompatv7 = ["com.android.support:appcompat-v7:$supportLibraryVer"] | |
retrofit = ["com.squareup.retrofit2:retrofit:$retrofitVer", | |
"com.squareup.retrofit2:adapter-rxjava2:$retrofitVer", | |
"com.squareup.retrofit2:converter-gson:$retrofitVer"] | |
rxAndroid = ["io.reactivex.rxjava2:rxandroid:$rxAndroidVer", | |
"io.reactivex.rxjava2:rxkotlin:$rxKotlinVer"] | |
gson = ["com.google.code.gson:gson:$gsonVer"] | |
constraintLayout = ["com.android.support.constraint:constraint-layout:$constraintLayoutVer"] | |
unittest = {} | |
unittest.ext { | |
junit = ["junit:junit:$junitVer"] | |
} | |
uitest = {} | |
uitest.ext { | |
espresso = ["com.android.support.test.espresso:espresso-core:$espressoVer", | |
"com.android.support.test.espresso:espresso-contrib:$espressoVer"] | |
} | |
} | |
ext.internalProject = { code -> | |
dependencies { | |
implementation project(":$code") | |
} | |
} | |
ext.exposedProject = { code -> | |
dependencies { | |
api project(":$code") | |
} | |
} | |
ext.internalLib = { codes -> | |
dependencies { | |
codes.each { | |
if (it.startsWith("#")) { | |
compileOnly it.substring(1) | |
} else if (it.startsWith("*")) { | |
kapt it.substring(1) | |
} else { | |
implementation it | |
} | |
} | |
} | |
} | |
ext.exposedLib = { codes -> | |
dependencies { | |
codes.each { | |
if (it.startsWith("#")) { | |
compileOnly it.substring(1) | |
} else if (it.startsWith("*")) { | |
kapt it.substring(1) | |
} else { | |
api it | |
} | |
} | |
} | |
} | |
ext.test = { codes -> | |
dependencies { | |
codes.each { | |
if (it.startsWith("*")) { | |
kaptTest it.substring(1) | |
} else { | |
testImplementation it | |
} | |
} | |
} | |
} | |
ext.androidTest = { codes -> | |
dependencies { | |
codes.each { | |
if (it.startsWith("*")) { | |
kaptAndroidTest it.substring(1) | |
} else { | |
androidTestImplementation it | |
} | |
} | |
} | |
} | |
} | |
task clean(type: Delete) { | |
delete rootProject.buildDir | |
} |
App build.gradle (multiple of these as your split your app into modules)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
apply plugin: 'com.android.application' | |
apply plugin: 'kotlin-android' | |
apply plugin: 'kotlin-android-extensions' | |
android { | |
compileSdkVersion(compileSdk) | |
buildToolsVersion(buildTools) | |
defaultConfig { | |
minSdkVersion(minSdk) | |
targetSdkVersion(targetSdk) | |
applicationId "com.stickerbox.gradle" | |
versionCode 1 | |
versionName "1.0" | |
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" | |
} | |
buildTypes { | |
release { | |
minifyEnabled false | |
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' | |
} | |
} | |
} | |
internalLib(retrofit) | |
internalLib(rxAndroid) | |
internalLib(gson) | |
internalLib(constraintLayout) | |
internalLib(supportv4) | |
internalLib(appcompatv7) | |
test(unittest.junit) | |
androidTest(uitest.espresso) | |
dependencies { | |
} |
Next week we’ll be taking a look at how Swift is compiled to machine code. See you then!