
Muchos hoy adoran la programación reactiva. Tiene muchas ventajas: la falta del llamado "
infierno de devolución de llamada ", y el mecanismo de manejo de errores incorporado, y un estilo de programación funcional que reduce la probabilidad de errores. Significativamente más fácil de escribir código de subprocesos múltiples y más fácil de administrar flujos de datos (combinar, dividir y convertir).
Muchos lenguajes de programación tienen su propia biblioteca reactiva: RxJava para JVM, RxJS para JavaScript, RxSwift para iOS, Rx.NET, etc.
¿Pero qué tenemos para Kotlin? Sería lógico suponer que RxKotlin. Y, de hecho, dicha biblioteca existe, pero es solo un conjunto de extensiones para RxJava2, el llamado "azúcar".
E idealmente, me gustaría tener una solución que cumpla con los siguientes criterios:
- multiplataforma: para poder escribir bibliotecas multiplataforma utilizando programación reactiva y distribuirlas dentro de la empresa;
- Seguridad nula : el sistema de tipo Kotlin nos protege de " errores de miles de millones de dólares ", por lo que los valores nulos deben ser válidos (por ejemplo,
Observable<String?>
);
- La covarianza y contravarianza es otra característica muy útil de Kotlin, que hace posible, por ejemplo, convertir de forma segura el tipo
Observable<String>
en Observable<CharSequence>
.
En Badoo decidimos no esperar el clima junto al mar e hicimos esa biblioteca. Como habrás adivinado, lo llamamos Reaktive y lo publicamos en
GitHub .
En este artículo, analizaremos más de cerca las expectativas de programación reactiva de Kotlin y veremos cómo las capacidades de Reaktive coinciden con ellas.
Tres beneficios naturales de recuperación
Multiplataforma
La primera ventaja
natural es la más importante. Nuestros equipos de iOS, Android y Web móvil actualmente existen por separado. Los requisitos son generales, el diseño es el mismo, pero cada equipo hace su propio trabajo.
Kotlin le permite escribir código multiplataforma, pero debe olvidarse de la programación reactiva. Y me gustaría poder escribir bibliotecas compartidas usando programación reactiva y distribuirlas dentro de la empresa o subirlas a GitHub. Potencialmente, este enfoque puede reducir significativamente el tiempo de desarrollo y la cantidad total de código.
Seguridad nula
Se trata más bien de la falla de Java y RxJava2. En resumen, nulo no se puede utilizar. Intentemos averiguar por qué. Eche un vistazo a esta interfaz Java:
public interface UserDataSource { Single<User> load(); }
¿El resultado puede ser nulo? Para evitar ambigüedades, nulo no está permitido en RxJava2. Y si aún lo necesita, eso es Quizás y Opcional. Pero en Kotlin no hay tales problemas. Podemos decir que
Single<User>
y
Single<User?>
diferentes tipos, y todos los problemas aparecen en la etapa de compilación.
Covarianza y contravarianza
Esta es una característica distintiva de Kotlin, algo que falta mucho en Java. Puede leer más sobre esto en el
manual . Daré solo un par de ejemplos interesantes de los problemas que surgen al usar RxJava en Kotlin.
Covarianza :
fun bar(source: Observable<CharSequence>) { } fun foo(source: Observable<String>) { bar(source)
Como
Observable
es una interfaz Java, dicho código no se compila. Esto se debe a que los tipos genéricos en Java son invariables. Por supuesto, puede usarlo, pero luego usar operadores como el escaneo nuevamente conducirá a un error de compilación:
fun bar(source: Observable<out CharSequence>) { source.scan { a, b -> "$a,$b" }
La declaración de exploración es diferente en que su tipo genérico "T" es tanto de entrada como de salida. Si Observable era la interfaz de Kotlin, entonces su tipo T podría denotarse como fuera y esto resolvería el problema:
interface Observable<out T> { … }
Y aquí hay un ejemplo con contravarianza:
fun bar(consumer: Consumer<String>) { } fun foo(consumer: Consumer<CharSequence>) { bar(consumer)
Por la misma razón que en el ejemplo anterior (los tipos genéricos en Java son invariables), este ejemplo no se compila. Agregarlo resolverá el problema, pero nuevamente no al cien por cien:
fun bar(consumer: Consumer<in String>) { if (consumer is Subject) { val value: String = consumer.value
Bueno, por tradición, en Kotlin este problema se resuelve al usarlo en la interfaz:
interface Consumer<in T> { fun accept(value: T) }
Por lo tanto, la variabilidad y la contravarianza de los tipos genéricos son la tercera ventaja
natural de la biblioteca Reaktive.
Kotlin + Reactivo = Reactivo
Pasamos a lo principal: la descripción de la biblioteca Reaktive.
Estas son algunas de sus características:
- Es multiplataforma, lo que significa que finalmente puede escribir código general. En Badoo, consideramos que este es uno de los beneficios más importantes.
- Está escrito en Kotlin, lo que nos brinda las ventajas descritas anteriormente: no hay restricciones en nulo, varianza / contravarianza. Esto aumenta la flexibilidad y proporciona seguridad durante la compilación.
- No hay dependencia de otras bibliotecas, como RxJava, RxSwift, etc., lo que significa que no es necesario llevar la funcionalidad de la biblioteca a un denominador común.
- API pura Por ejemplo, la interfaz
ObservableSource
en Reaktive simplemente se llama Observable
, y todos los operadores son funciones de extensión ubicadas en archivos separados. No hay clases de Dios de 15,000 líneas. Esto hace posible aumentar fácilmente la funcionalidad sin realizar cambios en las interfaces y clases existentes.
- Soporte para planificadores (usando
observeOn
familiares subscribeOn
y observeOn
).
- Compatible con RxJava2 (interoperabilidad), proporcionando conversión de fuente entre Reaktive y RxJava2 y la capacidad de reutilizar programadores de RxJava2.
- Cumplimiento ReactiveX .
Me gustaría hablar un poco más sobre los beneficios que hemos recibido debido al hecho de que la biblioteca está escrita en Kotlin.
- En Reaktive, se permiten valores nulos, porque en Kotlin es seguro. Aquí hay algunos ejemplos interesantes:
observableOf<String>(null) //
val o1: Observable<String?> = observableOf(null)
val o2: Observable<String> = o1 // ,
val o1: Observable<String?> = observableOf(null)
val o2: Observable<String> = o1.notNull() // , null
val o1: Observable<String> = observableOf("Hello")
val o2: Observable<String?> = o1 //
val o1: Observable<String?> = observableOf(null)
val o2: Observable<String> = observableOf("Hello")
val o3: Observable<String?> = merge(o1, o2) //
val o4: Observable<String> = merge(o1, o2) // ,
La variación también es una gran ventaja. Por ejemplo, en la interfaz Observable
, el tipo T se declara como out
, lo que hace posible escribir algo como lo siguiente:
fun foo() { val source: Observable<String> = observableOf("Hello") bar(source)
Así es como se ve hoy la biblioteca:- estado en el momento de la escritura: alfa (son posibles algunos cambios en la API pública);
- plataformas compatibles: JVM y Android;
- fuentes compatibles:
Observable
, Maybe
, Single
y Completable
;
- se admite un número bastante grande de operadores, incluidos map, filter, flatMap, concatMap, combineLatest, zip, merge y otros (la lista completa se puede encontrar en GitHub );
- Los siguientes planificadores son compatibles: computación, IO, trampolín y main;
- sujetos: PublishSubject y BehaviorSubject;
- la contrapresión aún no es compatible, pero estamos pensando en la necesidad y la implementación de esta función.
¿Cuáles son nuestros planes para el futuro cercano?- comenzar a usar Reaktive en nuestros productos (actualmente estamos considerando opciones);
- Soporte de JavaScript (solicitud de extracción ya en revisión);
- soporte para iOS
- publicar artefactos en JCenter (actualmente usando el servicio JitPack);
- Documentación
- aumento en el número de operadores compatibles;
- Pruebas
- más plataformas: ¡las solicitudes de extracción son bienvenidas!
Puede probar la biblioteca ahora, puede encontrar todo lo que necesita en
GitHub . Comparte tu experiencia y haz preguntas. Estaremos agradecidos por cualquier comentario.