Skip to content
← All posts

Android Plataforma · Part 15

Android Plataforma - Part 15: Taking care of your code with Detekt, ktlint and Spotless

By 6 min read Updated

In the last article we covered how our platform lets different modules opt into experimental features.

Now let’s look at how to safeguard code quality by integrating a few plugins.


Why bother automating code checks?

When you work as a team, having style and naming conventions is essential to stay consistent. Setting a solid standard reduces decision fatigue and makes collaboration easier.

Think of it this way: when you join an orchestra, you follow the conductor who sets the tempo for the music. It’s the same with our modules; we follow conventions that the team agreed on beforehand, applied automatically.

This is especially helpful when someone new joins the team, and it also keeps those agreements documented, codified, and open to collaboration.

Adding static code analysis with Detekt

Detekt is probably the most well-known tool in Kotlin for analyzing code and making sure certain practices are followed.

We won’t dwell too much on its features here; let’s go straight to the implementation.

Step by step

1 - Let’s start by declaring detekt in our libs.versions.toml:

[versions]
detekt = "1.23.1"
detektCompose = "0.2.3"

[libraries]
gradlePlugin-detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }

detektRules-compose = { module = "io.nlopez.compose.rules:detekt", version.ref = "detektCompose" }
detektRules-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
detektRules-libraries = { module = "io.gitlab.arturbosch.detekt:detekt-rules-libraries", version.ref = "detekt" }

[plugins]
arturbosch-detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }

2 - Sync the project. Head over to build-logic/build.gradle.kts and let’s compile the detekt dependency into our platform:

dependencies {
    compileOnly(libs.gradlePlugin.android)
    compileOnly(libs.gradlePlugin.kotlin)
    compileOnly(libs.gradlePlugin.detekt)
}

3 - Sync the project. Now let’s declare our DetektOptions DSL.

Create a DetektOptions file in build-logic/src/../options

data class DetektOptions(
    val parallel: Boolean,
    val buildUponDefaultConfig: Boolean,
    val configFileNames: List<String>,
    val includes: List<String>,
    val excludes: List<String>
)

class DetektOptionsBuilder {

    var parallel: Boolean = true
    var configFiles: List<String> = listOf(".detekt.yml, .detekt-compose.yml")
    var buildUponDefaultConfig: Boolean = true
    var includes: List<String> = listOf("**/*.kt", "**/*.kts")
    var excludes: List<String> = listOf(".*/resources/.*", ".*/build/.*")

    internal fun build(): DetektOptions = DetektOptions(
        parallel = parallel,
        configFileNames = configFiles,
        includes = includes,
        excludes = excludes,
        buildUponDefaultConfig = buildUponDefaultConfig
    )
}

4 - Next, create a new detekt.kt file in build-logic/src/.../decorations and declare an applyDetekt() function

This configuration enforces that:

  1. This plugin can only be called from the root build.gradle.kts
  2. A .detekt.yml file exists at the project root
  3. A .detekt-compose.yml file exists at the project root
import com.rsicarelli.kplatform.options.DetektOptions
import io.gitlab.arturbosch.detekt.Detekt
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
import org.gradle.api.Project
import org.gradle.api.artifacts.MinimalExternalModuleDependency
import org.gradle.kotlin.dsl.DependencyHandlerScope
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.withType

internal fun Project.applyDetekt(
    detektOptions: DetektOptions
) {
    check(rootProject == this) { "Must be called on a root project" }

    pluginManager.apply("io.gitlab.arturbosch.detekt")

    extensions.configure<DetektExtension> {
        parallel = detektOptions.parallel
        toolVersion = libs.version("detekt")
        buildUponDefaultConfig = detektOptions.buildUponDefaultConfig
        config.setFrom(detektOptions.configFileNames.map { "$rootDir/$it" })
    }

    tasks.withType<Detekt> {
        setSource(files(projectDir))
        include(detektOptions.includes)
        exclude(detektOptions.excludes)
    }

    addDetektPlugins(listOf("compose", "formatting", "libraries"))
}

fun Project.addDetektPlugins(detektPlugins: List<String>) {
    fun DependencyHandlerScope.detektPlugin(dependency: MinimalExternalModuleDependency) {
        add("detektPlugins", dependency)
    }

    dependencies {
        detektPlugins.forEach { plugin ->
            detektPlugin(libs.findLibrary("detektRules-$plugin").get().get())
        }
    }
}

5 - Next, let’s expose this decoration in KPlatformPlugin.kt:

fun Project.detekt(builderAction: DetektBuilder = {}) =
    applyDetekt(DetektOptionsBuilder().apply(builderAction).build())

6 - Sync the project. Next, go to the project’s root build.gradle.kts and include the detekt plugin:

plugins {
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.kotlin.android) apply false
    alias(libs.plugins.arturbosch.detekt) apply false
    id(libs.plugins.rsicarelli.kplatform.get().pluginId)
}

6 - Sync the project. Next, apply the detekt() decoration in the same file:

import com.rsicarelli.kplatform.detekt

plugins {
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.kotlin.android) apply false
    alias(libs.plugins.arturbosch.detekt) apply false
    id(libs.plugins.rsicarelli.kplatform.get().pluginId)
}

detekt()

