Arquitectura moderna de MVI basada en Kotlin



En los últimos dos años, los desarrolladores de Android en Badoo han recorrido un largo y espinoso camino desde MVP hacia un enfoque completamente diferente de la arquitectura de aplicaciones. ANublo y yo queremos compartir una traducción de un artículo de nuestro colega Zsolt Kocsi , describiendo los problemas que encontramos y su solución.

Este es el primero de varios artículos dedicados al desarrollo de la arquitectura moderna de MVI en Kotlin.

Comencemos desde el principio: problemas de estado


En cada momento, la aplicación tiene un cierto estado que determina su comportamiento y lo que ve el usuario. Si se enfoca solo en un par de clases, este estado incluye todos los valores de las variables, desde indicadores simples hasta objetos individuales. Cada una de estas variables vive su propia vida y está controlada por diferentes partes del código. Puede determinar el estado actual de la aplicación solo verificándolos uno por uno.

Trabajando en el código, creamos un modelo existente del trabajo del sistema en nuestras cabezas. Implementamos fácilmente casos ideales cuando todo va de acuerdo con el plan, pero somos completamente incapaces de calcular todos los posibles problemas y condiciones de la aplicación. Y tarde o temprano, una de las condiciones que no hemos imaginado nos superará y nos encontraremos con un error.

Inicialmente, el código está escrito de acuerdo con nuestras ideas sobre cómo debería funcionar el sistema. Pero en el futuro, pasando por las cinco etapas de depuración , es necesario rehacer todo dolorosamente, cambiando simultáneamente el modelo del sistema ya creado que se ha desarrollado en mi cabeza. Queda por esperar que tarde o temprano entendamos qué salió mal y se reparará el error.

Pero esto está lejos de ser siempre afortunado. Cuanto más complejo sea el sistema, más probable es que encuentre alguna condición imprevista, cuya depuración será un sueño durante mucho tiempo en las pesadillas.

En Badoo, todas las aplicaciones son sustancialmente asíncronas, no solo por la amplia funcionalidad disponible para el usuario a través de la interfaz de usuario, sino también por la posibilidad de que el servidor envíe datos unidireccionales. El estado y el comportamiento de la aplicación están muy influenciados, desde cambiar el estado del pago a nuevas coincidencias y solicitudes de verificación.

Como resultado, en nuestro módulo de chat nos encontramos con varios errores extraños y difíciles de reproducir que estropearon mucha sangre para todos. A veces los probadores lograron escribirlos, pero no se repitieron en el dispositivo del desarrollador. Debido al código asincrónico, la repetición completa de una cadena de eventos era extremadamente improbable. Y como la aplicación no se bloqueó, ni siquiera teníamos un seguimiento de la pila que mostrara dónde comenzar la búsqueda.

Clean Architecture tampoco pudo ayudarnos. Incluso después de reescribir el módulo de chat, las pruebas A / B revelaron discrepancias pequeñas pero significativas en la cantidad de mensajes de los usuarios que utilizan los módulos nuevos y antiguos. Decidimos que esto se debía a la difícil reproducibilidad de los errores y al estado de la carrera. La discrepancia persistió después de verificar todos los demás factores. Los intereses de la empresa sufrieron, fue difícil para los desarrolladores mantener el código.

No puede lanzar un nuevo componente si funciona peor que el existente, pero tampoco puede liberarlo, ya que tomó una actualización, hubo una razón. Por lo tanto, debe comprender por qué en un sistema que se ve completamente normal y no se bloquea, la cantidad de mensajes disminuye.

¿Dónde comenzar la búsqueda?

Spoiler: esto no es culpa de Clean Architecture, como siempre, el factor humano es el culpable. Al final, por supuesto, arreglamos estos errores, pero dedicamos mucho tiempo y esfuerzo a esto. Entonces pensamos: ¿hay una manera más fácil de evitar estos problemas?

La luz al final del túnel ...


Nos son familiares los términos de moda como Model-View-Intent y “flujo de datos unidireccional”. Si este no es el caso en su caso, le aconsejo que los busque en Google: hay muchos artículos sobre estos temas en Internet. Los desarrolladores de Android recomiendan especialmente el material de ocho piezas de Hannes Dorfman .

Comenzamos a jugar con estas ideas tomadas del desarrollo web a principios de 2017. Enfoques como Flux y Redux resultaron ser muy útiles: nos ayudaron a enfrentar muchos problemas.

