Guía de estilo de Kotlin


Ahora que estamos empezando a escribir nuestras primeras líneas de código con Kotlin, y antes de coger malos hábitos, es buen momento para conocer las convenciones sobre las buenas prácticas y los estándares de codificación. Vamos a repasar algunas de las más importantes.

Archivos

Todos los archivos fuente deben estar codificados como UTF-8.

El nombre de los archivos fuentes escritos con Kotlin deben escribirse en formato CamelCase (y más específicamente PascalCase que significa que la primera letra de cada una de las palabras se escribe en mayúscula) con la extensión kt.

Si el archivo contiene una sola clase, su nombre debe ser el mismo que el nombre de la clase. Si un archivo contiene varias clases, o solo declaraciones de nivel superior, el nombre del archivo debe describir su contenido, esto es, lo que hace el código en el archivo. Algunos ejemplos:
// MyClass.kt
class MyClass { }

// Bar.kt
class Bar { }
fun Runnable.toBar(): Bar = // …

// Map.kt
fun <T, O> Set<T>.map(func: (T) -> O): List<O> = // …
fun <T, O> List<T>.map(func: (T) -> O): List<O> = // …

Estructura

Un archivo .kt comprende lo siguiente, en este orden y con una línea en blanco separando cada una de estas secciones:
  1. Copyright y / o encabezado de licencia (opcional)
  2. Anotaciones del archivo
  3. Declaración del paquete
  4. Declaraciones Import
  5. Declaraciones de alto nivel
La sección de Copyright o licencia debe seguir este formato:
/*
 * Copyright 2017 Google, Inc.
 *
 * ...
 */
Las instrucciones de importación para clases, funciones y propiedades se agrupan en una sola lista y se ordenan alfabéticamente, no permitiéndose las importaciones de comodines (de cualquier tipo).

Bloques con llaves {}

En los bloques when y if no se requieren llaves cuando no se bifurcan con else if/else y cuando ocupan una sola línea. De lo contrario, se requieren llaves para cualquiera de las sentencias if, for, when, do, y while.

Las llaves siguen el siguiente estilo (estilo K&R, por Kernighan and Ritchie):
  • No hay salto de línea antes de la llave de apertura.
  • Salto de línea después de la llave de apertura, incluso en los bloques vacíos.
  • Salto de línea antes de la llave de cierre.
  • Salto de línea después de la llave de cierre solo si esa llave termina una declaración o termina el cuerpo de una función, un constructor o una clase con nombre. Por ejemplo, no hay un salto de línea después de la llave si es seguida por un else o una coma.
return Runnable {
    while (condition()) {
        foo()
    }
}

return object : MyClass() {
    override fun foo() {
        if (condition()) {
            try {
                something()
            } catch (e: ProblemException) {
                recover()
            }
        } else if (otherCondition()) {
            somethingElse()
        } else {
            lastThing()
        }
    }
}
Una excepción a esto es la clase enum, que cuando no tiene funciones ni constantes, opcionalmente puede presentarse en una sola línea:
enum class Respuesta { SI, NO, QUIZA }
Por su parte, un condicional if / else puede omitir llaves solo si la expresión completa se ajusta a una línea:
val value = if (string.isEmpty()) 0 else 1  // Okay

Identación y líneas

Con cada nuevo bloque la sangría aumenta en cuatro espacios, y cuando el bloque finaliza la sangría vuelve al nivel previo. Esto se aplica tanto al código como a los comentarios.

Cada declaración va seguida de un salto de línea, sin utilizar punto y coma.

El código tiene un límite de ancho máximo de 100 caracteres. Cuando una línea excede este límite debe ajustarse teniendo en cuenta que la directiva principal para el ajuste de líneas recomienda romper la línea en un nivel sintáctico superior. Hay que tener en cuenta que el objetivo principal del ajuste de línea es tener un código claro, no necesariamente un código que se ajuste al menor número de líneas.

