Where do Gradle properties in Android come from?
Understanding the insides of Gradle property resolution for (not only) Android developers
Following up on my previous post about Gradle, I’ll now dive into Gradle properties — we’ll discuss what kinds of properties there are, how we can access them and what they’re used for. If you haven’t read the previous post, I encourage you to do so, as this post relies on some bits that I explain there.
What are properties?
Groovy documentation defines a property as a combination of a private field and a getter/setter. Usually in Java we would declare a private field, a getter and a setter explicitly. In some programming languages though, this practice is wrapped in a concept of properties. In short, when declaring a property, a private field, a getter, and a setter (if the property is not final) are generated. By default, getters and setters just read/write to the backing field, but they can also be customised, so things like validation can be added.
Syntax
Here’s a comparison of Java code (without properties), and equivalent Kotlin and Groovy declarations using property syntax:
The same written in Kotlin:
And the same in Groovy:
This lengthy introduction to properties is only here so we know that:
- Property access syntax is similar to field access syntax in Java, but they shouldn’t be confused
- Accessing a property is actually translated to a method call, and this method can do things
Dynamic properties
One more important thing in Groovy that affects Gradle heavily is that Groovy is a dynamic language. In my previous post I briefly mentioned the dynamic method access, but Groovy actually offers the dynamic property access as well. This means that sometimes even though a property with certain name wasn’t declared in the object, such property can still be accessed — provided the said object offers dynamic property access. This is important, since quite a lot of properties that we access in Gradle are added dynamically. Just for fun, let’s have a look at how we’d use dynamic properties in Groovy (this is what Gradle uses under the hood):
As we can see, even though meaningOfLife
doesn’t exist, the code above not only compiles, but also doesn’t crash at runtime. This mechanism is used a lot in Gradle scripts.
Properties in Gradle
Now that we know what the properties are, what their syntax is, and that some of them may be added dynamically, let’s have a look at different properties that we can encounter.
Class properties
This one is pretty obvious — if a class, say Project
, declares a property, then it can be accessed.
Extension properties
Extension properties are ad-hoc properties that can be added to any object that implements ExtensionAware
. In other words, every ExtensionAware
object can be extended with an arbitrary extension object with the given name. This is done via extensions
property of type ExtensionContainer
present in every ExtensionAware
object. Added extension objects can be then accessed using property syntax. Here’s an example of how that would work with Project
class, which implements ExtensionAware
:
First thing to note is that theProject
class does not normally have aperson
property. It’s added dynamically when calling extensions.create
method. Second thing to note is that for this newly created property we can use both normal property syntax (assigning the name) as well as namespace method, which allows us to configure extension object using a closure. This is what we usually see when configuring android
extension. Both Project
and Task
are ExtensionAware
.
Talking about methods in a post about properties may be confusing, but the point is that
person
is an extension, and extensions can both be accessed as properties, or configured via namespace method.
We know extensions very well — for example Android Gradle plugin adds android
extension of AppExtension
type. We usually use method syntax to configure it, but we can access it using property syntax as well (e.g. android.defaultConfig.compileSdkVersion = 24
).
Extra properties
Extra properties is a special extension of type ExtraPropertiesExtension
added with name ext
to all ExtensionAware
objects. We can think of it as a container of arbitrary key-value pairs, just with special syntax:
As we see, we can create and access keys in extra properties using both property syntax (prefer that) and accessor syntax. Here’s the thing, though: we can skip the ext
part, and access a property directly from ExtensionAware
object:
The above statements aren’t always equivalent — we’ll see why later.
Another thing to remember is that properties passed with -P
command line options or added to gradle.properties
file are added to the extra properties extension. This is why (and how) we can access them — they’re not in a separate scope, or considered separately. They’re simply added to one of existing scopes. As we’ll see later, this is why project.name
property can’t be set by adding it in gradle.properties
, but for example project.personName
can.
Convention properties
Convention properties is an older version of extension properties, now mostly replaced by it. The difference is that convention properties are immutable (albeit assignable), and are lazily evaluated. We’ll not dig into details as extensions are much more prevalent in Gradle now.
Tasks of the project
Simply put, when we’re in Project
class, its tasks are accessible as properties. For example project.assembleDebug
is equivalent to project.tasks.getByName('assembleDebug')
.
Resolving properties in build.gradle
files
We now see that properties can come from many places — so now the questions is: what happens if there’s a collision? Supposed we created an extra property name
. Which name will we get when we call project.name
- name
defined in Project
class, or the one in extra property? Fortunately, there’s a strict order of scopes in which properties are searched. Remembering that build.gradle
files are executed against a Project
instances, the scopes which are searched for properties are:
- Project object — if there’s a property with a given name declared in
Project
class, it’s used. Whether it’s readable or writable is determined by the presence of corresponding getter or setter method. - Extra properties — from
ext
extension; these are both readable and writable - Extensions — read-only, usually added by plugins
- Convention properties
- Extra properties, extensions and convention properties of the parent project (recursively, up to the root project), but in the read-only scope
Examples
Let’s have a look at some code that we could write in build.gradle
files. Again, build.gradle
means that the code is executed on Project
instance, and Project
is ExtensionAware
.
[1] Here we write extra property called personName
. This is the first time we do it, so this actually creates a mapping for this property’s name in ext
object.
[2] Here we access extra property personName
explicitly from ext
namespace
[3] And here we access it second time, but this time without the namespace. This works because there’s no personName
in Project
class, and extra properties are next in order to look for a missing property. Notice that we wouldn’t be able to do that before [1], as the property wouldn’t be defined yet.
Extensions example
Let’s have a look at extensions now:
person
in [1] is our PersonExtension
, so we can access its name
property. [2] defines extra property person
, so now [3] will print “Tim” , because extra properties have higher priority than extensions. Notice that after we write [2], we can no longer write person.name
, because person
will now always refer to extra property, and not an extension (and this property is a String
, not a PersonExtension
)! If we wanted to change thename
in person
extension, we now have to refer to extension explicitly directly: this.extensions.person.name = "Molly"
.
Reading properties in child project
If we have two files: root project’s build.gradle
, and child project’s build.gradle
like this:
[1] will print out “Tim” — because person
is not found in the current project (it’s not in Project
class, there’s no extra person
in the child project, no extension like a convention or a task), the parent project is searched - and person
is finally found in its extra properties.
[2] will fail, because ext
refers to child project’s extra properties, and this one doesn’t have person
defined. If we comment this line, then:
[3] will define person
in child project’s extras
[4] and [5] Will now print out “John”, because child project’s extras now contain person
property, and they’re searched before parent’s extras
[6] will print “Tim”, because now we explicitly ask for parent’s extra properties
Writing properties in a child project
Let’s take the same root project’s build.gradle
:
and the following child project’s build.gradle
file:
[1] will fail — this is because theperson
property comes from parent project, so it’s read-only.
[2] and [3] Will work, and will assign “John” to theperson
property in root project’s extras. This is because now we’re directly operating on parent project's, where its extra properties are writable (or on extra extension, in which everything is readable and writable).
This may be confusing since previously it was stated that parent properties are accessed in read-only scope. This is true — when accessing property from parent using property access syntax on child project, then property is read-only. In [2] and [3] property from parent is accessed using property access on parent project, so it’s writable.
[4] will define person
in child project’s extras.
[5] and [6] Will assign “Tim” to child project’s person
extra property. This differs from [1] in than this person
is child project’s one, not parent’s.
[7] and [8] Will print “Tim” — both lines refer to child project’s extra person
property
[9] will print “John” — this time we explicitly ask for parent.
Properties vs variables
It’s also important to remember the distinction between properties (which can be dynamically resolved by Gradle) and variables (which are part of Groovy language). If we want to declare a propery within a Project
(for example to be used by child project), then we have to use ext
property. But if we want to declare a variable to be used in the script, then we should use standard Groovy syntax - using def
keyword, or by specifying type explicitly. But then what we declared is only a variable in the script, and it precedes all the scopes that we’ve talked about.
Here [1] will print “Molly”, because a script variable has precedence over properties. When in [2] we modify personName
, we refer to the variable, so [3] will print “Tim”. [4] addresses extra property explcitly, so it will print “John”.
Summary
That’s about it when it comes to Gradle properties. Hopefully, this brings you a step closer to fully understanding Gradle scripts and the ability to confidently write one by yourself!
P.S. Again many thanks to Gradle team and Android team at Tooploox for proofreading the post, and to Ania Langiewicz for the wonderful feature graphic!