7 - Let’s create 2 files at the project root: .detekt.yml and .detekt-compose.yml

8 - Sync the project. Notice that a number of detektX tasks were added to the project:

Gradle task list showing the detekt tasks

8 - Check that it’s working by running the following command.

Alternatively, you can simply double-click the detekt task in the Gradle task list:

./gradlew detekt

You’ll notice we get a bunch of violations.

Next, let’s use Spotless to help us shrink that list of issues.

Adding Spotless

Spotless is another indispensable tool in Kotlin projects.

Its job is to magically format your code according to a predefined code style/settings.

Again, we won’t go deep into the library’s details; let’s go straight to using it.

Step by step

1 - Declare the spotless coordinates in libs.versions.toml

[versions]
spotless = "6.21.0"

[libraries]
gradlePlugin-spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" }

[plugins]
diffplug-spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }

2 - Sync the project. Next, let’s create the SpotlessOptions files in the build-logic/src/.../options folder:

Here, our platform will be able to:

  1. Provide 2 default settings for the project: SpotlessKtsRule and SpotlessXmlRule. This configures Spotless for our Gradle files with the .kts extension, as well as Android .xml files.
  2. Allow other file settings, depending on what each project needs.
data class SpotlessOptions(
    val fileRules: List<SpotlessFileRule> = listOf(SpotlessKtRule, SpotlessXmlRule),
)

interface SpotlessFileRule {

    val fileExtension: String
    val targets: List<String>
    val excludes: List<String>
}

object SpotlessKtsRule : SpotlessFileRule {

    override val fileExtension: String = "kts"
    override val targets: List<String> = listOf("**/*.kts")
    override val excludes: List<String> = listOf("**/build/**/*.kts")
}

object SpotlessXmlRule : SpotlessFileRule {

    override val fileExtension: String = "xml"
    override val targets: List<String> = listOf("**/*.xml")
    override val excludes: List<String> = listOf("**/build/**/*.xml")
}

class SpotlessOptionsBuilder {

    var fileRules: List<SpotlessFileRule> = listOf(SpotlessKtRule, SpotlessXmlRule)

    internal fun build(): SpotlessOptions = SpotlessOptions(
        fileRules = fileRules
    )
}

3 - Let’s create a spotless.kt file inside build-logic/src/.../decorations and declare the applySpotless() function

Note that:

  1. We’re applying Spotless to the root project. This makes formatting also happen on the root scripts, as well as on the build-logic platform.
  2. We’re applying Spotless to all subprojects too.
  3. We’re using ktlint as the rules for Spotless.
  4. The plugin assumes there’s an .editorconfig file at the project root.
import com.diffplug.gradle.spotless.SpotlessExtension
import com.diffplug.gradle.spotless.SpotlessPlugin
import com.rsicarelli.kplatform.options.SpotlessOptions
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure

internal fun Project.applySpotless(spotlessConfig: SpotlessOptions) {
    val project = this

    configureSpotlessPlugin(spotlessConfig, project)

    rootProject.subprojects {
        configureSpotlessPlugin(spotlessConfig, project)
    }
}

private fun Project.configureSpotlessPlugin(
    spotlessConfig: SpotlessOptions,
    project: Project
) {
    apply<SpotlessPlugin>()

    extensions.configure<SpotlessExtension> {
        kotlin {
            target("src/**/*.kt")
            ktlint().setEditorConfigPath("${project.rootDir}/.editorconfig")
        }

        spotlessConfig.fileRules.forEach { spotlessFileRule ->
            with(spotlessFileRule) {
                format(fileExtension) {
                    target(targets)
                    targetExclude(excludes)
                }
            }
        }
    }
}

4 - Create an .editorconfig file at the project root:

5 - Let’s expose this decoration in KPlatformPlugin.kt:

fun Project.spotless(builderAction: SpotlessBuilder = { }) =
    applySpotless(SpotlessOptionsBuilder().apply(builderAction).build())

6 - Sync the project. Next, go to the project’s root build.gradle.kts and declare the spotless plugin:

plugins {
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.kotlin.android) apply false
    alias(libs.plugins.arturbosch.detekt) apply false
    alias(libs.plugins.diffplug.spotless) apply false
    id(libs.plugins.rsicarelli.kplatform.get().pluginId)
}

7 - Sync the project. Next, edit that same build.gradle.kts and apply the spotless() decoration:

import com.rsicarelli.kplatform.detekt
import com.rsicarelli.kplatform.spotless

plugins {
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.kotlin.android) apply false
    alias(libs.plugins.arturbosch.detekt) apply false
    alias(libs.plugins.diffplug.spotless) apply false
    id(libs.plugins.rsicarelli.kplatform.get().pluginId)
}

detekt()
spotless()

8 - Sync the project. Notice that several spotless tasks are now available in the Gradle task list:

Gradle task list showing the spotless tasks

9 - Check that it works by running the command, or double-click the spotlessApply task in the Gradle task list:

./gradlew spotlessApply

Success!

Spotless will fix a lot of the violations for us automatically. That said, there are a few, such as file naming, that Spotless doesn’t support.

While I was at it, in this branch I also added plenty of documentation for all of our platform APIs!

In the next article, we’ll wrap up this series of posts and share a bit about what’s coming next!