Cambiar a Kotlin en un proyecto de Android: consejos y trucos


Publicado por Sergey Yeshin, Desarrollador de Android Strong Middle, DataArt

Ha pasado más de un año y medio desde que Google anunció el soporte oficial para Kotlin en Android, y los desarrolladores más experimentados comenzaron a experimentar con él en su combate y no en proyectos de hace más de tres años.

El nuevo idioma fue recibido calurosamente en la comunidad de Android, y la gran mayoría de los nuevos proyectos de Android comenzarán con Kotlin a bordo. También es importante que Kotlin compile en un código de bytes JVM, por lo tanto, es totalmente compatible con Java. Entonces, en los proyectos de Android existentes escritos en Java, también existe la oportunidad (además, una necesidad) de usar todas las características de Kotlin, gracias a lo cual ganó tantos admiradores.

En el artículo, hablaré sobre la experiencia de migrar una aplicación de Android de Java a Kotlin, las dificultades que tuvieron que superarse en el proceso, y explicaré por qué todo esto no fue en vano. El artículo está más dirigido a los desarrolladores de Android que recién comienzan a aprender Kotlin y, además de la experiencia personal, se basa en los materiales de otros miembros de la comunidad.

¿Por qué Kotlin?


Describa brevemente las características de Kotlin, por lo que cambié a él en el proyecto, dejando el mundo Java "cómodo y dolorosamente familiar":

  1. Compatibilidad completa con Java
  2. Seguridad nula
  3. Inferencia de tipos
  4. Métodos de extensión
  5. Funciona como objetos de primera clase y lambda.
  6. Genéricos
  7. Corutinas
  8. Sin excepción marcada

Aplicación DISCO


Esta es una aplicación de tamaño pequeño para el intercambio de tarjetas de descuento, que consta de 10 pantallas. Usando su ejemplo, consideraremos la migración.

Brevemente sobre arquitectura


La aplicación utiliza la arquitectura MVVM con componentes de arquitectura de Google bajo el capó: ViewModel, LiveData, Room.


Además, de acuerdo con los principios de Clean Architecture de Uncle Bob, seleccioné 3 capas en la aplicación: datos, dominio y presentación.


Por donde empezar Entonces, imaginamos las características principales de Kotlin y tenemos una idea mínima del proyecto que debe migrarse. La pregunta natural es "¿por dónde empezar?".

La página oficial de documentación de Android Comenzando con Kotlin dice que si desea portar una aplicación existente a Kotlin, solo tiene que comenzar a escribir pruebas unitarias. Cuando adquiera un poco de experiencia con este lenguaje, escriba un nuevo código en Kotlin, solo necesita convertir el código Java existente.

Pero hay un "pero". De hecho, una conversión simple generalmente (aunque no siempre) le permite obtener un código de trabajo en Kotlin, sin embargo, su modismo deja mucho que desear. Además, le diré cómo eliminar esta brecha debido a las características mencionadas (y no solo) del lenguaje Kotlin.

Migración de capa


Como la aplicación ya está en capas, tiene sentido migrar por capas, comenzando desde la parte superior.

La secuencia de capas durante la migración se muestra en la siguiente imagen:


No es casualidad que comenzamos la migración precisamente desde la capa superior. De este modo nos ahorramos el uso del código Kotlin en el código Java. Por el contrario, hacemos que el código de Kotlin de la capa superior use las clases Java de la capa inferior. El hecho es que Kotlin se diseñó originalmente teniendo en cuenta la necesidad de interactuar con Java. El código Java existente puede llamarse desde Kotlin de forma natural. Podemos heredar fácilmente de las clases Java existentes, acceder a ellas y aplicar anotaciones Java a las clases y métodos de Kotlin. El código de Kotlin también se puede usar en Java sin demasiados problemas, pero a menudo requiere un esfuerzo adicional, como agregar una anotación JVM. ¿Y por qué hacer conversiones innecesarias en el código Java, si al final todavía se reescribirá en Kotlin?

Por ejemplo, veamos la generación de sobrecarga.

Por lo general, si escribe una función de Kotlin con valores de parámetros predeterminados, solo será visible en Java como una firma completa con todos los parámetros. Si desea proporcionar múltiples sobrecargas a las llamadas Java, puede usar la anotación @JvmOverloads:

