Hola a todos! En este artículo quiero hablar sobre una nueva biblioteca que trae el patrón de diseño MVI a Android. Esta biblioteca se llama MVIDroid, escrita 100% en Kotlin, ligera y utiliza RxJava 2.x. Yo personalmente soy el autor de la biblioteca, su código fuente está disponible en GitHub y puede conectarlo a través de JitPack (enlace al repositorio al final del artículo). Este artículo consta de dos partes: una descripción general de la biblioteca y un ejemplo de su uso.
MVI
Y así, como prefacio, permíteme recordarte qué es MVI. Modelo - Ver - Intención o, si está en ruso, Modelo - Ver - Intención. Este es un patrón de diseño en el que el Modelo es un componente activo que acepta Intentos y genera Estado. Presentación (Ver) a su vez acepta Modelos de Representación (Ver Modelo) y produce esas mismas Intenciones. El estado se convierte en un modelo de vista mediante una función de transformación (View Model Mapper). Esquemáticamente, el patrón MVI se puede representar de la siguiente manera:

En MVIDroid, la representación no produce intenciones directamente. En su lugar, produce eventos UI, que luego se convierten a Intent utilizando una función de transformación.

Componentes principales de MVIDroid
Modelo
Comencemos con el modelo. En la biblioteca, el concepto de Modelo se expande ligeramente, aquí produce no solo Estados sino también Etiquetas. Las etiquetas se utilizan para comunicar modelos entre sí. Las etiquetas de algunos Modelos se pueden convertir en Intenciones de otros Modelos usando funciones de transformación. Esquemáticamente, el Modelo se puede representar de la siguiente manera:

En MVIDroid, el modelo está representado por la interfaz MviStore (el nombre de la tienda está prestado de Redux):
interface MviStore<State : Any, in Intent : Any, Label : Any> : (Intent) -> Unit, Disposable { @get:MainThread val state: State val states: Observable<State> val labels: Observable<Label> @MainThread override fun invoke(intent: Intent) @MainThread override fun dispose() @MainThread override fun isDisposed(): Boolean }
Y para que tengamos:
- La interfaz tiene tres parámetros genéricos: Estado - tipo de estado, Intención - tipo de intención y Etiqueta - tipo de etiquetas
- Contiene tres campos: estado: el estado actual del modelo, estados: estados observables y etiquetas: etiquetas observables. Los dos últimos campos permiten suscribirse a los cambios en el Estado y en las Etiquetas, respectivamente.
- Intención del consumidor
- Es desechable, lo que hace posible destruir el modelo y detener todos los procesos que ocurren en él.
Tenga en cuenta que todos los métodos de modelo deben ejecutarse en el hilo principal. Lo mismo es cierto para cualquier otro componente. Por supuesto, puede realizar tareas en segundo plano utilizando las herramientas estándar de RxJava.
Componente
Un componente en MVIDroid es un grupo de Modelos unidos por un objetivo común. Por ejemplo, puede seleccionar todos los Modelos para una pantalla en el Componente. En otras palabras, el Componente es la fachada de los Modelos encerrados en él y le permite ocultar detalles de implementación (Modelos, funciones de transformación y sus relaciones). Veamos el diagrama de componentes:

