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.

instead of makes it easier to find. Help machines. Build systems need hints. The code in main/ should be assembled all the time, while test/ 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).

, — source sets. Include everything related to the code scope — like the production application ( ), unit tests ( ) and more ( 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;

— a set of source code files; SourceSet — a group of SourceDirectorySet s for Java code and resources.

The Gradle Kotlin for JVM plugin adds another one.

KotlinSourceSet — like SourceSet , but for Kotlin sources. Bonus — it configures src/main/kotlin and src/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 Gradle SourceDirectorySet ) — a set of source code files;

(mimics Gradle ) — a set of source code files; AndroidSourceSet (mimics Gradle SourceSet ) — a group of AndroidSourceDirectorySet 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.