Introducción a Kotlin: programación de Android para humanos
Publicado: 2022-03-11En un mundo Android perfecto, el lenguaje principal de Java es realmente moderno, claro y elegante. Puede escribir menos haciendo más, y cada vez que aparece una nueva función, los desarrolladores pueden usarla simplemente aumentando la versión en Gradle. Luego, al crear una aplicación muy agradable, parece totalmente comprobable, extensible y mantenible. Nuestras actividades no son demasiado grandes ni complicadas, podemos cambiar las fuentes de datos de la base de datos a la web sin muchas diferencias, etc. Suena genial, ¿verdad? Desafortunadamente, el mundo de Android no es tan ideal. Google sigue luchando por la perfección, pero todos sabemos que los mundos ideales no existen. Por lo tanto, tenemos que ayudarnos en ese gran viaje en el mundo de Android.
¿Qué es Kotlin y por qué debería usarlo?
Entonces, el primer idioma. Creo que Java no es el maestro de la elegancia o la claridad, y no es ni moderno ni expresivo (y supongo que estás de acuerdo). La desventaja es que debajo de Android N, todavía estamos limitados a Java 6 (incluidas algunas partes pequeñas de Java 7). Los desarrolladores también pueden adjuntar RetroLambda para usar expresiones lambda en su código, lo cual es muy útil al usar RxJava. Por encima de Android N, podemos usar algunas de las nuevas funcionalidades de Java 8, pero sigue siendo ese Java viejo y pesado. Muy a menudo escucho a los desarrolladores de Android decir "Ojalá Android admitiera un lenguaje más agradable, como lo hace iOS con Swift". ¿Y si te dijera que puedes usar un lenguaje muy agradable y simple, con seguridad nula, lambdas y muchas otras características nuevas y agradables? Bienvenido a Kotlin.
¿Qué es Kotlin?
Kotlin es un nuevo lenguaje (a veces denominado Swift para Android), desarrollado por el equipo de JetBrains, y ahora se encuentra en su versión 1.0.2. Lo que lo hace útil en el desarrollo de Android es que se compila en código de bytes JVM y también se puede compilar en JavaScript. Es totalmente compatible con Java, y el código Kotlin se puede convertir simplemente a código Java y viceversa (hay un complemento de JetBrains). Eso significa que Kotlin puede usar cualquier marco, biblioteca, etc. escrito en Java. En Android, se integra con Gradle. Si tiene una aplicación de Android existente y desea implementar una nueva función en Kotlin sin volver a escribir toda la aplicación, simplemente comience a escribir en Kotlin y funcionará.
Pero, ¿cuáles son las 'grandes nuevas funciones'? Permítanme enumerar algunos:
Parámetros de funciones opcionales y con nombre
fun createDate(day: Int, month: Int, year: Int, hour: Int = 0, minute: Int = 0, second: Int = 0) { print("TEST", "$day-$month-$year $hour:$minute:$second") }
Podemos llamar al método createDate de diferentes maneras
createDate(1,2,2016) prints: '1-2-2016 0:0:0' createDate(1,2,2016, 12) prints: '1-2-2016 12:0:0' createDate(1,2,2016, minute = 30) prints: '1-2-2016 0:30:0'
Seguridad nula
Si una variable puede ser nula, el código no se compilará a menos que los obliguemos a hacerlo. El siguiente código tendrá un error: nullableVar puede ser nulo:
var nullableVar: String? = “”; nullableVar.length;
Para compilar, tenemos que verificar si no es nulo:
if(nullableVar){ nullableVar.length }
O, más corto:
nullableVar?.length
De esta forma, si nullableVar es nulo, no pasa nada. De lo contrario, podemos marcar la variable como no anulable, sin un signo de interrogación después del tipo:
var nonNullableVar: String = “”; nonNullableVar.length;
Este código compila, y si queremos asignar un valor nulo a nonNullableVar, el compilador mostrará un error.
También hay un operador de Elvis muy útil:
var stringLength = nullableVar?.length ?: 0
Luego, cuando nullableVar es nulo (entonces, nullableVar?.length devuelve nulo), stringLength tendrá el valor 0.
Variables mutables e inmutables
En el ejemplo anterior, uso var cuando defino una variable. Este es mutable, podemos reasignarlo cuando queramos. Si queremos que esa variable sea inmutable (en muchos casos deberíamos), usamos val (como valor, no como variable):
val immutable: Int = 1
Después de eso, el compilador no nos permitirá reasignar a inmutable.
lambdas
Todos sabemos qué es una lambda, así que aquí solo mostraré cómo podemos usarla en Kotlin:
button.setOnClickListener({ view -> Log.d("Kotlin","Click")})
O si la función es el único o último argumento:
button.setOnClickListener { Log.d("Kotlin","Click")}
Extensiones
Las extensiones son una característica del lenguaje muy útil, gracias a la cual podemos “extender” las clases existentes, incluso cuando son definitivas o no tenemos acceso a su código fuente.
Por ejemplo, para obtener un valor de cadena de texto de edición, en lugar de escribir cada vez que editText.text.toString() podemos escribir la función:
fun EditText.textValue(): String{ return text.toString() }
O más corto:
fun EditText.textValue() = text.toString()
Y ahora, con cada instancia de EditText:
editText.textValue()
O bien, podemos agregar una propiedad que devuelva lo mismo:
var EditText.textValue: String get() = text.toString() set(v) {setText(v)}
Sobrecarga del operador
A veces es útil si queremos sumar, multiplicar o comparar objetos. Kotlin permite la sobrecarga de operadores binarios (más, menos, plusAssign, rango, etc.), operadores de matriz (obtener, establecer, obtener rango, establecer rango) y operaciones de igualdad y unarias (+a, -a, etc.)
Clase de datos
¿Cuántas líneas de código necesita para implementar una clase de Usuario en Java con tres propiedades: copiar, es igual a, hashCode y toString? En Kaotlin solo necesitas una línea:
data class User(val name: String, val surname: String, val age: Int)
Esta clase de datos proporciona los métodos equals(), hashCode() y copy(), y también toString(), que imprime Usuario como:
User(name=John, surname=Doe, age=23)
Las clases de datos también proporcionan otras funciones y propiedades útiles, que puede ver en la documentación de Kotlin.
Extensiones Anko
Usas Butterknife o extensiones de Android, ¿no? ¿Qué sucede si ni siquiera necesita usar esta biblioteca y, después de declarar las vistas en XML, simplemente utilícela desde el código por su ID (como con XAML en C#):
<Button android: android:layout_width="match_parent" android:layout_height="wrap_content" />
loginBtn.setOnClickListener{}
Kotlin tiene extensiones de Anko muy útiles, y con esto no necesitas decirle a tu actividad qué es loginBtn, lo sabe simplemente “importando” xml:
import kotlinx.android.synthetic.main.activity_main.*
Hay muchas otras cosas útiles en Anko, como iniciar actividades, mostrar brindis, etc. Este no es el objetivo principal de Anko: está diseñado para crear fácilmente diseños a partir del código. Entonces, si necesita crear un diseño mediante programación, esta es la mejor manera.
Esta es solo una breve vista de Kotlin. Recomiendo leer el blog de Antonio Leiva y su libro - Kotlin for Android Developers, y por supuesto el sitio oficial de Kotlin.
¿Qué es MVP y por qué?
Un lenguaje agradable, poderoso y claro no es suficiente. Es muy fácil escribir aplicaciones desordenadas con todos los idiomas sin una buena arquitectura. Los desarrolladores de Android (principalmente los que están comenzando, pero también los más avanzados) a menudo asignan a Activity la responsabilidad de todo lo que les rodea. Actividad (o Fragmento u otra vista) descarga datos, los envía para guardarlos, los presenta, responde a las interacciones del usuario, edita datos, administra todas las vistas secundarias. . . ya menudo mucho más. Es demasiado para objetos tan inestables como Actividades o Fragmentos (basta con girar la pantalla y Actividad dice 'Adiós...').
Una muy buena idea es aislar las responsabilidades de las vistas y hacerlas lo más estúpidas posible. Las vistas (Actividades, Fragmentos, vistas personalizadas o lo que sea que presente datos en pantalla) solo deben ser responsables de administrar sus subvistas. Las vistas deben tener presentadores, que se comunicarán con el modelo y les dirán lo que deben hacer. Esto, en resumen, es el patrón Modelo-Vista-Presentador (para mí, debería llamarse Modelo-Presentador-Vista para mostrar las conexiones entre capas).
"¡Oye, conozco algo así, y se llama MVC!" - ¿No pensaste? No, MVP no es lo mismo que MVC. En el patrón MVC, su vista puede comunicarse con el modelo. Mientras usa MVP, no permite ninguna comunicación entre estas dos capas; la única forma en que View puede comunicarse con Model es a través de Presenter. Lo único que View sabe sobre Model puede ser la estructura de datos. Ver sabe cómo, por ejemplo, mostrar Usuario, pero no sabe cuándo. Aquí hay un ejemplo simple:
Ver sabe “Soy actividad, tengo dos EditTexts y un botón. Cuando alguien hace clic en el botón, debo decírselo a mi presentador y pasarle los valores de EditTexts. Y eso es todo, puedo dormir hasta el próximo clic o el presentador me diga qué hacer”.
El presentador sabe que en algún lugar hay una vista y sabe qué operaciones puede realizar esta vista. También sabe que cuando recibe dos cadenas, debe crear un Usuario a partir de estas dos cadenas y enviar datos al modelo para guardarlos, y si el guardado es exitoso, decirle a la vista 'Mostrar información de éxito'.
El modelo solo sabe dónde están los datos, dónde deben guardarse y qué operaciones deben realizarse en los datos.
Las aplicaciones escritas en MVP son fáciles de probar, mantener y reutilizar. Un presentador puro no debe saber nada sobre la plataforma Android. Debe ser una clase Java pura (o Kotlin, en nuestro caso). Gracias a esto podemos reutilizar nuestro presentador en otros proyectos. También podemos escribir fácilmente pruebas unitarias, probando por separado Model, View y Presenter.
Una pequeña digresión: MVP debe ser parte de la arquitectura limpia del tío Bob para hacer que las aplicaciones sean aún más flexibles y tengan una arquitectura agradable. Intentaré escribir sobre eso la próxima vez.
Aplicación de muestra con MVP y Kotlin
¡Es suficiente teoría, veamos un poco de código! Bien, intentemos crear una aplicación simple. El objetivo principal de esta aplicación es crear usuarios. La primera pantalla tendrá dos EditTexts (Nombre y Apellido) y un Botón (Guardar). Después de ingresar el nombre y apellido y hacer clic en 'Guardar', la aplicación debería mostrar 'El usuario está guardado' y pasar a la siguiente pantalla, donde se presentan el nombre y apellido guardados. Cuando el nombre o apellido está vacío, la aplicación no debe guardar el usuario y mostrar un error que indica qué está mal.
Lo primero después de crear el proyecto de Android Studio es configurar Kotlin. Debe instalar el complemento Kotlin y, después de reiniciar, en Herramientas> Kotlin puede hacer clic en 'Configurar Kotlin en el proyecto'. IDE agregará dependencias de Kotlin a Gradle. Si tiene algún código existente, puede convertirlo fácilmente a Kotlin con (Ctrl+Shift+Alt+K o Código > Convertir archivo Java a Kotlin). Si algo está mal y el proyecto no compila, o Gradle no ve Kotlin, puede consultar el código de la aplicación disponible en GitHub.
Ahora que tenemos un proyecto, comencemos creando nuestra primera vista: CreateUserView. Esta vista debe tener las funcionalidades mencionadas anteriormente, por lo que podemos escribir una interfaz para eso:

interface CreateUserView : View { fun showEmptyNameError() /* show error when name is empty */ fun showEmptySurnameError() /* show error when surname is empty */ fun showUserSaved() /* show user saved info */ fun showUserDetails(user: User) /* show user details */ }
Como puede ver, Kotlin es similar a Java en la declaración de funciones. Todas esas son funciones que no devuelven nada, y la última tiene un parámetro. Esta es la diferencia, el tipo de parámetro viene después del nombre. La interfaz View no es de Android, es nuestra interfaz simple y vacía:
interface View
La interfaz del presentador básico debe tener una propiedad de tipo Vista y, al menos, un método (onDestroy, por ejemplo), donde esta propiedad se establecerá en nulo:
interface Presenter<T : View> { var view: T? fun onDestroy(){ view = null } }
Aquí puede ver otra característica de Kotlin: puede declarar propiedades en las interfaces y también implementar métodos allí.
Nuestro CreateUserView necesita comunicarse con CreateUserPresenter. La única función adicional que necesita este presentador es saveUser con dos argumentos de cadena:
interface CreateUserPresenter<T : View>: Presenter<T> { fun saveUser(name: String, surname: String) }
También necesitamos la definición del modelo: se menciona la clase de datos anterior:
data class User(val name: String, val surname: String)
Después de declarar todas las interfaces, podemos comenzar a implementar.
CreateUserPresenter se implementará en CreateUserPresenterImpl:
class CreateUserPresenterImpl(override var view: CreateUserView?): CreateUserPresenter<CreateUserView> { override fun saveUser(name: String, surname: String) { } }
La primera línea, con definición de clase:
CreateUserPresenterImpl(override var view: CreateUserView?)
Es un constructor, lo usamos para asignar la propiedad de vista, definida en la interfaz.
MainActivity, que es nuestra implementación CreateUserView, necesita una referencia a CreateUserPresenter:
class MainActivity : AppCompatActivity(), CreateUserView { private val presenter: CreateUserPresenter<CreateUserView> by lazy { CreateUserPresenterImpl(this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) saveUserBtn.setOnClickListener{ presenter.saveUser(userName.textValue(), userSurname.textValue()) /*use of textValue() extension, mentioned earlier */ } } override fun showEmptyNameError() { userName.error = getString(R.string.name_empty_error) /* it's equal to userName.setError() - Kotlin allows us to use property */ } override fun showEmptySurnameError() { userSurname.error = getString(R.string.surname_empty_error) } override fun showUserSaved() { toast(R.string.user_saved) /* anko extension - equal to Toast.makeText(this, R.string.user_saved, Toast.LENGTH_LONG) */ } override fun showUserDetails(user: User) { } override fun onDestroy() { presenter.onDestroy() } }
Al comienzo de la clase, definimos a nuestro presentador:
private val presenter: CreateUserPresenter<CreateUserView> by lazy { CreateUserPresenterImpl(this) }
Se define como inmutable (val) y lo crea un delegado perezoso, que se asignará la primera vez que se necesite. Además, estamos seguros de que no será nulo (sin signo de interrogación después de la definición).
Cuando el usuario hace clic en el botón Guardar, View envía información al presentador con valores EditTexts. Cuando eso sucede, el usuario debe guardarse, por lo que debemos implementar el método saveUser en Presenter (y algunas de las funciones del modelo):
override fun saveUser(name: String, surname: String) { val user = User(name, surname) when(UserValidator.validateUser(user)){ UserError.EMPTY_NAME -> view?.showEmptyNameError() UserError.EMPTY_SURNAME -> view?.showEmptySurnameError() UserError.NO_ERROR -> { UserStore.saveUser(user) view?.showUserSaved() view?.showUserDetails(user) } } }
Cuando se crea un usuario, se envía a UserValidator para comprobar su validez. Luego, de acuerdo con el resultado de la validación, se llama al método adecuado. La construcción when() {} es igual que switch/case en Java. Pero es más poderoso: Kotlin permite el uso no solo de enum o int en 'case', sino también de rangos, cadenas o tipos de objetos. Debe contener todas las posibilidades o tener una expresión else. Aquí, cubre todos los valores de UserError.
Al usar view?.showEmptyNameError() (con un signo de interrogación después de ver), estamos protegidos contra NullPointer. La vista se puede anular en el método onDestroy, y con esta construcción, no pasará nada.
Cuando un modelo de usuario no tiene errores, le dice a UserStore que lo guarde y luego le indica a View que muestre el éxito y muestre los detalles.
Como se mencionó anteriormente, tenemos que implementar algunas cosas modelo:
enum class UserError { EMPTY_NAME, EMPTY_SURNAME, NO_ERROR } object UserStore { fun saveUser(user: User){ //Save user somewhere: Database, SharedPreferences, send to web... } } object UserValidator { fun validateUser(user: User): UserError { with(user){ if(name.isNullOrEmpty()) return UserError.EMPTY_NAME if(surname.isNullOrEmpty()) return UserError.EMPTY_SURNAME } return UserError.NO_ERROR } }
Lo más interesante aquí es UserValidator. Al usar la palabra objeto, podemos crear una clase singleton, sin preocuparnos por los subprocesos, los constructores privados, etc.
Lo siguiente: en el método de validación de usuario (usuario), hay una expresión con (usuario) {}. El código dentro de dicho bloque se ejecuta en el contexto del objeto, se pasa con el nombre y el apellido como propiedades del usuario.
También hay otra cosita. Todo el código anterior, desde enumeración hasta UserValidator, la definición se coloca en un archivo (la definición de la clase Usuario también está aquí). Kotlin no lo obliga a tener cada clase pública en un solo archivo (o nombrar la clase exactamente como archivo). Por lo tanto, si tiene algunos fragmentos cortos de código relacionado (clases de datos, extensiones, funciones, constantes; Kotlin no requiere una clase para función o constante), puede colocarlo en un solo archivo en lugar de distribuirlo por todos los archivos del proyecto.
Cuando se guarda un usuario, nuestra aplicación debería mostrarlo. Necesitamos otra vista: puede ser cualquier vista de Android, vista personalizada, fragmento o actividad. Elegí Actividad.
Entonces, definamos la interfaz UserDetailsView. Puede mostrar al usuario, pero también debería mostrar un error cuando el usuario no está presente:
interface UserDetailsView { fun showUserDetails(user: User) fun showNoUserError() }
A continuación, UserDetailsPresenter. Debe tener una propiedad de usuario:
interface UserDetailsPresenter<T: View>: Presenter<T> { var user: User? }
Esta interfaz se implementará en UserDetailsPresenterImpl. Tiene que anular la propiedad del usuario. Cada vez que se asigna esta propiedad, el usuario debe actualizar la vista. Podemos usar un setter de propiedades para esto:
class UserDetailsPresenterImpl(override var view: UserDetailsView?): UserDetailsPresenter<UserDetailsView> { override var user: User? = null set(value) { field = value if(field != null){ view?.showUserDetails(field!!) } else { view?.showNoUserError() } } }
La implementación de UserDetailsView, UserDetailsActivity, es muy simple. Al igual que antes, tenemos un objeto presentador creado por carga diferida. El usuario para mostrar se debe pasar a través de la intención. Hay un pequeño problema con esto por ahora, y lo resolveremos en un momento. Cuando tenemos un usuario desde la intención, View necesita asignarlo a su presentador. Después de eso, el usuario se actualizará en la pantalla o, si es nulo, aparecerá el error (y la actividad finalizará, pero el presentador no lo sabe):
class UserDetailsActivity: AppCompatActivity(), UserDetailsView { private val presenter: UserDetailsPresenter<UserDetailsView> by lazy { UserDetailsPresenterImpl(this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_user_details) val user = intent.getParcelableExtra<User>(USER_KEY) presenter.user = user } override fun showUserDetails(user: User) { userFullName.text = "${user.name} ${user.surname}" } override fun showNoUserError() { toast(R.string.no_user_error) finish() } override fun onDestroy() { presenter.onDestroy() } }
Pasar objetos a través de intents requiere que este objeto implemente la interfaz Parcelable. Este es un trabajo muy 'sucio'. Personalmente, odio hacer esto debido a todos los CREADORES, propiedades, guardar, restaurar, etc. Afortunadamente, existe un complemento adecuado, Parcelable for Kotlin. Después de instalarlo, podemos generar Parcelable con solo un clic.
Lo último que debe hacer es implementar showUserDetails (usuario: Usuario) en nuestra MainActivity:
override fun showUserDetails(user: User) { startActivity<UserDetailsActivity>(USER_KEY to user) /* anko extension - starts UserDetailsActivity and pass user as USER_KEY in intent */ }
Y eso es todo.
Tenemos una aplicación sencilla que guarda un Usuario (en realidad, no se guarda, pero podemos agregar esta funcionalidad sin tocar presentador o vista) y lo presenta en la pantalla. En el futuro, si queremos cambiar la forma en que se presenta al usuario en la pantalla, como de dos actividades a dos fragmentos en una actividad, o dos vistas personalizadas, los cambios serán solo en las clases de vista. Por supuesto, si no cambiamos la funcionalidad o la estructura del modelo. El presentador, que no sabe qué es exactamente Ver, no necesitará ningún cambio.
¿Que sigue?
En nuestra aplicación, Presenter se crea cada vez que se crea una actividad. Este enfoque, o su opuesto, si Presenter debe persistir en instancias de actividad, es un tema de mucha discusión en Internet. Para mí, depende de la aplicación, sus necesidades y el desarrollador. A veces es mejor destruir al presentador, a veces no. Si decide conservar uno, una técnica muy interesante es usar LoaderManager para eso.
Como se mencionó anteriormente, MVP debería ser parte de la arquitectura limpia del tío Bob. Además, los buenos desarrolladores deberían usar Dagger para inyectar las dependencias de los presentadores a las actividades. También ayuda a mantener, probar y reutilizar el código en el futuro. Actualmente, Kotlin funciona muy bien con Dagger (antes del lanzamiento oficial no era tan fácil) y también con otras bibliotecas útiles de Android.
Envolver
Para mí, Kotlin es un gran lenguaje. Es moderno, claro y expresivo y, al mismo tiempo, está siendo desarrollado por grandes personas. Y podemos usar cualquier nueva versión en cualquier dispositivo y versión de Android. Cualquier cosa que me haga enojar con Java, Kotlin mejora.
Por supuesto, como dije, nada es ideal. Kotlin también tiene algunas desventajas. Las versiones más recientes del complemento Gradle (principalmente de alfa o beta) no funcionan bien con este lenguaje. Muchas personas se quejan de que el tiempo de compilación es un poco más largo que el de Java puro, y las aplicaciones tienen algunos MB adicionales. Pero Android Studio y Gradle siguen mejorando, y los teléfonos tienen cada vez más espacio para aplicaciones. Por eso creo que Kotlin puede ser un lenguaje muy agradable para todos los desarrolladores de Android. Pruébelo y comparta en la sección de comentarios a continuación lo que piensa.
El código fuente de la aplicación de muestra está disponible en Github: github.com/tomaszczura/AndroidMVPKotlin