What’s the motivation behind organizing the code? Two points come to mind.
- Help humans. Consistent environments are easier to understand and adapt.
Storing the source code in
src/
instead of_k_/
makes it easier to find. - Help machines. Build systems need hints. The code in
main/
should be assembled all the time, whiletest/
is test-specific and shouldn’t make it to a production environment.
Sounds empathic. Where do we start?
SDL
Maven introduced a concept of the Standard Directory Layout. Gradle tends to follow it bringing so-called source sets along the way. The following file system tree is SDL-compliant.
.
├── main
│ ├── java
│ │ └── Code.java
│ ├── kotlin
│ │ └── Kode.kt
│ └── resources
│ └── production.xml
└── test
├── java
│ └── CodeTests.java
├── kotlin
│ └── KodeTests.kt
└── resources
└── test.xml
main
,test
— source sets. Include everything related to the code scope — like the production application (main
), unit tests (test
) and more (androidTest
and friends).java
,kotlin
,resources
— source set implementation details. Unit tests can be written in Kotlin and Groovy at the same time, the production code might be a Java + Scala mix.
This two-level structure allows us to organize the code based on a functional target (production, tests) and on implementation details (language, tools). Let’s leverage this.
Tips
src/{sourceSet}/kotlin
Storing Kotlin files in the Kotlin-specific directory sounds obvious but a lot of projects are 100% Kotlin and store the source code as Java. Take a look at LeakCanary, Muzei, OkHttp, Scarlet, Timber, ViewPump, Workflow and more. I see a number of reasons behind this.
- The Kotlin compiler supports mixing Java and Kotlin code, so there is no punishment from the tooling.
- Projects migrate from Java to Kotlin using the mixing and forget to change the source set configuration.
- The Gradle Android plugin requires additional configuration which might be not trivial.
To be honest, there is nothing outright wrong with mixing Java and Kotlin code.
It’s more accurate and expectable to store them separately.
Also, it might help with Java → Kotlin migration efforts — it’s easier to observe
that the Java directory is shrinking and the Kotlin one is growing than
running cloc
all the time.
src/{sourceSet}/kotlinX
There is a common issue of organizing Kotlin extensions.
I’ve seen a lot of projects with the Extensions.kt
garbage fire. When everything is in
a single file — it’s easier to overlook an extension and write a new one placed at…
extensions/Extensions.kt
. Guess what happens next.
I suggest storing extensions using the target class package and file names.
Plus — move them to the kotlinX/
directory
as a separation of the project code from additions to the external one. This approach leads
to a better separation of concerns.
For example, the following io.reactivex.functions.Consumer
extension should be placed at
src/main/kotlinX/io/reactivex/functions/Consumer.kt
.
package io.reactivex.functions
fun Consumer<Unit>.asAction() = Action { accept(Unit) }
Bonus — imports start to make sense.
- import hello.there.asAction
+ import io.reactivex.functions.asAction
src/testFixtures/kotlin
A growing test / specification suite might be not pleasant to look at. Using fakes is great but there is a possibility of having a huge file tree with mixed tests and fakes.
.
└── src
└── test
└── kotlin
├── ApplicationSpec.kt
├── FakeApplication.kt
├── FakePermissions.kt
└── PermissionsSpec.kt
Since fakes and tests are different things — I suggest to split them in the digital world as well.
.
└── src
├── test
│ └── kotlin
│ ├── ApplicationSpec.kt
│ └── PermissionsSpec.kt
└── testFixtures
└── kotlin
├── FakeApplication.kt
└── FakePermissions.kt
In fact, Gradle supports this approach for the Java code
and with benefits — it’s possible to share testFixtures
across modules.
However, it doesn’t work with Gradle Kotlin
and Android plugins.
Gradle API
The code below will use the Gradle Kotlin DSL but it can be adapted to the Groovy DSL as well. The code was run against Gradle 6.1.1, Gradle Kotlin plugin 1.3.61 and Gradle Android plugin 3.5.3.
JVM
Gradle uses a couple of classes as an API to configure the source code location:
SourceDirectorySet
— a set of source code files;SourceSet
— a group ofSourceDirectorySet
s for Java code and resources.
The Gradle Kotlin for JVM plugin adds another one.
KotlinSourceSet
— likeSourceSet
, but for Kotlin sources. Bonus — it configuressrc/main/kotlin
andsrc/test/kotlin
automatically.
The DSL works with those classes.
-
Single module (put in the module
build.gradle.kts
file).import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet // Get a SourceSet collection sourceSets { // Get a SourceSet by name named("source set name") { // Resolve a KotlinSourceSet withConvention(KotlinSourceSet::class) { // Configure Kotlin SourceDirectorySet kotlin.srcDirs("path A", "path B", "path C") } } }
-
Multiple modules (put in the root
build.gradle.kts
file).import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet subprojects { // The sourceSets function is not available at root so we use a different syntax configure<SourceSetContainer> { named("source set name") { withConvention(KotlinSourceSet::class) { kotlin.srcDirs("path A", "path B", "path C") } } } }
Android
Gradle Android plugin ignores native Gradle source set infrastructure and introduces its own. To be fair, the Android API tries to mimic the Gradle one, so I suspect the reinvention was done for a reason.
AndroidSourceDirectorySet
(mimics GradleSourceDirectorySet
) — a set of source code files;AndroidSourceSet
(mimics GradleSourceSet
) — a group ofAndroidSourceDirectorySet
s for Java code and resources, Android resources, assets, AIDL, RenderScript files and more.
The Gradle Kotlin for Android plugin doesn’t provide a KotlinAndroidSourceSet
(like KotlinSourceSet
for JVM). Fortunately enough we can use the Java AndroidSourceSet
instead
(thanks to mixing).
The DSL is similar to the JVM one.
-
Single module (put in the module
build.gradle.kts
file).android { // Get an AndroidSourceSet collection sourceSets { // Get an AndroidSourceSet by name named("source set name") { // Configure Java AndroidSourceDirectorySet java.srcDirs("path A", "path B", "path C") } } }
-
Multiple modules (put in the root
build.gradle.kts
file).import com.android.build.gradle.AppPlugin import com.android.build.gradle.BaseExtension import com.android.build.gradle.LibraryPlugin subprojects { // Since the API comes from a plugin we have to wait for it plugins.matching { it is AppPlugin || it is LibraryPlugin }.whenPluginAdded { // The android function is not available at root so we use a different syntax configure<BaseExtension> { sourceSets { named("source set name") { java.srcDirs("path A", "path B", "path C") } } } } }
Gradle Implementation
Nice, we can use the Gradle API to apply our tips! Snippets below are DSL declarations that can be used in both single and multiple module configurations described above.
JVM
named("main") {
withConvention(KotlinSourceSet::class) {
// Gradle Kotlin for JVM plugin configures "src/main/kotlin" on its own
kotlin.srcDirs("src/main/kotlinX")
}
}
named("test") {
withConvention(KotlinSourceSet::class) {
// Gradle Kotlin for JVM plugin configures "src/test/kotlin" on its own
kotlin.srcDirs("src/test/kotlinX", "src/testFixtures/kotlin")
}
}
Android
named("main") {
java.srcDirs("src/main/kotlin", "src/main/kotlinX")
}
named("test") {
java.srcDirs("src/test/kotlin", "src/test/kotlinX", "src/testFixtures/kotlin")
}
Next?
Don’t afraid to configure source sets — think about what can be done better and adapt. The Gradle API might be not intuitive at first glance — especially when Kotlin and Android are brought in the mix — but almost everything can be achieved.