En primer lugar, es muy útil contener todos los elementos de estado (variables que afectan la interfaz de usuario y desencadenan varias acciones) en un objeto: el estado . Cuando todo se almacena en un solo lugar, la imagen general es mejor visible. Por ejemplo, si desea imaginar cargar datos utilizando este enfoque, necesita la carga útil y los campos isLoading . Al observarlos, verá cuándo se reciben los datos ( carga útil ) y si la animación ( isLoading ) se muestra al usuario.

Además, si nos alejamos de la ejecución de código paralelo con devoluciones de llamada y expresamos cambios en el estado de la aplicación como una serie de transacciones, obtendremos un único punto de entrada. Te presentamos Reducer , quien vino a nosotros desde la programación funcional. Toma el estado actual y los datos sobre acciones adicionales ( Intención ) y crea un nuevo estado a partir de ellos:

Reducer = (State, Intent) -> State

Continuando con el ejemplo anterior con la carga de datos, obtenemos las siguientes acciones:

  • IniciadoCargando
  • AcabadoCon éxito


Entonces puedes crear Reducer con las siguientes reglas:

  1. En el caso de StartedLoading, cree un nuevo objeto State copiando el antiguo y establezca el valor isLoading en true.
  2. En el caso de FinishedWithSuccess, cree un nuevo objeto State , copiando el antiguo, en el que el valor isLoading se establecerá en falso y el valor de la carga útil será
    partido cargado.

Si enviamos la serie de estado resultante al registro, veremos lo siguiente:

  1. Estado ( carga útil = nulo, isLoading = false): el estado inicial.
  2. Estado ( carga útil = nulo, isLoading = true): después de StartedLoading.
  3. Estado ( carga útil = datos, isLoading = false): después de FinishedWithSuccess.

Al conectar estos estados a la interfaz de usuario, verá todas las etapas del proceso: primero una pantalla en blanco, luego una pantalla de carga y, finalmente, los datos necesarios.

Este enfoque tiene muchas ventajas.

  • En primer lugar, al cambiar centralmente el estado mediante una serie de transacciones, no permitimos el estado de la raza y muchos errores molestos invisibles.
  • En segundo lugar, después de estudiar una serie de transacciones, podemos entender lo que sucedió, por qué sucedió y cómo afectó el estado de la aplicación. Además, con Reducer es mucho más fácil imaginar todos los cambios de estado antes del primer lanzamiento de la aplicación en el dispositivo.
  • Finalmente, podemos crear una interfaz simple. Dado que todos los estados se almacenan en un solo lugar (Tienda), que tiene en cuenta las intenciones (Intentos), realiza cambios utilizando Reducer y demuestra una cadena de estados, entonces puede colocar toda la lógica empresarial en la Tienda y usar la interfaz para iniciar intenciones y mostrar estados.


O no?

... tal vez el tren corriendo hacia ti