class Foo @JvmOverloads constructor(x: Int, y: Double = 0.0) { @JvmOverloads fun f(a: String, b: Int = 0, c: String = "abc") { ... } } 

Para cada parámetro con un valor predeterminado, esto creará una sobrecarga adicional, que tiene este parámetro y todos los parámetros a la derecha, en la lista de parámetros remotos. En este ejemplo, se creará lo siguiente:

 // Constructors: Foo(int x, double y) Foo(int x) // Methods void f(String a, int b, String c) { } void f(String a, int b) { } void f(String a) { } 

Hay muchos ejemplos del uso de anotaciones JVM para el correcto funcionamiento de Kotlin. Esta página de documentación detalla la llamada a Kotlin desde Java.

Ahora describimos el proceso de migración capa por capa.

Capa de presentación


Esta es una capa de interfaz de usuario que contiene pantallas con vistas y un ViewModel, que a su vez contiene propiedades en forma de LiveData con datos del modelo. A continuación, observamos los trucos y herramientas que resultaron útiles al migrar esta capa de aplicación.

1. Procesador de anotaciones Kapt


Al igual que con cualquier MVVM, View está vinculado a las propiedades de ViewModel a través del enlace de datos. En el caso de Android, estamos tratando con la Biblioteca de enlace de datos de Android, que utiliza el procesamiento de anotaciones. Entonces Kotlin tiene su propio procesador de anotaciones , y si no realiza cambios en el archivo build.gradle correspondiente, el proyecto dejará de compilarse. Por lo tanto, haremos estos cambios:

 apply plugin: 'kotlin-kapt' android { dataBinding { enabled = true } } dependencies { api fileTree(dir: 'libs', include: ['*.jar']) ///… kapt "com.android.databinding:compiler:$android_plugin_version" } 

Es importante recordar que debe reemplazar por completo todas las apariciones de la configuración de AnnotationProcessor en su build.gradle con kapt.

Por ejemplo, si usa las bibliotecas Dagger o Room en el proyecto, que también usan el procesador de anotaciones bajo el capó para la generación de código, debe especificar kapt como el procesador de anotaciones.

2. Funciones en línea


Al marcar una función como en línea, le pedimos al compilador que la coloque en el lugar de uso. El cuerpo de la función se incrusta, en otras palabras, se sustituye por el uso habitual de la función. Gracias a esto, podemos eludir la restricción de borrado de tipo, es decir, borrar un tipo. Al usar funciones en línea, podemos obtener el tipo (clase) en tiempo de ejecución.

Esta característica de Kotlin se usó en mi código para "extraer" la clase de la Actividad lanzada.

 inline fun <reified T : Activity> Context?.startActivity(args: Bundle) { this?.let { val intent = Intent(this, T::class.java) intent.putExtras(args) it.startActivity(intent) } } 

reified: designación de un tipo reified.

En el ejemplo descrito anteriormente, también mencionamos una característica del lenguaje Kotlin como Extensiones.

3. Extensiones


Son extensiones. Los métodos de utilidad se eliminaron en extensiones, lo que ayudó a evitar utilidades de clase hinchadas y monstruosas.

Daré un ejemplo de las extensiones involucradas en la aplicación:

 fun Context.inflate(res: Int, parent: ViewGroup? = null): View { return LayoutInflater.from(this).inflate(res, parent, false) } fun <T> Collection<T>?.isNotNullOrEmpty(): Boolean { return this != null && isNotEmpty(); } fun Fragment.hideKeyboard() { view?.let { hideKeyboard(activity, it.windowToken) } } 

Los desarrolladores de Kotlin pensaron de antemano en útiles extensiones de Android al ofrecer su complemento Kotlin Android Extensions. Entre las características que ofrece se encuentran el enlace de visualización y el soporte parcelable. Puede encontrar información detallada sobre las características de este complemento aquí .

4. Funciones Lambda y funciones de orden superior.


Usando las funciones lambda en el código de Android, puede deshacerse del torpe ClickListener y la devolución de llamada, que en Java se implementaron a través de interfaces autoescritas.

Un ejemplo de uso de una lambda en lugar de onClickListener:

 button.setOnClickListener({ doSomething() }) 

Las lambdas también se utilizan en funciones de orden superior, por ejemplo, para funciones de recopilación.

Tome el mapa como ejemplo:

 fun <T, R> List<T>.map(transform: (T) -> R): List<R> {...} 

Hay un lugar en mi código donde necesito "mapear" la identificación de las tarjetas para su posterior eliminación.

Usando la expresión lambda pasada al mapa, obtengo la matriz de id deseada:

  val ids = cards.map { it.id }.toIntArray() cardDao.deleteCardsByIds(ids) 

Tenga en cuenta que los paréntesis se pueden omitir cuando se llama a la función, si lambda es el único argumento, y la palabra clave es el nombre implícito del único parámetro.

5. Tipos de plataforma


Inevitablemente, tendrá que trabajar con los SDK escritos en Java (incluido, de hecho, el SDK de Android). Esto significa que siempre debe estar en guardia con Kotlin y Java Interop, como los tipos de plataforma.

Un tipo de plataforma es un tipo para el que Kotlin no puede encontrar información de validez nula. El hecho es que, de forma predeterminada, el código Java no contiene información sobre la validez de nulo, y las anotaciones NotNull y @ Nullable no siempre se usan. Cuando no hay una anotación correspondiente en Java, el tipo se convierte en plataforma. Puede trabajar con él como un tipo que permite nulo y como un tipo que no permite nulo.


Esto significa que, al igual que en Java, el desarrollador es totalmente responsable de las operaciones con este tipo. El compilador no agrega un tiempo de ejecución de verificación nula y le permitirá hacer todo.

En el siguiente ejemplo, anulamos onActivityResult en nuestra actividad:

 override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent{ super.onActivityResult(requestCode, resultCode, data) val randomString = data.getStringExtra("some_string") } 

En este caso, los datos son un tipo de plataforma que puede contener nulo. Sin embargo, desde el punto de vista del código de Kotlin, los datos no pueden ser nulos bajo ninguna circunstancia, e independientemente de si especifica el tipo de Intento como anulable, no recibirá una advertencia o error del compilador, ya que ambas versiones de la firma son válidas . Pero dado que recibir datos no vacíos no está garantizado, porque en casos con el SDK no puede controlar esto, obtener nulo en este caso conducirá a NPE.

Además, como ejemplo, podemos enumerar los siguientes lugares de la posible aparición de tipos de plataforma:

  1. Service.onStartCommand (), donde la intención puede ser nula.
  2. BroadcastReceiver.onReceive ().
  3. Activity.onCreate (), Fragment.onViewCreate () y otros métodos similares.

Además, sucede que los parámetros del método están anotados, pero por alguna razón el estudio pierde Nullability al generar una anulación.

Capa de dominio


Esta capa incluye toda la lógica de negocios; es responsable de la interacción entre la capa de datos y la capa de presentación. El papel clave aquí lo desempeña el repositorio. En Repository, llevamos a cabo las manipulaciones de datos necesarias, tanto del lado del servidor como locales. Arriba, a la capa Presentación, solo damos el método de interfaz de Repositorio, que oculta la complejidad de las operaciones de datos.

Como se indicó anteriormente, RxJava se utilizó para la implementación.

1. RxJava


Kotlin es totalmente compatible con RxJava y más conciso junto con él que Java. Sin embargo, incluso aquí tuve que enfrentar un problema desagradable. Suena así: si pasa una lambda como parámetro del método andThen , ¡esta lambda no funcionará!

Para verificar esto, simplemente escriba una prueba simple:

 Completable .fromCallable { cardRepository.uploadDataToServer() } .andThen { cardRepository.markLocalDataAsSynced() } .subscribe() 

Y luego el contenido fallará. Este es el caso con la mayoría de los operadores (como flatMap , diferir , fromAction y muchos otros), realmente se espera lambda como argumentos. Y con tal registro con andThen , se espera Completable / Observable / SingleSource . El problema se resuelve usando paréntesis ordinarios () en lugar de rizado {}.

Este problema se describe en detalle en el artículo "Kotlin y Rx2. Cómo perdí 5 horas debido a los corchetes incorrectos " .

2. Reestructuración


También tocamos sintaxis de Kotlin tan interesante como la desestructuración o la asignación de desestructuración . Le permite asignar un objeto a varias variables a la vez, dividiéndolo en partes.

Imagine que tenemos un método en la API que devuelve varias entidades a la vez:

 @GET("/foo/api/sync") fun getBrandsAndCards(): Single<BrandAndCardResponse> data class BrandAndCardResponse(@SerializedName("cards") val cards: List<Card>?, @SerializedName("brands") val brands: List<Brand>?) 

Una forma compacta de devolver el resultado de este método es la desestructuración, como se muestra en el siguiente ejemplo:

 syncRepository.getBrandsAndCards() .flatMapCompletable {it-> Completable.fromAction{ val (cards, brands) = it syncCards(cards) syncBrands(brands) } } } 

