Skip to content

Kotlin code style

Yevhenii Nadtochii edited this page Feb 25, 2025 · 10 revisions

Kotlin code of Spine SDK follows the standard conventions with the extensions and modifications described below.

Property names for constants

Kotlin conventions for property names encourage using SCREAMING_SNAKE_CASE for constant properties.

Unlike in Kotlin conventions, we prefer lowerCamelCase for naming such properties for the following reasons.

1. Better readability

Compare "$group:$infix-fat-cli:$version" and "$GROUP:$INFIX-fat-cli:$VERSION".

Uppercase constants usually attract more attention than real interesting text around them.
There's no need to SCREAM about them ALL_THE_TIME.

2. Flexibility to changes

This is more important than №1 above.

Suppose you have a dependency defined via constant that end up with interpolated string such as "$GROUP:$INFIX-fat-cli:$VERSION".
The dependency is used in several modules of your project.

After some time, you figure out that the version to be used depends on some condition.
So VERSION is no longer a const val but simply val.
So, by standard convention, you now need to rename it.
In turn, the constant which previously defined the dependency is also no longer a constant because its value is interpolated from non-constant.
Now this property also has to be renamed. A slight extension of logic resulted in a cascade of changes.

If we do not SCREAM about constants, we hide the implementation details (at this micro level), making our code is less "fragile".

3. Consistency

Consider this code:

public object ProtoData {
    private const val VERSION: String = "1.0.1"
    public const val GROUP: String = "io.spine.protodata"
    internal const val INFIX: String = "protodata"
    //...
    public const val COORDINATES: String = "$GROUP:$INFIX-cli:$VERSION"
}

public data class ProtocPluginArtifact {
    private const val VERSION: String = "1.0.2"
    public val coordinates: String = "${ProtoData.GROUP}:${ProtoData.INFIX}-protoc:$VERSION:exe@jar"
}

The ProtoData.COORDINATES property is a constant, while ProtocPluginArtifact.coordinates is not because.
It could not be declared as const val because interpolated from constant (!) properties of another object, ProtoData.

In order to reduce the mental load on remembering if a property is a real constant or not, we relaxed the constant value name rule.

It is still make sense to follow the SCREAMING_CASE rule for cases related to performance optimization.

Dependency objects

Instead of Gradle version catalogs we declare dependencies as Kotlin objects.

The reasoning behind this approach and our plans

We started using dependency objects in order to avoid string-based mess when when wording with dependencies. Then Version catalog feature appeared in Gradle, and were in incubation state for some time. Gradle started promoting this feature more recently.

Assuming the amount of planned work for the production code of Spine SDK, we don't see much benefits in migrating to Gradle Version Catalogs in the near future. Still, it should be done to reduce the cognitive load on new developers of the SDK.

See the io.spine.internal.dependency package under buildSrc of a Spine SDK subproject for details.

Naming properties of dependency objects

Versions and Maven coordinates of dependencies are defined using lowerCamelCase.

Extension functions and properties

Kotlin provides a powerful way of teaching existing code new tricks.
Here are basic principles regarding extensions to follow in our code:

  1. Make extensions internal or even private if you are not sure they are going to be popular outside of the module you work on.
    We can always promote it later.
    We do not want to introduce much of naïve extension noise.

  2. If your extensions are really public, gather them in separate files.
    They are easier to find this way.

  3. Name the public extension files after this pattern: <TypeOrTypes>Exts.kt.
    For example, FileTypesExts.kt, CharSequenceExts.kt.
    The Exts stands for Extensions, but is shorter, easier to read and type, and almost equally understood.

Formatting String literals

Concatenated strings

When concatenating a long string, start each continuation line with a space character:

const val text = "This is an important error message." +
        " Here are some details regarding what happened." +
        " See the documentation for more information."

It's easier to spot and understand as a continuation. Oftentimes, people miss a space character in concatenated messages. With the starting space, it happens less often.