Otras directrices respecto al ajuste de línea:
  • Cuando se rompe una línea en un operador sin asignación, la ruptura aparece antes del símbolo. Esto también se aplica al punto (.) y a los dos puntos (::) que hacen referencia a un miembro.
  • Cuando se rompe una línea en un operador de asignación, la ruptura viene después del símbolo.
  • Un nombre de método o constructor siempre permanece adjunto al paréntesis abierto que lo sigue.
  • Una coma siempre permanece unida a la palabra que la precede.
  • Una flecha lambda (->) siempre permanece unida a la lista de argumentos que la precede.
  • Cuando los parámetros de una función no caben en una sola línea, cada uno debe aparecer en su propia línea con una única sangría (+4). En estos casos, el paréntesis de cierre y el tipo de retorno se colocan en su propia línea sin sangría adicional:
fun <T> Iterable<T>.joinToString(
    separator: CharSequence = ", ",
    prefix: CharSequence = "",
    postfix: CharSequence = ""
): String {
    // …
}
  • Cuando una función contiene una sola expresión, se puede representar como función de una expresión en una línea:
override fun toString(): String {
    return "Hey"
}

override fun toString(): String = "Hey"
  • La única vez que una función de expresión debe ajustarse a varias líneas es cuando abre un bloque.
fun main() = runBlocking {
  // …
}
No obstante, si esa función de expresión crece hasta requerir un ajuste de línea, es preferible utilizar en su lugar un cuerpo de función normal.

Respecto a las propiedades, cuando un inicializador de una propiedad no entra en una sola línea, se debe romper después del signo igual y con sangría. Además, las propiedades que declaran una función get o set se deben colocar cada una en su propia línea con una sangría normal (+4), aunque las propiedades de solo lectura pueden usar una sintaxis más corta que se ajusta a una sola línea. Algunos ejemplos sobre el ajuste de línea en las propiedades:
private val defaultCharset: Charset? =
    EncodingRegistry.getInstance().getDefaultCharsetForPropertiesFiles(file)

var directory: File? = null
    set(value) {
        // …
    }

val defaultExtension: String get() = "kt"

Espacios en blanco

Se deben dejar una línea en blanco (nunca se recomienda más de una) entre miembros consecutivos de una clase (propiedades, constructores, funciones, clases anidadas, etc.) con dos excepciones: entre dos propiedades consecutivas (espacio opcional para para crear agrupaciones lógicas de propiedades asociadas) y entre las constantes de una clase enum.

También se debe dejar una línea en blanco entre las declaraciones según sea necesario para organizar el código en subsecciones lógicas (y opcionalmente antes de la primera declaración en una función, antes del primer miembro de una clase o después del último miembro de una clase).

A las clases enum se les aplica estas reglas como a cualquier clase, teniendo en cuenta que cuando las constantes se colocan en líneas separadas no se requiere una línea en blanco entre ellas excepto en el caso de que definan un cuerpo o bloque:
enum class Answer {
    YES,
    NO,

    MAYBE {
        override fun toString() = """¯\_(ツ)_/¯"""
    }
}
En cuanto a los espacios en blanco horizontales, deben aparecer separando cualquier palabra reservada como if, for, o catch del paréntesis abierto que le sigue en esa línea:
// MAL
for(i in 0..1) {
}

// BIEN
for (i in 0..1) {
}
También deben separar cualquier palabra reservada como else o catch de la llave de cierre que la precede en esa línea:
// MAL
}else {
}

// BIEN
} else {
}
El espacio en blanco también debe aparecer antes de cualquier llave abierta y a ambos lados de cualquier operador binario. Esto también se aplica a la flecha en una expresión lambda pero no a los dobles dos puntos (::) de referencia de un miembro ni al punto separador ni al operador de rango (...):
// MAL
ints.map { value->value.toString() }

val toString = Any :: toString

it . toString()

for (i in 1 .. 4) print(i)

// BIEN
ints.map { value -> value.toString() }

val toString = Any::toString

it.toString()

for (i in 1..4) print(i)
También se usará este espacio en blanco antes de dos puntos (:) solo si se usa en una declaración de clase para especificar una clase base o interfaz, o cuando se usa en una cláusula where para restricciones genéricas.
// MAL
class Foo: Runnable

fun <T: Comparable> max(a: T, b: T)

fun <T> max(a: T, b: T) where T: Comparable<T>


// BIEN
class Foo : Runnable

