Android applications are typically built using the Gradle build system. Before we dive into the details of how to configure your build, we'll explore the concepts behind the build so you can see the system as a whole.
What is a build?
A build system transforms your source code into an executable application. Builds often involve multiple tools, to analyze, compile, link, and package your application or library. Gradle uses a task-based approach to organize and run these commands.
Tasks encapsulate commands that translate their inputs into
outputs. Plugins define tasks and their configuration. Applying
a plugin to your build registers its tasks, and wires them together using their
inputs and outputs.. For example, applying the Android Gradle Plugin (AGP)
to your build file will register all the tasks necessary to build an APK, or an
Android Library. The java-library
plugin lets you build a jar from Java source
code. Similar plugins exist for Kotlin, and other languages, but other plugins
are meant to extend plugins. For example, the protobuf
plugin is meant to add
protobuf support to existing plugins like AGP or java-library
.
Gradle prefers convention over configuration so plugins will come with good default values out of the box, but you can further configure the build through a declarative Domain-Specific Language (DSL). The DSL is designed so can you specify what to build, rather than how to build it. The logic in the plugins manages the "how". That configuration is specified across several build files in your project (and subprojects).
Task inputs can be files and directories as well as other information encoded as Java types (integer, strings, or custom classes). Outputs can only be directory or files as they have to be written on disk. Wiring a task output into another task input, links the tasks together so that one has to run before the other.
While Gradle supports writing arbitrary code and task declarations in your build files, this can make it more difficult for tooling to understand your build and for you to maintain. For example, you can write tests for code inside plugins but not in build files. Instead, you should restrict build logic and task declarations to plugins (that you or someone else define) and declare how you want to use that logic in your build files.
What happens when a Gradle build runs?
Gradle builds run in three phases. Each of these phases executes different parts of code that you define in your build files.
- Initialization determines which projects and subprojects are included in the build, and sets up classpaths containing your build files and applied plugins. This phase focuses on a settings file where you declare projects to build and the locations from which to fetch plugins and libraries.
- Configuration registers tasks for each project, and executes the build file to apply the user's build specification. It's important to understand that your configuration code won't have access to data or files produced during execution.
- Execution performs the actual "building" of your application. The output of configuration is a Directed Acyclic Graph (DAG) of tasks, representing all required build steps that were requested by the user (the tasks provided on the command line or as defaults in the build files). This graph represents the relationship between tasks, either explicit in a task's declaration, or based on its inputs and outputs. If a task has an input that is the output of another task, then it must run after the other task. This phase runs out-of-date tasks in the order defined in the graph; if a task's inputs haven't changed since its last execution, Gradle will skip it.
For more information see the Gradle Build lifecycle.
Configuration DSLs
Gradle uses a Domain-Specific Language (DSL) to configure builds. This declarative approach focuses on specifying your data rather than writing step-by-step (imperative) instructions.
DSLs attempt to make it easier for everyone, domain experts and programmers, to contribute to a project, defining a small language that represents data in a more natural way. Gradle plugins can extend the DSL to configure the data they need for their tasks.
For example, configuring the Android part of your build might look like:
Kotlin
android { namespace = "com.example.app" compileSdk = 34 // ... defaultConfig { applicationId = "com.example.app" minSdk = 34 // ... } }
Groovy
android { namespace 'com.example.myapplication' compileSdk 34 // ... defaultConfig { applicationId "com.example.myapplication" minSdk 24 // ... } }
Behind the scenes, the DSL code is similar to:
fun Project.android(configure: ApplicationExtension.() -> Unit) {
...
}
interface ApplicationExtension {
var compileSdk: Int
var namespace: String?
val defaultConfig: DefaultConfig
fun defaultConfig(configure: DefaultConfig.() -> Unit) {
...
}
}
Each block in the DSL is represented by a function that takes a lambda to configure it, and a property with the same name to access it. This makes the code in your build files feel more like a data specification.
External dependencies
The Maven build system introduced a dependency specification, storage and management system. Libraries are stored in repositories (servers or directories), with metadata including their version and dependencies on other libraries. You specify which repositories to search, versions of the dependencies you want to use, and the build system downloads them during the build.
Maven Artifacts are identified by group name (company, developer, etc), artifact
name (the name of the library) and version of that artifact. This is typically
represented as group:artifact:version
.
This approach significantly improves build management. You'll often hear such repositories called "Maven repositories", but it's all about the way the artifacts are packaged and published. These repositories and metadata have been reused in several build systems, including Gradle (and Gradle can publish to these repositories). Public repositories allow sharing for all to use, and company repositories keep internal dependencies in-house.
You can also modularize your project into subprojects (also known as "modules" in Android Studio), which can also be used as dependencies. Each subproject produces outputs (such as jars) that can be consumed by subprojects or your top-level project. This can improve build time by isolating which parts need to be rebuilt, as well as better separate responsibilities in the application.
We'll go into more detail on how to specify dependencies in Add build dependencies.
Build variants
When you create an Android application, you'll typically want to build multiple variants. Variants contain different code or are built with different options, and are composed of build types and product flavors.
Build types vary declared build options. By default, AGP sets up "release" and "debug" build types, but you can adjust them and add more (perhaps for staging or internal testing).
A debug build doesn't minify or obfuscate your application, speeding up its build and preserving all symbols as is. It also marks the application as "debuggable", signing it with a generic debug key and enabling access to the installed application files on the device. This makes it possible to explore saved data in files and databases while running the application.
A release build optimizes the application, signs it with your release key, and protects the installed application files.
Using product flavors, you can change the included source and dependency variants for the application. For example, you may want to create "demo" and "full" flavors for your application, or perhaps "free" and "paid" flavors. You write your common source in a "main" source set directory, and override or add source in a source set named after the flavor.
AGP creates variants for each combination of build type and product flavor. If
you don't define flavors, the variants are named after the build types. If you
define both, the variant is named <flavor><Buildtype>
. For example, with build
types release
and debug
, and flavors demo
and full
, AGP will create
variants:
demoRelease
demoDebug
fullRelease
fullDebug
Next steps
Now that you've seen the build concepts, take a look at the Android build structure in your project.