Vale la pena mencionar que las declaraciones múltiples se basan en convenciones: las clases que se supone que deben destruirse deben contener funciones N (), donde N es el número de componente correspondiente, un miembro de la clase. Es decir, el ejemplo anterior se traduce en el siguiente código:

 val cards = it.component1() val brands = it.component2() 

Nuestro ejemplo utiliza una clase de datos que declara automáticamente las funciones componentesN (). Por lo tanto, las declaraciones múltiples funcionan con él fuera de la caja.

Hablaremos más sobre la clase de datos en la siguiente parte, dedicada a la capa de datos.

Capa de datos


Esta capa incluye POJO para datos del servidor y la base, interfaces para trabajar con datos locales y datos recibidos del servidor.

Para trabajar con datos locales, se utilizó Room, que nos proporciona un contenedor conveniente para trabajar con la base de datos SQLite.

El primer objetivo para la migración, que se sugiere, son los POJO, que en el código estándar de Java son clases masivas con muchos campos y sus correspondientes métodos get / set. Puede hacer que POJO sea más conciso con la ayuda de las clases de datos. Una línea de código será suficiente para describir una entidad con múltiples campos:

 data class Card(val id:String, val cardNumber:String, val brandId:String,val barCode:String) 

Además de la concisión, obtenemos:

  • Invalida los métodos equals () , hashCode () y toString () debajo del capó. Generar iguales para todas las propiedades de la clase de datos es extremadamente conveniente cuando se usa DiffUtil en un adaptador que genera vistas para RecyclerView. El hecho es que DiffUtil compara dos conjuntos de datos, dos listas: la antigua y la nueva, descubre qué cambios se han producido y, al utilizar métodos de notificación, se actualiza de manera óptima el adaptador. Y típicamente, los elementos de la lista se comparan usando iguales.

    Por lo tanto, después de agregar un nuevo campo a la clase, no necesitamos agregarlo a iguales para que DiffUtil tenga en cuenta el nuevo campo.
  • Clase inmutable
  • Compatibilidad con los valores predeterminados, que pueden reemplazarse mediante el uso del patrón Builder.

    Un ejemplo:

     data class Card(val id : Long = 0L, val cardNumber: String="99", val barcode: String = "", var brandId: String="1") val newCard = Card(id =1L,cardNumber = "123") 