fun <T : Comparable> max(a: T, b: T)

fun <T> max(a: T, b: T) where T : Comparable<T>
Otros usos del espacio en blanco son después de una coma, de dos puntos (:) y a ambos lados de la barra diagonal doble (//) que comienza un comentario de final de línea (en este último caso se permiten múltiples espacios, pero no son obligatorios).
// MAL
val oneAndTwo = listOf(1,2)

class Foo :Runnable

var debugging = false//disabled by default

// BIEN
val oneAndTwo = listOf(1, 2)

class Foo : Runnable

var debugging = false // disabled by default

Omisión de tipos implícitos

Cuando el inicializador de una propiedad o variable define claramente su tipo de dato, o cuando se puede inferir claramente por el cuerpo de una función de expresión el tipo de retorno, entonces éste se puede omitir.
private val ICON: Icon = IconLoader.getIcon("/icons/kotlin.png")
// puede ser:
private val ICON = IconLoader.getIcon("/icons/kotlin.png")

override fun toString(): String = "Hey"
// puede ser:
override fun toString() = "Hey"

Nombres

Los identificadores solo usan letras y dígitos ASCII y, en un pequeño número de casos, pueden utilizar guiones bajos (como las propiedades de respaldo cuyo nombre debe coincidir exactamente con el de la propiedad real precedido por un guion bajo).

Para nombrar los paquetes solo se usan minúsculas con palabras concatenadas.
// MAL
package com.example.deepSpace
package com.example.deep_space

// BIEN
package com.example.deepspace
Para las clases se utiliza el formato PascalCase y suelen ser sustantivos, al igual que las interfaces aunque estas últimas a veces pueden ser adjetivos.

Los nombres de las funciones están escritos en camelCase y suelen ser verbos, por ejemplo, enviarMensaje (se permite el uso de guiones bajos en el nombre de funciones de prueba).

Los nombres de las constantes (típicamente nombres) usan UPPER_SNAKE_CASE: todas las letras en mayúscula con palabras separadas por guiones bajos. A estos efectos se consideran constantes las propiedades val marcadas con cons (ver la entrada Variables en Kotlin) y las que, sin ser cons, no pueden modificar su valor por efecto de otras funciones, como las colecciones inmutables de tipos inmutables (no es suficiente el hecho de no mutar nunca).
const val NUMBER = 5
val NAMES = listOf("Alice", "Bob")
val AGES = mapOf("Alice" to 35, "Bob" to 32)
val COMMA_JOINER = Joiner.on(',') // Joiner is immutable
val EMPTY_ARRAY = arrayOf()
Por su parte, los nombres mutables se escriben en formato camelCase y se aplican a las propiedades de instancia, propiedades locales y nombres de parámetros.
val variable = "var"
val nonConstScalar = "non-const"
val mutableCollection: MutableSet = HashSet()
val mutableElements = listOf(mutableInstance)
val mutableValues = mapOf("Alice" to mutableInstance, "Bob" to mutableInstance2)
val logger = Logger.getLogger(MyClass::class.java.name)
val nonEmptyArray = arrayOf("these", "can", "change")
Los tipos de variables suelen ser nombrados con una sola letra en mayúscula o bien por una palabra que empieza en mayúscula seguida de una T mayúscula (RequestT).

Por último, hay que tener en cuenta que el IDE IntelliJ IDEA facilita el formateo automático del código de acuerdo a la guía oficial de estilo, y que se puede configurar desde File -> Settings -> Editor -> Code Style -> Kotlin y clic en el enlace "Set from..." que aparece en la esquina superior derecha, y en el menú desplegado seleccionar Predefined style -> Kotlin style guide. Aplicar cambios y OK. Para verificar que el código está formateado de acuerdo con la guía de estilo, hay que habilitar la opción desde File -> Settings -> Editor -> Inspections -> Kotlin -> Style issues -> File is not formatted according to project settings.

Puedes ampliar la información relativa a las convenciones de codificación con Kotlin en las siguientes fuentes (en inglés):

Comentarios

Entradas populares

Recursos gratis para aprender Kotlin

I/O: entrada y salida de datos en consola

Lectura y escritura de archivos