
Hola a todos! Mi nombre es Anatoly Varivonchik, soy desarrollador de Android en Badoo. Hoy compartiré con ustedes la traducción de la segunda parte del artículo de mi colega Zsolt Kocsi sobre la implementación de MVI, que usamos diariamente en el proceso de desarrollo. La primera parte está
aquí .
Que queremos y como lo hacemos
En la primera parte del artículo, presentamos
Características , los elementos centrales de
MVICore que pueden reutilizarse. Pueden tener la estructura más simple e incluir un solo
reductor , o pueden convertirse en una herramienta completamente funcional para administrar tareas asincrónicas, eventos y mucho más.
Cada característica es rastreable: existe la oportunidad de suscribirse a los cambios en su estado y recibir notificaciones al respecto. Sin embargo, Feature se puede suscribir a una fuente de entrada. Y esto tiene sentido, porque con la inclusión de Rx en la base del código, ya tenemos muchos objetos y suscripciones observables en varios niveles.
Es en relación con el aumento en el número de componentes reactivos que es hora de reflexionar sobre lo que tenemos y si es posible mejorar aún más el sistema.
Tenemos que responder tres preguntas:
- ¿Qué elementos deben usarse al agregar nuevos componentes reactivos?
- ¿Cuál es la forma más fácil de administrar sus suscripciones?
- ¿Es posible ignorar la administración del ciclo de vida / la necesidad de borrar las suscripciones para evitar pérdidas de memoria? En otras palabras, ¿podemos separar el enlace de componentes de la gestión de suscripción?
En esta parte del artículo, veremos los conceptos básicos y los beneficios de construir un sistema utilizando componentes reactivos y veremos cómo Kotlin ayuda con esto.
Elementos principales
Cuando llegamos a trabajar en el diseño y la estandarización de nuestras
características , ya habíamos probado muchos enfoques diferentes y decidimos que las
características estarían en forma de componentes reactivos. Primero, nos centramos en las interfaces principales. En primer lugar, necesitábamos determinar los tipos de datos de entrada y salida.
Razonamos de la siguiente manera:
- No reinventemos la rueda, veamos qué interfaces ya existen.
- Como ya estamos usando la biblioteca RxJava, tiene sentido referirse a sus interfaces básicas.
- El número de interfaces debe minimizarse.
Como resultado, decidimos usar
ObservableSource <T> para la salida y
Consumer <T> para la entrada. ¿Por qué no
Observable / Observador , preguntas?
Observable es una clase abstracta de la que necesita heredar, y
ObservableSource es la interfaz que implementa que satisface completamente la necesidad de implementar un protocolo reactivo.
package io.reactivex; import io.reactivex.annotations.*; public interface ObservableSource<T> { void subscribe(@NonNull Observer<? super T> observer); }
Observer , la primera interfaz que viene a la mente, implementa cuatro métodos: onSubscribe, onNext, onError y onComplete. En un esfuerzo por simplificar el protocolo tanto como sea posible, preferimos
Consumer <T> , que acepta nuevos elementos utilizando un único método. Si elegimos
Observer , los métodos restantes a menudo serían redundantes o funcionarían de manera diferente (por ejemplo, nos gustaría presentar errores como parte del estado, y no como excepciones, y ciertamente no interrumpir el flujo).
public interface Consumer<T> { void accept(T t) throws Exception; }
Entonces, tenemos dos interfaces, cada una de las cuales contiene un método. Ahora podemos vincularlos firmando
Consumer <T> a
ObservableSource <T> . Este último acepta solo instancias de
Observer <T> , pero podemos envolverlo en un
Observable <T> , que está suscrito a
Consumer <T> :
val output: ObservableSource<String> = Observable.just("item1", "item2", "item3") val input: Consumer<String> = Consumer { System.out.println(it) } val disposable = Observable.wrap(output).subscribe(input)
(Afortunadamente, la función
.wrap (salida) no crea un nuevo objeto si la
salida ya
es un
Observable <T> ).
Es posible que recuerde que el componente
Característica de la primera parte del artículo utilizaba datos de entrada del tipo
Deseo (correspondiente a la intención del modelo-vista-intención) y la salida del tipo
Estado , y por lo tanto puede estar en ambos lados del paquete:
Esta vinculación de
Consumidor y
Productor ya parece bastante simple, pero hay una forma aún más sencilla en la que no necesita crear suscripciones manualmente o cancelarlas.
Introduciendo
Binder .
Enlace de esteroides
MVICore contiene una clase llamada
Binder que proporciona una API simple para administrar las suscripciones Rx y tiene una serie de características interesantes.
¿Por qué es necesario?
- Cree un enlace suscribiendo entradas para el fin de semana.
- La capacidad de darse de baja al final del ciclo de vida (cuando es un concepto abstracto y no tiene nada que ver con Android).
- Bonificación: Binder le permite agregar objetos intermedios, por ejemplo, para el registro o la depuración de viajes en el tiempo.
En lugar de firmar manualmente, puede volver a escribir los ejemplos anteriores de la siguiente manera:
val binder = Binder() binder.bind(wishes to feature) binder.bind(feature to logger)
Gracias a Kotlin, todo parece muy simple.
Estos ejemplos funcionan si el tipo de entrada y salida es el mismo. Pero, ¿y si no es así? Al implementar la función de extensión, podemos hacer que la transformación sea automática:
val output: ObservableSource<A> = TODO() val input: Consumer<B> = TODO() val transformer: (A) -> B = TODO() binder.bind(output to input using transformer)
Presta atención a la sintaxis: se lee casi como una oración normal (y esta es otra razón por la que amo a Kotlin). Pero
Binder no solo se usa como azúcar sintáctico, también nos es útil para resolver problemas con el ciclo de vida.
Crear carpeta
Crear una instancia no parece más fácil:
val binder = Binder()
Pero en este caso, debe darse de baja manualmente, y debe llamar a
binder.dispose()
cada vez que necesite eliminar suscripciones. Hay otra forma: inyectar la instancia del ciclo de vida en el constructor. Así:
val binder = Binder(lifecycle)
Ahora no necesita preocuparse por las suscripciones, ya que se eliminarán al final del ciclo de vida. Al mismo tiempo, el ciclo de vida puede repetirse muchas veces (como el ciclo de inicio y finalización en la interfaz de usuario de Android), y
Binder creará y eliminará suscripciones cada vez.
¿Y qué es un ciclo de vida?
La mayoría de los desarrolladores de Android, al ver la frase "ciclo de vida", representan los ciclos de Actividad y Fragmento. Sí,
Binder puede trabajar con ellos, cancelando la suscripción al final del ciclo.
Pero esto es solo el comienzo, porque no utiliza la interfaz de Android
LifecycleOwner de ninguna manera:
Binder tiene su propia
versión , más universal. Es esencialmente un flujo de señal BEGIN / END:
interface Lifecycle : ObservableSource<Lifecycle.Event> { enum class Event { BEGIN, END }
Puede implementar esta secuencia usando Observable (mediante mapeo), o simplemente usar la clase
ManualLifecycle de la biblioteca para entornos que no son Rx (ver exactamente a continuación).
¿Cómo funciona la
carpeta ? Al recibir una señal de INICIO, crea suscripciones para los componentes que configuró previamente (
entrada / salida ), y al recibir una señal de FIN, los elimina. Lo más interesante es que puedes comenzar de nuevo:
val output: PublishSubject<String> = PublishSubject.create() val input: Consumer<String> = Consumer { System.out.println(it) } val lifecycle = ManualLifecycle() val binder = Binder(lifecycle) binder.bind(output to input) output.onNext("1") lifecycle.begin() output.onNext("2") output.onNext("3") lifecycle.end() output.onNext("4") lifecycle.begin() output.onNext("5") output.onNext("6") lifecycle.end() output.onNext("7")
Esta flexibilidad en la reasignación de suscripciones es especialmente útil cuando se trabaja con Android, cuando puede haber varios ciclos Start-Stop y Resume-Pause, además del habitual Create-Destroy.
Android Binder Lifecycles
Hay tres clases en la biblioteca:
- CreateDestroyBinderLifecycle ( androidLifecycle )
- StartStopBinderLifecycle ( androidLifecycle )
- ResumePauseBinderLifecycl e ( androidLifecycle )
androidLifecycle
es el valor devuelto por el método
getLifecycle()
, es decir,
AppCompatActivity ,
AppCompatDialogFragment , etc. Todo es muy simple:
fun createBinderForActivity(activity: AppCompatActivity) = Binder( CreateDestroyBinderLifecycle(activity.lifecycle) )
Ciclos de vida individuales
No nos detengamos allí, porque no estamos vinculados a Android de ninguna manera. ¿Cuál es el ciclo de vida de una
carpeta ? Literalmente cualquier cosa: por ejemplo, el tiempo de reproducción de un diálogo o el tiempo de ejecución de alguna tarea asincrónica. Puede, por ejemplo, vincularlo con el alcance DI, y luego cualquier suscripción se eliminará con él. Completa libertad de acción.
- ¿Desea guardar las suscripciones antes de que el Observable envíe el elemento? Convierta este objeto a Lifecycle y páselo a Binder . Implemente el siguiente código en la función de extensión y utilícelo más adelante:
fun Observable<T>.toBinderLifecycle() = Lifecycle.wrap(this .first() .map { END } .startWith(BEGIN) )
- ¿Desea conservar sus enlaces hasta que se complete Completable ? Sin problemas: esto se hace por analogía con el párrafo anterior:
fun Completable.toBinderLifecycle() = Lifecycle.wrap( Observable.concat( Observable.just(BEGIN), this.andThen(Observable.just(END)) ) )
- ¿Desea algún otro código que no sea Rx para decidir cuándo eliminar las suscripciones? Use ManualLifecycle como se describe arriba.
En cualquier caso, puede colocar una secuencia reactiva en la secuencia del elemento
Lifecycle.Event o usar
ManualLifecycle si está trabajando con código que no es Rx.
Descripción general del sistema
Binder oculta los detalles de la creación y administración de suscripciones Rx. Todo lo que queda es una breve descripción general: "El componente A interactúa con el componente B en el alcance C".
Supongamos que tenemos los siguientes componentes reactivos para la pantalla actual:

Nos gustaría que los componentes se conectaran dentro de la pantalla actual, y sabemos que:
- UIEvent se puede alimentar directamente a AnalyticsTracker ;
- UIEvent se puede transformar en Wish for Feature ;
- El estado se puede transformar en un modelo de vista para una vista .
Esto se puede expresar en un par de líneas:
with(binder) { bind(feature to view using stateToViewModelTransformer) bind(view to feature using uiEventToWishTransformer) bind(view to analyticsTracker) }
Hacemos tales apretones para demostrar la interconexión de componentes. Y dado que los desarrolladores pasamos más tiempo leyendo el código que escribiéndolo, una breve descripción general es extremadamente útil, especialmente a medida que aumenta el número de componentes.
Conclusión
Vimos cómo
Binder ayuda a administrar las suscripciones de Rx y cómo le ayuda a obtener una visión general de un sistema construido a partir de componentes reactivos.
En los siguientes artículos, diremos cómo separamos los componentes reactivos de la interfaz de usuario de la lógica empresarial y cómo agregar objetos intermedios usando
Binder (para el registro y la depuración del viaje en el tiempo). ¡No cambies!
Mientras tanto, echa un vistazo a la biblioteca en
GitHub .