One of the vital parts of creating an app, along with the development itself, is the process of building – putting together the modules and specifying the interactions between them. The primary goal of build automation systems is to make this process more convenient and efficient for a developer. Gradle is an excellent example of such a tool, and is the primary build tool for Android.
In this tutorial, you’ll gain a better understanding of what Gradle is, and how you can use Gradle to supercharge your builds. By the end of this tutorial you should be able to
- Build your Android apps from the command-line
- Read through a Gradle build file
- Create your own Gradle plugin
- Create build flavors for profit!
What is Gradle?
Gradle is an open source build automation system. It brings the convenience of a Groovy-based DSL along with the advantages of Ant and Maven. With Gradle, you can easily manipulate the build process and its logic to create multiple versions of your app. It’s much easier to use and a lot more concise and flexible when compared to Ant or Maven alone.
So, there was little wonder why during Google I/O in May 2013, the Android Gradle plugin was introduced as the build tool built into the first preview of Android Studio :]
Getting Started
Download SocializifyStarter, the starter project for this tutorial. At minimum, you’ll need Android Studio 3.0 installed on your computer. Open the project in Android Studio, and you’ll be prompted to setup the Gradle wrapper:
Choose OK to configure the wrapper, which you’ll learn more about later in the tutorial.
Depending on which version of Android Studio you’re running, you may also be prompted to update the Gradle plugin:
Choose Update to finish opening the project in Android Studio.
Before starting working with the project, let’s review its structure in the Project pane in Android Studio:
Pay attention to the files with the green Gradle icon and .gradle extension. These files are generated by Android Studio automatically during project creation. They are responsible for the processing of your project’s build. They contain the necessary info about the project structure, library dependencies, library versions, and the app versions you’ll get as a result of the build process.
Project-level build.gradle
Find the build.gradle file in the root directory of the project. It’s called a top-level (project-level) build.gradle file. It contains the settings which are applied to all modules of the project.
// 1 buildscript { // 2 repositories { google() jcenter() } // 3 dependencies { classpath 'com.android.tools.build:gradle:3.0.0' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.1.51' } } // 4 allprojects { repositories { google() jcenter() } }
Here’s what’s going on, step by step:
- In the
buildscript
block you define settings needed to perform your project building. - In the
repositories
block you add names of the repositories that Gradle should search for the libraries you use. - The
dependencies
block contains necessary plugin dependencies, in this case the Gradle and Kotlin plugins. Do not put your module dependencies in this block. - The structure of the
allprojects
block is similar to thebuildscript
block, but here you define repositories for all of your modules, not for Gradle itself. Usually you don’t define thedependencies
section forallprojects
. The dependencies for each module are different and should reside in the module-level build.gradle.
Module-level build.gradle
Now go to the build.gradle file in the app module directory. It contains dependencies (libraries which a module relies on), and instructions for the build process. Each module defines its own build.gradle file.
// 1 apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' // 2 android { // 3 compileSdkVersion 27 // 4 buildToolsVersion "26.0.2" // 5 defaultConfig { // 6 applicationId "com.raywenderlich.socializify" // 7 minSdkVersion 21 // 8 targetSdkVersion 27 // 9 versionCode 1 // 10 versionName "1.0" } } // 11 dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'org.jetbrains.kotlin:kotlin-stdlib-jre7:1.1.51' implementation 'com.android.support:appcompat-v7:27.0.1' implementation 'com.android.support.constraint:constraint-layout:1.0.2' }
The code above does the following:
- Specifies a list of plugins needed to build the module. The
com.android.application
plugin is necessary in order to setup the Android-specific settings of the build process. Here you can also usecom.android.library
if you’re creating a library module. Thekotlin-android
andkotlin-android-extensions
plugins allow you to use the Kotlin language and the Kotlin Android extensions in your module. - In the
android
block you place all platform-specific options of the module. - The
compileSdkVersion
option indicates the API level your app will be compiled with. In other words, you cannot use features from an API higher than this value. Here, you’ve set the value to use APIs from Android Oreo. - The
buildToolsVersion
option indicates the version of the compiler. From Gradle plugin 3.0.0 onward, this field is optional. If it is not specified, the Android SDK uses the most recent downloaded version of the Build Tools. - The
defaultConfig
block contains options which will be applied to all build versions (e.g., debug, release, etc) of your app by default. - The
applicationId
is the identifier of your app. It should be unique so as to successfully publish or update your app on Google Play Store. - In order to set the lowest API level supported, use
minSdkVersion
. Your app will not be available in the Play Store for the devices running on lower API levels. - The
targetSdkVersion
parameter defines the maximum API level your app has beeen tested on. That is to say, you’re sure your app works properly on the devices with this SDK version, and it doesn’t require any backward compatibility behaviors. The best approach is to thoroughly test an app using the latest API, keeping yourtargetSdkVersion
value equal tocompileSdkVersion
. versionCode
is a numeric value for the app version.versionName
is a user-friendly string for the app version.- The
dependencies
block contains all dependencies needed for this module. Later in this tutorial, you’ll find out more about managing your project’s dependencies.
Finally, settings.gradle
Whew, build.gradle
was quite a big file! Hope, you’re not tired yet :] The next file will be quite short – move to the settings.gradle file in the root directory. Its contents should look as follows:
include ':app'
In this file, you should define all of your project’s modules by name. Here we have only one module – app. In a large, multi-module project, this file can have a much longer list.
Groovy vs. Kotlin in Gradle
Kotlin’s popularity is growing every day. Besides Android apps, you can also write back-end web code, front-end web code, and even iOS apps using Kotlin! Recently, Gradle announced Kotlin language support for writing build scripts. The Gradle Kotlin DSL is still in pre-release and requires nontrivial setup, and won’t be covered in this tutorial. However, it’s quite promising and surely worth waiting for its release.
Why Kotlin
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 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 build system.
Mastering the build: Gradle Commands
To execute Gradle commands, you can use both the command line and Android Studio. It’s better to start from the former one to get acquainted more deeply about what’s going on. So, how can you start working with Gradle commands? Pretty easy – use gradlew.
What is gradlew
gradlew is the Gradle Wrapper. You don’t need to worry about installating Gradle on your computer – the wrapper will do that for you. Even more, it’ll allow you to have different projects built with various versions of Gradle.
Open your command line and move to the root directory of the starter project:
cd path/to/your/Android/projects/SocializifyStarter/
gradlew tasks
After that, execute the following command:
./gradlew tasks
You’ll see a list containing all available tasks:
> Task :tasks ------------------------------------------------------------ All tasks runnable from root project ------------------------------------------------------------ Android tasks ------------- androidDependencies - Displays the Android dependencies of the project. signingReport - Displays the signing info for each variant. sourceSets - Prints out all the source sets defined in this project. Build tasks ----------- assemble - Assembles all variants of all applications and secondary packages. assembleAndroidTest - Assembles all the Test applications. assembleDebug - Assembles all Debug builds. assembleRelease - Assembles all Release builds. ... Build Setup tasks ----------------- init - Initializes a new Gradle build. wrapper - Generates Gradle wrapper files. Help tasks ---------- ... Install tasks ------------- ... Verification tasks ------------------ ... lint - Runs lint on all variants. ... To see all tasks, run gradlew tasks --all To get more detail about task, run gradlew help --task <task>
These commands exist to help you with tasks like project initialization, building, testing and analyzing. If you forget a specific command, just execute ./gradlew tasks
to refresh your memory.
gradlew assemble
Now skim the list of commands again, and find commands starting with ‘assemble’ under the Build tasks
section. Run the first command:
./gradlew assemble
Below is the output of executing this command:
> Task :app:compileDebugKotlin Using kotlin incremental compilation > Task :app:compileReleaseKotlin Using kotlin incremental compilation BUILD SUCCESSFUL in 29s 52 actionable tasks: 52 executed
From the output, it’s apparent that Gradle compiled two versions of the app – debug and release.
Verify this by changing to the build output directory:
cd app/build/outputs/apk/
To review the contents of a directory run the following command:
ls -R
The ls
command displays all files and directories in the current directory. The -R
parameter forces this command to execute recursively. In other words, you’ll not only see the contents of your current directory but also of child directories.
You’ll get the following output:
debug release ./debug: app-debug.apk output.json ./release: app-release-unsigned.apk output.json
As you see, Gradle generated both debug and release apks.
gradlew lint
Move back to the root directory:
cd ../../../..
Run the following command:
./gradlew lint
The lint
command, and any commands which start with ‘lint’, analyzes the whole project looking for various mistakes, typos or vulnerabilities. The first command will find all the issues in a project with both critical and minor severity.
You’ll get the output with the count of issues found:
> Task :app:lint Ran lint on variant debug: 47 issues found Ran lint on variant release: 47 issues found Wrote HTML report to file:///Users/username/path/to/your/Android/projects/SocializifyStarter/app/build/reports/lint-results.html Wrote XML report to file:///Users/username/path/to/your/Android/projects/SocializifyStarter/app/build/reports/lint-results.xml
Review the report by typing the following on Mac:
open app/build/reports/lint-results.html
or on Linux:
xdg-open app/build/reports/lint-results.html
The default browser on your computer will open with the specified file:
You can inspect all the issues found with code snippets and an expanded description of a possible solution. However, don’t focus too much on all of these issues – pay attention to the critical ones and fix them immediately. Minor issues shouldn’t necessarily warrant a refactoring, depending upon your teams guidelines and processes.
Managing Dependencies
Now it’s time to make changes to the application itself. Build and run the starter project:
This screen shows a user’s profile – name, followers, photos, etc. However, something’s missing – an avatar! In order to load an avatar from a URL, we’ll use a third-party library in our application, namely, we’ll use Picasso.
Picasso is described as a “A powerful image downloading and caching library for Android”. To get started with Picasso, you need to add it as a dependency to your project.
First, create a file named dependencies.gradle in the root directory of the project. You’ll use this file as the means of identifying all the project dependency versions in one place. Add the following to this file:
ext { minSdkVersion = 17 targetSdkVersion = 27 compileSdkVersion = 27 buildToolsVersion = "26.0.2" kotlinVersion = "1.1.51" supportVersion = "27.0.1" picassoVersion = "2.5.2" }
Open the project-level build.gradle file (the one in the root directory, not the one in the app directory!) and add the following line on the top of the file:
apply from: 'dependencies.gradle'
Now you can use the properties you specified in the dependencies.gradle file in your other project build files like this:
app module-level build.gradle
android { compileSdkVersion rootProject.compileSdkVersion buildToolsVersion rootProject.buildToolsVersion defaultConfig { applicationId "com.raywenderlich.socializify" minSdkVersion rootProject.minSdkVersion targetSdkVersion rootProject.targetSdkVersion versionCode 1 versionName "1.0" } } dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation "com.android.support:appcompat-v7:$rootProject.supportVersion" implementation "com.android.support:design:$rootProject.supportVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$rootProject.kotlinVersion" }
Add the following line, to include Picasso, in your module-level build.gradle file (in the app directory!) inside the dependencies
block:
implementation "com.squareup.picasso:picasso:$rootProject.picassoVersion"
When you modify build files, you’ll be prompted to sync the project:
Don’t be afraid to re-sync your project. It takes a short while until the sync is completed, and the time it takes gets longer when you have more dependencies and more code in your project.
When the project syncing is complete, open ProfileActivity
and add this function to load a user’s avatar:
// 1
private fun loadAvatar() {
// 2
Picasso.with(this).load("https://goo.gl/tWQB1a").into(avatar)
}
If you get build errors, or if you’re prompted to resolve the imports in Android studio, be sure the following imports are included:
import com.squareup.picasso.Picasso
import kotlinx.android.synthetic.main.activity_profile.*
Here’s a step-by-step explanation of what’s going on:
- You define a Kotlin function
loadAvatar()
- Picasso needs an instance of a
Context
to work, so you must callwith(context: Context!)
, passing in the current Activity as the Context. This returns an instance of the Picasso class. Next, you specify the URL of an image you want to load with theload(path: String!)
method. The only thing left is to tell Picasso where you want this image to be shown by calling theinto(target: ImageView!)
method.
Add this function invocation in the onCreate(savedInstanceState: Bundle?)
function of your ProfileActivity
:
loadAvatar()
The last step, which even senior Android developers tend to forget, is to add the android.permission.INTERNET
permission. If you forgot to add this permission, Picasso simply can’t download the image and it’s hard to spot any error. Go to the AndroidManifest.xml file and add the following permission above the application
tag:
<uses-permission android:name="android.permission.INTERNET" />
That’s all you need to show the user’s avatar. Build and run the project:
Gradle Dependency Configurations
The implementation
keyword you previously used is a dependency configuration, which tells Gradle to add Picasso in such way, that it’s not available to other modules. This option significantly speeds up the build time. You’ll use this keyword more often than others.
In some other cases, you may want your dependency to be accessible to other modules of your project. In those cases, you can use the api
keyword.
Other options include runtimeOnly
and compileOnly
configurations, which mark a dependency’s availability during runtime or compile time only.
Ready to Publish: Working with Product Flavors and Build Types
Your app is ready, and you’re thinking of ways to profit from it :]
One solution might be to have multiple versions of your app: a free and paid version. Luckily for you, gradle supports this at the build level, allowing you to define the boundaries of different build types. However, before getting started, you need to understand how Gradle allows you to work with different app versions.
Build Types
By default, there are two build types – debug and release. The only difference between them is the value of the debuggable
parameter. In other words, you can use the debug version to review logs and to debug the app, while the release one is used to publish your app to the Google Play Store. You can configure properties to the build types by adding the following code in the android
block of your module-level build.gradle file:
buildTypes { release { } debug { } }
In the debug
and release
blocks you can specify the type-specific settings of your application.
Build Signing
One of the most important configurations of the build is its signature. Without a signature, you’ll be unable to publish your application, since it’s necessary to verify you as an owner of the specific application. While you don’t need to sign the debug build – Android Studio does it automatically – the release build should be signed by a developer.
When your keystore is ready, add the code below in the android
block and above the buildTypes
block (the order of declaration matters) of the module-level build.gradle file:
signingConfigs { release { storeFile file("path to your keystore file") storePassword "your store password" keyAlias "your key alias" keyPassword "your key password" } }
In the signingConfigs
block, you specify your signature info for the build types. Pay attention to the keystore file path. It should be specified with respect to the module directory. In other words, if you created a keystore file in the module directory and named it “keystore.jks”, the value you should specify will be equal to the name of the file.
Update the buildTypes
block to sign your release build automatically:
release { signingConfig signingConfigs.release }
- Once you’ve published your app to the Google Play Store, subsequent submissions must use the same keystore file and password, so keep them safe.
- Be sure NOT to commit your keystore passwords to a version control system such as GitHub. You can do so by keeping the password in a separate file from
build.gradle
, saykeystorePassword.gradle
in aSigning
directory, and then referencing the file from the app module-levelbuild.gradle
via:apply from: "../Signing/keystorePassword.gradle
Then be sure to keep keystorePassword.gradle
ignored by your version control system. Other techniques include keeping the password in an OS-level environment variable, especially on your remote Continuous Integration system, such as CircleCI.
Build Flavors
In order to create multiple versions of your app, you need to use product flavors. Flavors are a way to differentiate the properties of an app, whether it’s free/paid, staging/production, etc.
You’ll distinguish your app flavors with different app names. First, add the following names as strings in the strings.xml file:
<string name="app_name_free">Socializify Free</string>
<string name="app_name_paid">Socializify Paid</string>
And remove the existing:
<string name="app_name">Socializify</string>
Now that the original app_name
string is no longer available, edit your AndroidManifest.xml file and replace android:label="@string/app_name"
with android:label="${appName}"
inside the application
tag.
Add the following code in the android
block of your module-level build.gradle file:
// 1 flavorDimensions "appMode" // 2 productFlavors { // 3 free { // 4 dimension "appMode" // 5 applicationIdSuffix ".free" // 6 manifestPlaceholders = [appName: "@string/app_name_free"] } paid { dimension "appMode" applicationIdSuffix ".paid" manifestPlaceholders = [appName: "@string/app_name_paid"] } }
- You need to specify the flavor dimensions to properly match the build types. In this case, you need only one dimension – the app mode.
- In the
productFlavors
specify a list of flavors and their settings. In this case,free
, andpaid
- Specify the name of the first product flavor –
free
. - It’s mandatory to specify the
dimension
parameter value. Thefree
flavor belongs to theappMode
dimension. - Since you want to create separate apps for free and paid functionality, you need them to have different app identifiers. The
applicationIdSuffix
parameter defines a string that’ll be appended to theapplicationId
giving your app unique identifiers. - The
manifestPlaceholders
allows you to modify properties in your AndroidManifest.xml file at build time. In this case, modify the application name depending on its version.
Sync your project with Gradle again. After the project sync, run the tasks
command, and see if you can spot what’s changed:
./gradlew tasks
You’ll get a similar list of tasks to the one you got when you ran this command first time:
... Build tasks ----------- ... assembleDebug - Assembles all Debug builds. assembleFree - Assembles all Free builds. assemblePaid - Assembles all Paid builds. assembleRelease - Assembles all Release builds. ...
Spot the difference? If you pay attention to the tasks under the Build tasks
section, you should have some new ones there. You now have separate commands for each build type and build flavor.
Remove the generated output folder from previous ./gradlew assemble
task so that you can see the clear difference before and after adding buildTypes
and productFlavors
. Run the command:
rm -rf app/build/outputs/apk
Then
./gradlew assembleDebug
When the command completes, check the output directory:
cd app/build/outputs/apk
ls -R
You’ll get something like this:
free paid ./free: debug ./free/debug: app-free-debug.apk output.json ./paid: debug ./paid/debug: app-paid-debug.apk output.json
You should have two builds generated – freeDebug
and paidDebug
.
What is a Build Variant
From the output above, what you’ve actually generated are different build variants, which are a combination of build types – debug and release and build flavors – free and paid. That is to say, you have four possible build variants – paidDebug, paidRelease, freeDebug and freeRelease.
Great! You’ve got two different build flavors, however, differing names isn’t enough for you to profit from. Instead, you’ll configure your app’s behavior based on the flavor type!
Declare a constant for the paid flavor right below the declaration of ProfileActivity
class:
companion object {
const val PAID_FLAVOR = "paid"
}
Add the following function to ProfileActivity
:
private fun isAppPaid() = BuildConfig.FLAVOR == PAID_FLAVOR
You can now check if a user is using a paid version of the app. Depending on the result of this check, you’ll enable or disable some functionality visible to your user so they can clearly see what version they’re using in-app.
Add these strings to the strings.xml file:
<string name="free_app_message">Hi! You\'re using the free version of the application</string>
<string name="paid_app_message">Hi! Congratulations on buying
the premium version of the application</string>
Add the following functions below isAppPaid()
:
private fun showMessage() {
val message = if (isAppPaid()) R.string.paid_app_message else R.string.free_app_message
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
}
private fun togglePhotosVisibility() {
extraPhotos.visibility = if (isAppPaid()) View.VISIBLE else View.GONE
restriction.visibility = if (isAppPaid()) View.GONE else View.VISIBLE
}
Add these functions invocations in the onCreate(savedInstanceState: Bundle?)
function:
showMessage()
togglePhotosVisibility()
Now, your user will see a different greeting message and will be able to view the whole photo feed or just some of the photos depending on the app version.
Select the freeRelease build variant in the window below:
Build and run the project (you may first need to choose the app build configuration in the drop-down next to the Run button):
You should see that the functionality of the app is restricted and the message with a corresponding text is shown.
Select the paidRelease option, and run the app again:
If a user buys your app, they’ll be able to access its full functionality.
Creating Tasks
Sometimes you need your build system to do something more complicated or customize the build process in some way. For example, you may want Gradle to output an APK file containing the build date in its name. One possible solution to this is to create a custom Gradle task.
Add the following code in your module-level build.gradle file at the same level as android
block:
// 1 task addCurrentDate() { // 2 android.applicationVariants.all { variant -> // 3 variant.outputs.all { output -> // 4 def date = new Date().format("dd-MM-yyyy") // 5 def fileName = variant.name + "_" + date + ".apk" // 6 output.outputFileName = fileName } } }
Here’s what’s is going on:
- You define an
addCurrentDate()
task. - You iterate through all the output build variants.
- You iterate over all the APK files.
- You create an instance of
Date
and format it. - You create a new filename appending the current date to the initial name.
- You set the new filename to current APK file.
Now you need to execute this task at a specific point of the build process. Add the following code below the task addCurrentDate()
block:
gradle.taskGraph.whenReady { addCurrentDate }
The task specified in the whenReady
block will be called once when the current graph is filled with tasks and ready to start executing them. Here, you specify the name of your addCurrentDate
task.
Now, go back to the command line and make sure you’re in the root directory. Run the following command to assemble a build:
./gradlew assemblePaidRelease
After the task has completed, go to the output directory and check if the build has been named correctly:
cd app/build/outputs/apk/paid/release/
ls
You should get a similar output:
output.json paidRelease_12-11-2017.apk
If your task executed correctly, all your builds will be named with this convention.
Creating Custom Plugins
Usually it’s a good idea to factor out your code into smaller pieces so it can be reused. Similarly, you can factor out your tasks into a custom behavior for the building process as a plugin. This will allow you to reuse the same behavior in other modules you may add to your project.
To create a plugin, add the following class below the addCurrentDate
task in the module-level build.gradle file:
class DatePlugin implements Plugin<Project> { void apply(Project project) { project.task('addCurrentDatePluginTask') { project.android.applicationVariants.all { variant -> variant.outputs.all { output -> def date = new Date().format("dd-MM-yyyy") def fileName = variant.name + "_" + date + ".apk" output.outputFileName = fileName } } } } }
Add the name of your plugin at the top of this file along with the other apply plugin
definitions:
apply plugin: DatePlugin
Conceptually, the code in the plugin is doing the same thing as the task – you’re still modifying the names of the output files. The only difference is that you define a class which implements Plugin
and its single method apply(Project project)
.
In this method, you’re adding your plugin to the target – Project
. By calling the task(String name, Closure configureClosure)
method you’re creating a new task with a specific name and behavior and adding it to the project.
Now modify the whenReady
block to call a new task:
gradle.taskGraph.whenReady { addCurrentDatePluginTask }
and remove the task addCurrentDate()
block you added earlier.
Now you can verify that this plugin is doing the same thing like the task. Assemble a new build and verify the APK filename:
./gradlew assemblePaidRelease
cd app/build/outputs/apk/paid/release/
ls
output.json paidRelease_12-11-2017.apk
Where to Go From Here
You can download the final project here.
The Android Gradle plugin 3.0 contains some significant differences from previous versions. So it’s worth reviewing the changelog.
Also, if you’re insterested in the Gradle Kotlin DSL, here you can find a list of usage examples to get familiar with it.
I hope you’ve enjoyed this Getting Started with Gradle tutorial! Don’t forget to leave your feedback and feel free to ask any questions in the comments below :]
The post Gradle Tutorial for Android: Getting Started appeared first on Ray Wenderlich.