Como puede ver en el diagrama, el componente tiene una función importante de transformar y redirigir eventos.
Una lista completa de la función Componente es la siguiente:
- Asocia eventos y etiquetas de representación entrantes con cada modelo utilizando las funciones de transformación proporcionadas
- Lleve las etiquetas de modelos salientes al exterior
- Destruye todos los modelos y rompe todos los lazos cuando se destruye un componente
El componente también tiene su propia interfaz:
interface MviComponent<in UiEvent : Any, out States : Any> : (UiEvent) -> Unit, Disposable { @get:MainThread val states: States @MainThread override fun invoke(event: UiEvent) @MainThread override fun dispose() @MainThread override fun isDisposed(): Boolean }
Considere la interfaz de componentes con más detalle:
- Contiene dos parámetros genéricos: UiEvent - tipo de visualización de eventos y estados - tipo de estados de modelos
- Contiene el campo de estados que da acceso al grupo de estados modelo (por ejemplo, como una interfaz o clase de datos)
- Ver eventos del consumidor
- Es desechable, lo que permite destruir el componente y todos sus modelos.
Vista
Como puede suponer, se necesita una vista para mostrar los datos. Los datos para cada vista se agrupan en un modelo de vista y generalmente se representan como una clase de datos (Kotlin). Considere la interfaz de Vista:
interface MviView<ViewModel : Any, UiEvent : Any> { val uiEvents: Observable<UiEvent> @MainThread fun subscribe(models: Observable<ViewModel>): Disposable }
Aquí todo es un poco más simple. Dos parámetros genéricos: ViewModel: tipo de modelo de vista y UiEvent: tipo de eventos de vista. Un campo de uiEvents es el evento Observable View, que permite a los clientes suscribirse a estos mismos eventos. Y un método subscribe () que le permite suscribirse a View Models.
Ejemplo de uso
Ahora es el momento de probar algo en la práctica. Propongo hacer algo muy simple. Algo que no requiere mucho esfuerzo para entender, y al mismo tiempo da una idea de cómo usar todo esto y en qué dirección avanzar. Sea un generador de UUID: con solo tocar un botón, generaremos un UUID y lo mostraremos en la pantalla.
Sumisión
Primero, describimos el modelo de vista:
data class ViewModel(val text: String)
Y ver eventos:
sealed class UiEvent { object OnGenerateClick: UiEvent() }
Ahora implementamos la Vista en sí, para esto necesitamos herencia de la clase abstracta MviAbstractView:
class View(activity: Activity) : MviAbstractView<ViewModel, UiEvent>() { private val textView = activity.findViewById<TextView>(R.id.text) init { activity.findViewById<Button>(R.id.button).setOnClickListener { dispatch(UiEvent.OnGenerateClick) } } override fun subscribe(models: Observable<ViewModel>): Disposable = models.map(ViewModel::text).distinctUntilChanged().subscribe { textView.text = it } }
Todo es extremadamente simple: nos suscribimos a los cambios de UUID y actualizamos TextView cuando recibimos un nuevo UUID, y cuando se hace clic en el botón, enviamos el evento OnGenerateClick.
Modelo
El modelo constará de dos partes: interfaz e implementación.
Interfaz:
interface UuidStore : MviStore<State, Intent, Nothing> { data class State(val uuid: String? = null) sealed class Intent { object Generate : Intent() } }
Aquí todo es simple: nuestra interfaz extiende la interfaz MviStore, indicando los tipos de Estado (Estado) e Intención (Intención). Tipo de etiquetas: nada, porque nuestro modelo no las produce. La interfaz también contiene las clases State e Intent.
Para implementar el modelo, debe comprender cómo funciona. A la entrada del modelo, se reciben intenciones, que se convierten en acciones utilizando la función especial IntentToAction. Las acciones se ingresan al Ejecutor, quien las ejecuta y produce el Resultado y la Etiqueta. Los resultados luego van al Reductor, que convierte el Estado actual en uno nuevo.
Los cuatro modelos que componen:
- IntentToAction: una función que convierte IntentTo Action
- MviExecutor - Ejecuta acciones y produce resultados y etiquetas
- MviReducer - convierte pares (estado, resultado) a nuevos estados
- MviBootstrapper es un componente especial que le permite inicializar el modelo. Da las mismas acciones que también van al ejecutor. Puede realizar una acción única o puede suscribirse a una fuente de datos y realizar acciones en ciertos eventos. Bootstrapper se inicia automáticamente cuando crea un modelo.
Para crear el Modelo en sí, debe usar una fábrica especial de Modelos. Está representado por la interfaz MviStoreFactory y su implementación de MviDefaultStoreFactory. La fábrica acepta los componentes del Modelo y emite un Modelo listo para usar.
La fábrica de nuestro modelo tendrá el siguiente aspecto:
class UuidStoreFactory(private val factory: MviStoreFactory) { fun create(factory: MviStoreFactory): UuidStore = object : UuidStore, MviStore<State, Intent, Nothing> by factory.create( initialState = State(), bootstrapper = Bootstrapper, intentToAction = { when (it) { Intent.Generate -> Action.Generate } }, executor = Executor(), reducer = Reducer ) { } private sealed class Action { object Generate : Action() } private sealed class Result { class Uuid(val uuid: String) : Result() } private object Bootstrapper : MviBootstrapper<Action> { override fun bootstrap(dispatch: (Action) -> Unit): Disposable? { dispatch(Action.Generate) return null } } private class Executor : MviExecutor<State, Action, Result, Nothing>() { override fun invoke(action: Action): Disposable? { dispatch(Result.Uuid(UUID.randomUUID().toString())) return null } } private object Reducer : MviReducer<State, Result> { override fun State.reduce(result: Result): State = when (result) { is Result.Uuid -> copy(uuid = result.uuid) } } }
Este ejemplo presenta los cuatro componentes del modelo. Primero, el método de creación de fábrica, luego Acciones y Resultados, seguido por el Contratista y al final el Reductor.
Componente
La clase de datos describe los estados de los componentes (grupo de estados):
data class States(val uuidStates: Observable<UuidStore.State>)
Al agregar nuevos modelos a un componente, su estado también debe agregarse al grupo.
Y, de hecho, la implementación en sí:
class Component(uuidStore: UuidStore) : MviAbstractComponent<UiEvent, States>( stores = listOf( MviStoreBundle( store = uuidStore, uiEventTransformer = UuidStoreUiEventTransformer ) ) ) { override val states: States = States(uuidStore.states) private object UuidStoreUiEventTransformer : (UiEvent) -> UuidStore.Intent? { override fun invoke(event: UiEvent): UuidStore.Intent? = when (event) { UiEvent.OnGenerateClick -> UuidStore.Intent.Generate } } }
Heredamos la clase abstracta MviAbstractComponent, especificamos los tipos de Estados y Ver Eventos, pasamos nuestro Modelo a la superclase e implementamos el campo de estados. Además, creamos una función de transformación que transformará Ver eventos en Intenciones de nuestro modelo.
Modelos de vista de mapeo
Tenemos condiciones y modelos de presentación, es hora de convertir uno en otro. Para hacer esto, implementamos la interfaz MviViewModelMapper:
object ViewModelMapper : MviViewModelMapper<States, ViewModel> { override fun map(states: States): Observable<ViewModel> = states.uuidStates.map { ViewModel(text = it.uuid ?: "None") } }
Vinculante
La presencia del componente y la presentación por sí sola no es suficiente. Para que todo comience a funcionar, deben estar conectados. Es hora de crear una actividad:
class UuidActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_uuid) bind( Component(UuidStoreFactory(MviDefaultStoreFactory).create()), View(this) using ViewModelMapper ) } }
Utilizamos el método bind (), que toma un Componente y una matriz de Vistas con los mapeadores de sus Modelos. Este método es un método de extensión sobre LifecycleOwner (que son Activity y Fragment) y utiliza el DefaultLifecycleObserver del paquete Arch, que requiere compatibilidad de fuente Java 8. Si por alguna razón no puede usar Java 8, entonces el segundo método bind () es adecuado para usted, que no es un método de extensión y devuelve MviLifecyleObserver. En este caso, deberá llamar a los métodos del ciclo de vida usted mismo.
Referencias
El código fuente de la biblioteca, así como las instrucciones detalladas para conectarse y usar se pueden encontrar en GitHub .