Este documento es un conjunto de reglas para crear API públicas en Java y Kotlin con la intención de que el código se sienta idiomático cuando se lo consuma desde el otro lenguaje.
Última actualización: 18 de mayo de 2018
Java (para consumo de Kotlin)
No uses palabras clave fijas
No utilices ninguna de las palabras clave fijas de Kotlin como nombre de métodos o campos, ya que requieren el uso de acentos graves como forma escapada cuando se las llama desde Kotlin. Se permiten palabras clave no fijas, palabras clave modificadoras e identificadores especiales.
Por ejemplo, la función when
de Mockito requiere un acento grave cuando se usa desde Kotlin:
val callable = Mockito.mock(Callable::class.java) Mockito.`when`(callable.call()).thenReturn(/* … */)
Evita usar nombres de extensión Any
Evita usar los nombres de las funciones de extensión de Any
para los métodos o los nombres de las propiedades de extensión de Any
para los campos, a menos que sea absolutamente necesario. Si bien los métodos y campos de miembros siempre tendrán prioridad sobre las funciones o propiedades de extensión de Any
, puede ser difícil saber a cuál se llama cuando se lee el código.
Anotaciones de nulabilidad
Cada uno de los tipos de campo, valores que se muestran y parámetros no primitivos de una API pública debe tener una anotación de nulabilidad. Los tipos no anotados se interpretan como tipos de "plataforma", que tienen nulabilidad ambigua.
De forma predeterminada, las marcas del compilador de Kotlin respetan las anotaciones de JSR 305, pero lo hacen con advertencias. También puedes configurar una marca para que el compilador trate las anotaciones como errores.
Los parámetros lambda van al final
Los tipos de parámetros aptos para la conversión de SAM deben ser los últimos.
Por ejemplo, la firma del método RxJava 2’s Flowable.create()
se define como:
public staticFlowable create( FlowableOnSubscribe source, BackpressureStrategy mode) { /* … */ }
Debido a que FlowableOnSubscribe es apto para la conversión de SAM, las llamadas de función de este método desde Kotlin se ven así:
Flowable.create({ /* … */ }, BackpressureStrategy.LATEST)
Sin embargo, si los parámetros se revirtieron en la firma del método, las llamadas a funciones podrían usar la sintaxis trailing-lambda:
Flowable.create(BackpressureStrategy.LATEST) { /* … */ }
Prefijos de propiedad
Para que se represente un método como una propiedad en Kotlin, se debe usar el prefijo estricto de estilo "bean".
Los métodos de descriptores de acceso requieren un prefijo "get", mientras que los métodos que muestran valores booleanos pueden usar el prefijo "is".
public final class User { public String getName() { /* … */ } public boolean isActive() { /* … */ } }
val name = user.name // Invokes user.getName() val active = user.isActive // Invokes user.isActive()
Los métodos de mutador asociados requieren el prefijo "set".
public final class User { public String getName() { /* … */ } public void setName(String name) { /* … */ } public boolean isActive() { /* … */ } public void setActive(boolean active) { /* … */ } }
user.name = "Bob" // Invokes user.setName(String) user.isActive = true // Invokes user.setActive(boolean)
Si deseas que se expongan los métodos como propiedades, no uses prefijos no estándares como "has" o "set" ni descriptores de acceso sin prefijo "get". Los métodos con prefijos no estándares aún se pueden llamar como funciones que pueden ser aceptables según el comportamiento del método.
Sobrecarga del operador
Ten en cuenta los nombres de métodos que permiten una sintaxis especial del sitio de llamada (es decir, la sobrecarga del operador) en Kotlin. Asegúrate de que tenga sentido usar los nombres de los métodos con la sintaxis abreviada.
public final class IntBox { private final int value; public IntBox(int value) { this.value = value; } public IntBox plus(IntBox other) { return new IntBox(value + other.value); } }
val one = IntBox(1) val two = IntBox(2) val three = one + two // Invokes one.plus(two)
Kotlin (para consumo de Java)
Nombre del archivo
Cuando un archivo contenga funciones o propiedades de nivel superior, siempre anótalo con @file:JvmName("Foo")
para proporcionar un buen nombre.
De forma predeterminada, los miembros de nivel superior de un archivo MyClass.kt terminarán en una clase llamada MyClassKt
, que no es atractiva y que filtra el lenguaje como un detalle de implementación.
Procura agregar @file:JvmMultifileClass
para combinar los miembros de nivel superior de múltiples archivos en una única clase.
Argumentos lambda
Los tipos de función que están destinados a ser utilizados desde Java deben evitar el tipo de datos Unit
que se muestra. Para ello, debes especificar una declaración return
Unit.INSTANCE;
explícita que no sea idiomática.
fun sayHi(callback: (String) -> Unit) = /* … */
// Kotlin caller: greeter.sayHi { Log.d("Greeting", "Hello, $it!") }
// Java caller: greeter.sayHi(name -> { Log.d("Greeting", "Hello, " + name + "!"); return Unit.INSTANCE; });
Esta sintaxis tampoco permite proporcionar un tipo con nombre semántico de modo que pueda implementarse en otros tipos.
Definir una interfaz con nombre y método abstracto único (SAM) en Kotlin para el tipo lambda corrige el problema para Java, pero evita que se use la sintaxis lambda en Kotlin.
interface GreeterCallback { fun greetName(name: String): Unit } fun sayHi(callback: GreeterCallback) = /* … */
// Kotlin caller: greeter.sayHi(object : GreeterCallback { override fun greetName(name: String) { Log.d("Greeting", "Hello, $name!") } })
// Java caller: greeter.sayHi(name -> Log.d("Greeting", "Hello, " + name + "!"))
La definición de una interfaz SAM con nombre en Java permite el uso de una versión ligeramente inferior de la sintaxis lambda de Kotlin en la que el tipo de interfaz debe especificarse de manera explícita.
// Defined in Java: interface GreeterCallback { void greetName(String name); }
fun sayHi(greeter: GreeterCallback) = /* … */
// Kotlin caller: greeter.sayHi(GreeterCallback { Log.d("Greeting", "Hello, $it!") })
// Java caller: greeter.sayHi(name -> Log.d("Greeter", "Hello, " + name + "!"));
En la actualidad, no hay forma de definir un tipo de parámetro para usar como lambda desde Java o Kotlin que se sienta idiomático en ambos lenguajes. La recomendación actual es priorizar el tipo de función, a pesar de la experiencia degradada de Java cuando el tipo de datos es Unit
.
Evita usar parámetros Nothing
genéricos
Un tipo cuyo parámetro genérico es Nothing
se expone como tipos sin formato en Java. Los tipos sin formato se usan con poca frecuencia en Java y se deben evitar.
Excepciones de documentos
Las funciones que pueden arrojar excepciones verificadas deben documentarse con @Throws
. Las excepciones de tiempo de ejecución deben estar documentadas en KDoc.
Ten en cuenta la función que se delega a las API, ya que pueden arrojar excepciones verificadas que Kotlin, de otra forma, permite propagar de forma silenciosa.
Copias defensivas
Cuando se muestren colecciones de solo lectura compartidas o sin propietario de las API públicas, únelas en un contenedor que no se pueda modificar o realiza una copia defensiva. Kotlin aplica su propiedad de solo lectura, pero esa opción no está disponible en Java. Sin el wrapper o la copia defensiva, es posible vulnerar objetos invariantes mostrando una referencia de colección permanente.
Funciones complementarias
Para exponer como un método estático las funciones públicas de un objeto complementario, hay que anotarlas con @JvmStatic
.
Sin la anotación, esas funciones solo están disponibles como métodos de instancia en un campo estático Companion
.
Incorrecto: Sin anotación
class KotlinClass { companion object { fun doWork() { /* … */ } } }
public final class JavaClass { public static void main(String... args) { KotlinClass.Companion.doWork(); } }
Correcto: Anotación @JvmStatic
class KotlinClass { companion object { @JvmStatic fun doWork() { /* … */ } } }
public final class JavaClass { public static void main(String... args) { KotlinClass.doWork(); } }
Constantes complementarias
Para exponer como un campo estático las propiedades públicas no const
que son constantes efectivas de un companion object
, hay que anotarlas con @JvmField
.
Sin la anotación, estas propiedades solo están disponibles como "métodos get" de instancias con nombres extraños en el campo Companion
estático. Si usas @JvmStatic
en lugar de @JvmField
, los "métodos get" con nombres extraños se convertirán en métodos estáticos de la clase, lo que sigue siendo incorrecto.
Incorrecto: Sin anotación
class KotlinClass { companion object { const val INTEGER_ONE = 1 val BIG_INTEGER_ONE = BigInteger.ONE } }
public final class JavaClass { public static void main(String... args) { System.out.println(KotlinClass.INTEGER_ONE); System.out.println(KotlinClass.Companion.getBIG_INTEGER_ONE()); } }
Incorrecto: Anotación @JvmStatic
class KotlinClass { companion object { const val INTEGER_ONE = 1 @JvmStatic val BIG_INTEGER_ONE = BigInteger.ONE } }
public final class JavaClass { public static void main(String... args) { System.out.println(KotlinClass.INTEGER_ONE); System.out.println(KotlinClass.getBIG_INTEGER_ONE()); } }
Correcto: Anotación @JvmField
class KotlinClass { companion object { const val INTEGER_ONE = 1 @JvmField val BIG_INTEGER_ONE = BigInteger.ONE } }
public final class JavaClass { public static void main(String... args) { System.out.println(KotlinClass.INTEGER_ONE); System.out.println(KotlinClass.BIG_INTEGER_ONE); } }
Nomenclatura idiomática
Kotlin tiene diferentes convenciones de llamadas respecto de Java que pueden cambiar la forma en que nombras funciones. Usa @JvmName
a fin de diseñar nombres que se sientan idiomáticos para las convenciones de ambos lenguajes o para que coincidan con las nomenclaturas de las bibliotecas estándares correspondientes.
Esto ocurre con mayor frecuencia para las funciones y propiedades de extensión porque la ubicación del tipo de receptor es diferente.
sealed class Optionaldata class Some (val value: T): Optional () object None : Optional () @JvmName("ofNullable") fun T?.asOptional() = if (this == null) None else Some(this)
// FROM KOTLIN: fun main(vararg args: String) { val nullableString: String? = "foo" val optionalString = nullableString.asOptional() }
// FROM JAVA: public static void main(String... args) { String nullableString = "Foo"; OptionaloptionalString = Optionals.ofNullable(nullableString); }
Sobrecargas de funciones predeterminadas
Las funciones con parámetros que tienen un valor predeterminado deben usar @JvmOverloads
.
Sin esta anotación, es imposible invocar la función con cualquier valor predeterminado.
Cuando uses @JvmOverloads
, inspecciona los métodos generados para asegurarte de que cada uno tenga sentido. De lo contrario, realiza una de las siguientes refactorizaciones, o ambas, hasta que estés satisfecho:
- Cambia el orden de los parámetros para que los que tengan valores predeterminados se dirijan hacia el final.
- Mueve los valores predeterminados a sobrecargas de funciones manuales.
Incorrecto: Sin @JvmOverloads
class Greeting { fun sayHello(prefix: String = "Mr.", name: String) { println("Hello, $prefix $name") } }
public class JavaClass { public static void main(String... args) { Greeting greeting = new Greeting(); greeting.sayHello("Mr.", "Bob"); } }
Correcto: Anotación @JvmOverloads
class Greeting { @JvmOverloads fun sayHello(prefix: String = "Mr.", name: String) { println("Hello, $prefix $name") } }
public class JavaClass { public static void main(String... args) { Greeting greeting = new Greeting(); greeting.sayHello("Bob"); } }
Verificaciones de Lint
Requisitos
- Versión de Android Studio: 3.2 Canary 10 o posteriores
- Versión del complemento de Gradle para Android: 3.2 o posteriores
Verificaciones compatibles
Ahora hay verificaciones de Android Lint que te ayudarán a detectar y marcar algunos de los problemas de interoperabilidad descritos con anterioridad. En este momento, solo se detectan problemas en Java (para consumo de Kotlin). Específicamente, las verificaciones compatibles son las siguientes:
- Nulabilidad desconocida
- Acceso a la propiedad
- No usar palabras clave fijas de Kotlin
- Los parámetros lambda van al final
Android Studio
Para habilitar estas verificaciones, ve a File > Preferences > Editor > Inspections y verifica las reglas que deseas habilitar en Kotlin Interoperability:
Figura 1: Configuración de interoperabilidad de Kotlin en Android Studio
Una vez que hayas verificado las reglas que deseas habilitar, se ejecutarán las nuevas verificaciones cuando ejecutes las inspecciones de tu código (Analyze > Inspect Code…)
Compilaciones de líneas de comandos
Para habilitar estas verificaciones desde las compilaciones de líneas de comandos, agrega la siguiente línea en tu archivo build.gradle
:
Groovy
android { ... lintOptions { enable 'Interoperability' } }
Kotlin
android { ... lintOptions { enable("Interoperability") } }
Para conocer el conjunto completo de configuraciones admitidas dentro de lintOptions, consulta la referencia Gradle DSL de Android.
A continuación, ejecuta ./gradlew lint
desde la línea de comandos.