ConstraintLayout Flow: Simple Grid Building Without Nested Layouts
AndroidConstraintLayout chains are great, but they only work for one row of items. What if you have too many items to fit on one...
The folks at Gradle recently released version 5.0, which means that us Android developers now have access to Gradle Kotlin DSL v1.0! This means that we can write Gradle build scripts in our favorite language, Kotlin.
Gradle Kotlin DSL is a domain specific language built with the express purpose of defining Gradle build plans. This has all of your favorite functions and assignments from the Gradle Groovy DSL, but now in Kotlin.
The full feature set of Gradle Kotlin DSL is supported on less IDEs than Groovy, but Android Studio and IntelliJ IDEA support everything we need, so most Android developers shouldn’t have a lot of issues.
I don’t know about you, but editing Gradle Groovy files is not my favorite part about Android development. Groovy is dynamically typed, so Android Studio has a tough time providing you with intelligent hints about what methods you can call, what parameters they take, and when you’ve done something wrong. Furthermore, since Gradle scripts are the only thing I typically see Groovy in, the syntax is way more difficult for me to parse than that of Kotlin, which I use every day during app development.
By writing our Gradle scripts in Kotlin instead of Groovy, we can solve both these problems. Since Kotlin is statically typed, it’s simpler for the IDE to fill in helpful hints about the code. Plus, since we use Kotlin for day-to-day development on Android apps, editing those Gradle files isn’t as much of a cognitive load since we’re familiar with the language’s syntax.
When you create a project in Android Studio, it creates a Gradle script using Groovy by default. To continue past this, we’ll convert the default Gradle scripts to use Kotlin instead. We’ll first make some minor syntactical changes to our Groovy scripts to inch them closer to their eventual Kotlin forms, so that when we actually change the file extension, it won’t have as many errors.
Depending on the default version of Gradle for your version of Android Studio, your project may have a version of Gradle from before Gradle Kotlin DSL was fully supported. In your app’s gradle-wrapper.properties
file, change the Gradle version to 5.0 so that you’ll have the stable 1.0 version of Gradle Kotlin:
distributionUrl=https://services.gradle.org/distributions/gradle-5.0-all.zip
In most places, Groovy doesn’t care whether you use single quotes or double quotes to encapsulate strings. Kotlin is pickier: it requires double quotes. Do a find and replace (cmd+R macOS/ctrl+R Windows) for all the single quotes in your Gradle scripts and change them all to double quotes.
Any plugin applications using the legacy apply plugin
syntax should be replaced with the newer plugins DSL. This new syntax allows Gradle to perform optimizations for loading your plugins, and helps the IDE with providing hints about the plugin classes.
// legacy
apply plugin: 'com.android.application'
// plugins DSL
plugins {
id("com.android.application")
}
However, the plugins DSL does have limitations, so if you can’t use it in your case, you can still use the legacy apply plugin
in Kotlin after converting the syntax.
A feature of Groovy is the ability to assign a value or call a method with the exact same syntax:
// this is a method call
targetSdkVersion 28
// this is an assignment
versionCode 1
That’s not the case in Kotlin, and Groovy doesn’t require this syntax, so we can go ahead and change those to their more explicit counterparts right now:
// this is a method call
targetSdkVersion(28)
// this is an assignment
versionCode = 1
Note that Groovy uses a similar property access paradigm as Kotlin, using the underlying setProperty(5)
function when you type property = 5
. Therefore, the versionCode = 1
above could also be changed to setVersionCode(1)
to the same effect, but because we also have property access in Kotlin, we can simply use the assignment operator.
If you’re not sure if a Groovy line should be converted to an assignment or a method call, you can always use quick info (ctrl+J macOS/ctrl+Q Windows) to pop up some information about the item where the cursor is. They all will show methods, but the ones that follow the JavaBean naming convention (get...
, set...
) can be converted to assignments in Kotlin.
Since most of the dependencies
block in app/build.gradle
is typically formatted in a similar way, I like to use a regex find and replace to reformat all of the implementation
lines in one move:
// find regex
mplementations?(.*)$
// replace with regex
mplementation($1)
Now we’ve gotten the scripts as close as possible to Kotlin syntax while still being Groovy, so we’re ready to actually convert the files to Kotlin scripts instead of Groovy scripts. Rename the file names from build.gradle
to build.gradle.kts
to indicate that they’re now Kotlin script files.
Of course, this will cause a lot of red syntax highlighting, since we haven’t actually converted everything to Kotlin yet, but we’ll get there. Do a Gradle sync now, and at the end of every section below. If Android Studio ever tells you that “There are new script dependencies available” via a pop-down at the top of the window, choose the Enable auto-reload option to automatically apply them.
In Groovy, we had access to the ext
object, which allows us to set variables that can be accessed from any of the inheriting Gradle scripts (this is often used to set version numbers for various dependencies). We can use extra.set("constraint_layout_version", "1.1.3")
with rootProject.extra.get("constraint_layout_version")
but this doesn’t give us the benefit of autocomplete for our properties, since they need to be accessed via those key strings.
We’ll instead use a buildSrc
module to create an external store for these globally-needed variables. In the root directory of your project, create the following directory path: buildSrc/src/main/kotlin
. Within the buildSrc
directory, create a file called build.gradle.kts
and use it to apply the Kotlin DSL plugin:
plugins {
// note the backtick syntax (since `kotlin-dsl` is
// an extension property on the plugin's scope object)
`kotlin-dsl`
}
repositories {
jcenter() // this is needed to download dependencies for kotlin-dsl
}
Within the buildSrc/src/main/kotlin
directory, create a .kt
file. It doesn’t matter what you name it, but since I usually use it for dependency declaration and versioning, I call it dependency.kt
. You can then create objects with properties, and these will be able to be accessed from any of your build scripts.
object Versions {
const val appCompat = "28.0.0"
const val constraintLayout = "1.1.3"
}
object Deps {
const val appCompat = "com.android.support:appcompat-v7:${Versions.appCompat}"
}
You can then use these in your app’s dependencies
block, or anywhere else you need access to global variables for your scripts.
// can reference directly
implementation(Deps.appCompat)
// can also use Kotlin's string concatenation
implementation("com.android.support.constraint:constraint-layout:${Versions.constraintLayout}")
buildTypes
block in app/build.gradle.kts
In Groovy, our buildTypes
block sets up our different build types like so:
buildTypes {
release {
...
}
debug {
...
}
}
It’s difficult to tell, but the general idea of what’s happening here is that it’s accessing a map of BuildType
s, which are mapped to String
keys. All Android projects have both release
and debug
build types by default, but you can also define custom-named build types here as well. Unfortunately, the Kotlin equivalent for this is a bit hidden: inside the buildTypes
block, the scope is a NamedDomainObjectContainer<BuildType>
, which (along with its parent classes) has several functions for accessing these string keys:
// from parent class NamedDomainObjectCollection
fun getByName(String name, Action<BuildType> configureAction): BuildType
// from NamedDomainObjectContainer
fun create(String name, Action<BuildType> configureAction): BuildType
fun maybeCreate(String name): BuildType
We can use getByName
if we already know the container holds a BuildType
with a given name – this is the case for release
and debug
. If we want to make a custom-named build type, we can use the create
method. However, the downside of both of these is that they can throw exceptions. maybeCreate
protects us from this – it will first look for a pre-existing build type with the name given as an argument, but if that doesn’t exist, it will create it. However, maybeCreate
doesn’t have an Action
parameter like the other two, so we’ll have to use our trusty apply
after we create the build type:
buildTypes {
maybeCreate("release").apply {
isMinifyEnabled = false
...
}
}
Note I also change the minifyEnabled = false
to be isMinifyEnabled
instead, since that’s the actual name of the variable in BuildType
.
In this Groovy line:
// before
implementation(fileTree(include: ["*.jar"], dir: "libs"))
we’re asking Gradle to include any jar files from the libs
directory by supplying them as a FileTree
. To build this FileTree
, we give the method a map of named properties, one of whose value is a single-element list. We’ll need to change this map and list to their Kotlin equivalents:
// after
implementation(fileTree(mapOf("include" to listOf("*.jar"), "dir" to "libs")))
At this point, Android Studio should be able to mostly parse your gradle scripts. However, any tasks that are still written in Groovy syntax will still be highlighted as incorrect. The following task is added in a new AS project by default:
task clean(type: Delete) {
delete rootProject.buildDir
}
Here, we’re registering a task called “clean” of type Delete
, which invokes the function Delete.delete(rootProject.buildDir)
when run. In Kotlin, we can write it like this instead:
tasks {
val clean by registering(Delete::class) {
delete(rootProject.buildDir)
}
}
We’ve completed the conversion of a set of Groovy Gradle scripts to Kotlin Gradle in an Android project!
As the Gradle Kotlin DSL version 1.0 was just released, there are still some unsupported features, and some minor bugs with Android Studio IDE support. However, the API is stable, and the converted script is able to build an Android project, along with improved support for code completion and editing hints from the IDE.
ConstraintLayout chains are great, but they only work for one row of items. What if you have too many items to fit on one...
So you've watched the CameraX introduction at Google I/O 2019 and you saw all the cool image manipulation and face detection implemented in the...