Recupere la capacidad de respuesta de su gestión estatal, diga no al imperativo MVI | de Gabor Varadi | mayo 2022
¿Está persiguiendo «código limpio, arquitectura limpia, diseño limpio, administración de estado limpia» pero aún se siente atrapado en un mar de repeticiones incluso para las tareas más simples, como mostrar una lista simple obtenida con una corrutina? podría hacerse en una sola línea o tal vez en siete líneas, pero seguramente no debería requerir mayúsculas y minúsculas, tres niveles de direccionamiento indirecto, etc. Bueno, normalmente podría simplemente llamar a funciones en ViewModel y funcionaría, pero cuando se ve obligado a buscar el «santo grial arquitectónico», nadie a su alrededor confiará en su código a menos que incluya al menos una clase sellada llamada Add ViewActions y aumente la complejidad ciclomática de su función de «controlador de acción» hasta que se sienta «lo suficientemente limpio». (Después de todo, cuanto más independientes sean las cosas que hace una sola función en función de su argumento, más «responsabilidad única» tiene para manejar literalmente todo, por lo que sabes que esta es definitivamente la mejor manera posible de hacerlo. 😏) De todos modos, el componente básico de poner todo en una sola clase, ya sea una llamada de función o una propiedad de estado, todo tiene una historia: vino de la web. MVI significa Model-View-Intent y proviene de un marco Javascript (no muy popular para uso en producción) llamado Cycle.js, que va de la mano con un concepto (que ya no es popular) llamado The Elm Architecture, que se define como el mejor prácticas y uso previsto de un experimento (y a partir de 2019 sin mantenimiento) ‘lenguaje de programación reactivo funcional para la web’ llamado ELM. Por otro lado, tampoco surgieron de la nada: el creador es redux, en 2015. La idea general era implementar una máquina de estado utilizando el patrón de procesador de comandos en Javascript, lo que respaldaría la funcionalidad de «deshacer» (también conocida como «depuración de viajes en el tiempo»). Por supuesto, la mayoría de las decisiones de diseño de Redux solo tienen sentido para Javascript, ya que es un lenguaje de escritura no estático. Tiene sentido tener un solo controlador de eventos como .onEvent(‘click’, function() {}) ya que no hay capacidad de detección de API en un lenguaje de escritura dinámica. Solo está tratando de llamar cadenas como funciones (o acceder a cadenas como propiedades/valores) y esperar lo mejor. MVI en Android proviene de dos lugares, el primero es el patrón PRNSAASPFRUICC de mayo de 2016 que modelaría todos los eventos de UI como intenciones que luego se fusionarían en una única transmisión observable, aunque a pesar de los chismes, nunca llegó a ser realmente popular. El siguiente paso fue Mosby-MVI, aunque introdujo el concepto de un único modelo almacenado que combina todos los estados cargados de forma asincrónica y la entrada del usuario, y estados de transición como la carga, lo que da como resultado una «recomendación» que da como resultado un diseño defectuoso para las aplicaciones de Android. Conducir a:
De aplicaciones reactivas con modelo-vista-intento — Parte 1: Modelo”[…] solo necesitamos un Clase de modelo que representa el estado general. Entonces es fácil guardar este modelo en un paquete y luego restaurarlo. De todos modos, yo [the author of that article] Personalmente, creo que la mayoría de las veces es mejor no guardar el estado sino recargar toda la pantalla como lo hacemos cuando iniciamos la aplicación por primera vez. […] Cuando nuestra aplicación sale y guardamos el estado y 6 horas despues El usuario abre nuestra aplicación de nuevo […]”
Por lo tanto, podemos ver claramente que MVI para Android se desarrolló considerando los siguientes supuestos:
- Este proceso de muerte no ocurre hasta más de 6 horas después (aunque puede experimentar una muerte de proceso simplemente abriendo su aplicación de correo electrónico para obtener un código de registro de confirmación de correo electrónico), por lo que no lo necesita «en absoluto». .
- Si admite la muerte del proceso, el límite de tamaño de 1 MB no es un problema (por lo tanto, todos los datos cargados de forma asíncrona se agruparían en el paquete como parte del estado restaurado, incluso en marcos MVI más nuevos como Orbit).
Sobre los reductores de estado:
Aplicaciones receptivas con intención de vista de modelo — Parte 3: Reductor de estado “El reductor de estado es un concepto de programación funcional que toma el estado anterior como entrada y calcula un nuevo estado a partir del estado anterior. […] Un reductor de estado encaja perfectamente en la filosofía modelo-vista-intento con un flujo de datos unidireccional y un modelo que representa el estado”.
MVI introduce la suposición de que la mejor manera de modelar cualquier pantalla es usar una máquina de estados finitos (FSM), donde cada estado siguiente se evalúa en función del estado evaluado previamente. state._state.value = state.value.copy( / / imperativo MVIuserName = newUsername) Y todo suena «genial» en el papel, hasta que te das cuenta de que este diseño tiene limitaciones, es decir, que puedes evaluar cualquier estado en cualquier momento siempre Debe evaluar el anterior antes de poder evaluar el siguiente.Imagine que su interfaz de usuario contiene una vista de texto con autocompletar y el usuario puede ingresar cualquier texto. Entonces, si el usuario cambia el texto entrante, debemos iniciar una nueva consulta de base de datos para reflejar los últimos parámetros de filtro. El uso de MVI requeriría evaluar los resultados de cada carácter individual para obtener la lista más reciente, aunque solo nos preocupamos por la entrada de usuario más reciente. En teoría, con los operadores reactivos podríamos usar debounce (para reducir la cantidad de solicitudes dentro de un período de tiempo determinado) y switchMap/flatMapLatest, pero esto se logra mediante el uso de un reductor de estado. imposible. ¡No puede omitir la evaluación de la condición! Por este motivo, si usa una aplicación que ignora por completo sus acciones de navegación mientras carga datos, no puede cancelar una solicitud en ejecución porque su modelado de estado no lo permite. Cuando se agrega antirrebote en aplicaciones MVI, lo hace la vista y no puede ser un detalle de implementación de ViewModel. Prácticamente todos los marcos de trabajo de tendencias actualmente disponibles comparten las mismas limitaciones; de hecho, si está utilizando un «marco MVI», esto es básicamente lo que hacen. No haces mucho más excepto que:
- Almacene todo su estado en un solo MutableStateFlow/MutableLiveData/BehaviorRelay
- Herede este campo de una clase base (para que esté fuertemente acoplado al marco)
- Cuando se realizan cambios en el estado, estas «mutaciones» se serializan estrictamente y la variable de estado se envuelve en «sincronizado», «mutex» o «bloqueo».
Esto se aplica a casi todos los marcos MVI populares: Orbit, Mavericks, Uniflow-kt, Mobius… ya que todos estos son una forma de implementar el patrón de comando usando una pila de deshacer (procesador de comando).El historial de deshacer en GIMPE es útil si en realidad necesito eso: una lista de operaciones realizadas anteriormente para que pueda deshacerlas cuando lo desee. Sin embargo, esto es una sobrecarga si tiene algo como un formulario de entrada o simplemente muestra una lista de datos recuperados de la base de datos y/o la red. Si reconocemos que ahora, hacemos esto no necesitamos mantener los resultados evaluados (estado + datos cargados asincrónicamente + estado de transición) como un solo objeto en un solo campo, y no necesitamos hacer que la transmisión tenga estado, luego podemos intercambiar el escaneo (que debe devolver un valor síncrono) con CombineLatest (que permite una combinación de cualquier número de secuencias reactivas y, por lo tanto, permite trabajar con resultados obtenidos de forma asíncrona).Usando Combine en lugar de ScanNow podemos eliminar con _state.value = _state.value.copy() porque nuestro estado siempre se construye a partir de los últimos parámetros de entrada evaluados. val state = Combine(flow1, flow2, flow3) { param1, param2, param3 ->State(param1, param2, param3)}.stateIn(coroutineScope, SharingStarted.WhileSubscribed(5000), State()) Si necesitamos eliminar los rebotes de los valores de flow1, podemos agregar eso. Si necesitamos filtrar en flow2, podemos agregar eso. Si necesitamos cargar datos de forma asíncrona desde flow3 desde la base de datos, podemos agregar eso. ¡Hemos recuperado la reactividad! Todo lo que teníamos que hacer era renunciar a lo que los marcos MVI te obligan a hacer con sus API basadas en herencia. Podemos crear un estado de IU reactivo con LiveData (y SavedStateHandle) o BehaviorRelay o MutableStateFlow de RxJava, pero teóricamente incluso con las propiedades computadas observables del enlace de datos. La opción siempre estuvo ahí, todo lo que teníamos que hacer era, eh, ¿no MVI obligatorio?Ejemplo de estado de interfaz de usuario evaluado de forma reactiva Ahora podemos usar su estado para evaluar datos de forma asíncrona, e incluso la persistencia del estado a través de la muerte del proceso es trivial. Una vez que nos damos cuenta de que nunca tuvimos que crear un reductor de estado, podemos crear un combinador de estado en su lugar, finalmente podemos usar marcos reactivos con Combine de manera reactiva, en lugar de estar obligados a implementar una operación de escaneo que procesa todos los eventos estrictamente secuencialmente ( de lo contrario, podríamos tener condiciones de carrera). Siempre ve los valores más recientes de las entradas, lo que significa que las condiciones de carrera son imposibles (no se basa en valores anteriores para evaluar su estado actual). Si asegurar la correcta ejecución era tan fácil como no tener que leer el valor tasado previamente, ¿por qué se ha hecho así durante más de seis años? Como ya he escrito sobre las desventajas del MVI imperativo e incluso di una charla sobre cómo cambiar las respuestas de administración de estado, no seré la persona que responda. ¿Quizás la tercera vez es la vencida? 😅