Otra buena noticia: con kapt configurado (como se describió anteriormente), las clases de datos funcionan bien con las anotaciones de sala, lo que le permite traducir todas las entidades de la base de datos en clases de datos. Room también admite propiedades anulables. Es cierto que Room todavía no admite los valores predeterminados de Kotlin, pero el error correspondiente ya se ha instituido para esto.

Conclusiones


Examinamos solo algunas trampas que pueden surgir durante la migración de Java a Kotlin. Es importante que, aunque surjan problemas, especialmente con la falta de conocimiento teórico o experiencia práctica, todos puedan resolverse.

Sin embargo, el placer de escribir un código conciso, expresivo y seguro en Kotlin compensará con creces todas las dificultades que surjan en el camino de transición. Puedo decir con confianza que el ejemplo del proyecto DISCO ciertamente confirma esto.

Libros, enlaces útiles, recursos.


  1. La base teórica del conocimiento del idioma permitirá colocar el libro Kotlin en acción de los creadores del idioma Svetlana Isakova y Dmitry Zhemerov.

    El laconismo, la información, la amplia cobertura de temas, el enfoque en los desarrolladores de Java y la disponibilidad de una versión en ruso lo convierten en el mejor de los manuales posibles al comienzo del aprendizaje de idiomas. Empecé con ella.
  2. Fuentes de Kotlin con developer.android.
  3. Guía de Kotlin en ruso
  4. Un excelente artículo de Konstantin Mikhailovsky, desarrollador de Android de Genesis, sobre la experiencia de cambiar a Kotlin.

Source: https://habr.com/ru/post/es433074/


All Articles