El reductor solo claramente no es suficiente. ¿Qué pasa con las tareas asincrónicas con diferentes resultados? ¿Cómo responder al envío desde el servidor? ¿Qué pasa con el lanzamiento de tareas adicionales (por ejemplo, borrar el caché o cargar datos de la base de datos local) después de un cambio de estado? Resulta que, o bien no incluimos toda esta lógica en Reducer (es decir, una buena parte de la lógica de negocios no estará cubierta, y aquellos que decidan usar nuestro componente no tendrán que ocuparse de ella, o forzaremos a Reducer a hacer todo de una vez.

Requisitos marco de MVI


Por supuesto, nos gustaría incluir toda la lógica de negocios de una característica individual en un componente independiente, con el cual los desarrolladores de otros equipos podrían trabajar fácilmente simplemente creando una instancia y suscribiéndose a su estado.

Además

  • Debe interactuar fácilmente con otros componentes del sistema;
  • en su estructura interna debe haber una clara separación de deberes;
  • todas las partes internas del componente deben ser completamente deterministas;
  • La implementación básica de dicho componente debe ser simple y complicada solo si se necesitan elementos adicionales.

No pasamos inmediatamente de Reducer a la solución que usamos hoy. Cada equipo enfrentó problemas al usar enfoques diferentes, y desarrollar una solución universal que se adaptara a todos parecía poco probable.

Y, sin embargo, el estado actual de las cosas se adapta a todos. ¡Nos complace presentarle MVICore! El código fuente de la biblioteca está abierto y disponible en GitHub .

Que es bueno MVICore


  • Una manera fácil de implementar funciones empresariales de programación reactiva con un flujo de datos unidireccional.
  • Escalado: la implementación básica incluye solo Reductor, y en casos más complejos, puede usar componentes adicionales.
  • Una solución para trabajar con eventos que no desea incluir en el estado ( problema SingleLiveEvent ).
  • Una API simple para vincular características (y otros componentes reactivos de su sistema) a la interfaz de usuario y entre sí con soporte para el ciclo de vida de Android (y no solo).
  • Soporte de middleware (ver abajo) para cada componente del sistema.
  • Registrador listo para usar y la capacidad de depurar el viaje en el tiempo para cada componente.


Breve introducción a la función


Como las instrucciones paso a paso ya se han publicado en GitHub, omitiré ejemplos detallados y me centraré en los componentes principales del marco.

Característica : el elemento central del marco que contiene toda la lógica empresarial del componente. La función se define mediante tres parámetros: interfaz Función <Deseo, Estado, Noticias>

Wish corresponde a la Intención de Model-View-Intent: estos son los cambios que queremos ver en el modelo (dado que el término Intent tiene su propio significado en el entorno de los desarrolladores de Android, tuvimos que encontrar un nombre diferente). Wish es el punto de entrada para Feature.

El estado es, como ya entendió, el estado del componente. El estado no es inmutable: no podemos cambiar sus valores internos, pero podemos crear nuevos estados. Este es el resultado: cada vez que creamos un nuevo estado, lo pasamos a la transmisión Rx.

Noticias : un componente para procesar señales que no deberían estar en estado; Las noticias se usan una vez durante la creación ( problema SingleLiveEvent ). El uso de Noticias es opcional (puede usar Nothing from Kotlin en la Firma de funciones).

También en Característica debe estar presente Reductor .

La característica puede contener los siguientes componentes:

  • Actor: realiza tareas asincrónicas y / o modificaciones de estado condicionales basadas en el estado actual (por ejemplo, validación de formulario). El actor vincula el deseo a un número de efecto específico y luego lo pasa al reductor (en ausencia del actor, el reductor recibe el deseo directamente).
  • NewsPublisher: se llama cuando Wish se convierte en cualquier efecto que produce el resultado como un nuevo estado. En base a estos datos, decide si crear Noticias.
  • PostProcessor: también llamado después de crear un nuevo Estado y también sabe qué efecto llevó a su creación. Lanza ciertas acciones adicionales (Acciones). Acción: estos son "Deseos internos" (por ejemplo, borrar el caché) que no se pueden iniciar desde el exterior. Se ejecutan en el Actor, lo que conduce a una nueva cadena de Efectos y Estados.
  • Bootstrapper es un componente que puede ejecutar acciones por sí solo. Su función principal es inicializar Feature y / o correlacionar fuentes externas con Action. Estas fuentes externas pueden ser Noticias de otra Característica o datos del servidor que deberían modificar el Estado sin la intervención del usuario.


El diagrama puede parecer simple:


o incluir todos los componentes adicionales anteriores:


La función en sí misma, que contiene toda la lógica de negocios y está lista para usar, no parece más fácil:



Que mas


Feature, la piedra angular del marco, funciona a nivel conceptual. Pero la biblioteca tiene mucho más que ofrecer.

  • Dado que todos los componentes de Feature son deterministas (con la excepción de Actor, que no es completamente determinista porque interactúa con fuentes de datos externas, pero incluso con eso, la rama que ejecuta está determinada por los datos de entrada, y no por las condiciones externas), cada uno de ellos puede estar envuelto en Middleware. Al mismo tiempo, la biblioteca ya contiene soluciones preparadas para el registro y la depuración de viajes en el tiempo .
  • Middleware es aplicable no solo a Feature, sino también a cualquier otro objeto que implemente la interfaz Consumer <T>, lo que la convierte en una herramienta de depuración indispensable.
  • Cuando utilice un depurador para depurar mientras se mueve en la dirección opuesta, puede implementar el módulo DebugDrawer .
  • La biblioteca incluye un complemento IDEA que se puede usar para agregar plantillas para las implementaciones más comunes de Feature, lo que ahorra mucho tiempo.
  • Hay clases auxiliares para admitir Android, pero la biblioteca en sí no está vinculada a Android.
  • Existe una solución lista para vincular componentes a la interfaz de usuario y entre sí a través de una API primaria (esto se discutirá en el próximo artículo).

Esperamos que pruebe nuestra biblioteca y su uso le brinde tanta alegría como a nosotros: ¡es su creación!

¡Los días 24 y 25 de noviembre, puedes probar suerte y unirte a nosotros! Realizaremos un evento de contratación móvil: en un día será posible pasar por todas las etapas de selección y recibir una oferta. Mis colegas de los equipos iOS y Android vendrán a comunicarse con los candidatos en Moscú. Si eres de otra ciudad, Badoo incurre en gastos de viaje. Para obtener una invitación, realice la prueba de detección en el enlace . Buena suerte

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


All Articles