230 lecturas

Esta pequeña biblioteca de Kotlin podría ser la forma más limpia de construir aplicaciones cross-platform

por Android Insights19m2025/05/07
Read on Terminal Reader

Demasiado Largo; Para Leer

SimpleMVI es una solución ligera pero potente para implementar el patrón MVI en los proyectos multiplataforma de Kotlin.
featured image - Esta pequeña biblioteca de Kotlin podría ser la forma más limpia de construir aplicaciones cross-platform
Android Insights HackerNoon profile picture
0-item

En el mundo del desarrollo móvil, la elección de la arquitectura de aplicaciones adecuada juega un papel crítico en garantizar la calidad del código, la mantenibilidad y la escalabilidad. Cada año trae nuevos enfoques, bibliotecas y marcos diseñados para simplificar el proceso de desarrollo y hacer que el código sea más estructurado.En los últimos años, la arquitectura MVI (Model-View-Intent) ha ganado particular popularidad ofreciendo una solución elegante para gestionar el estado de la aplicación y organizar el flujo de datos unidireccional.


En este artículo, examinaremos SimpleMVI, una solución ligera pero poderosa para implementar el patrón MVI en proyectos multiplataforma de Kotlin. Exploraremos los componentes centrales de la biblioteca, sus características y analizaremos ejemplos prácticos que le ayudarán a entender cómo aplicar SimpleMVI en sus proyectos.

Qué es el patrón MVI y sus conceptos básicos

Model-View-Intent (MVI) es un patrón arquitectónico para el desarrollo de interfaces de usuario, inspirado en la programación funcional y los sistemas reactivos.

  1. Unidirectional Data Flow — data moves in one direction, forming a cycle: from user action to model change, then to view update.

  2. Immutable State — the application state is not changed directly; instead, a new state is created based on the previous one.

  3. Determinism — the same user actions with the same initial state always lead to the same result.


En arquitectura de MVI:

  • El modelo representa el estado inmutable de la aplicación que describe completamente los datos necesarios para mostrar la interfaz de usuario.
  • View muestra pasivamente el estado actual y transmite las acciones del usuario como Intentos.
  • Intent describe las intenciones del usuario o sistema que pueden potencialmente cambiar el estado de la aplicación.


Además de estos componentes básicos, MVI a menudo incluye:

  • Reductor: una función que toma el estado actual y la intención, y devuelve un nuevo estado.
  • Efectos secundarios: efectos secundarios que no afectan al estado, pero requieren interacción con sistemas externos (por ejemplo, navegación, notificaciones, solicitudes de API).

Breve historia de los patrones arquitectónicos

Los patrones arquitectónicos de la interfaz de usuario han evolucionado significativamente con el tiempo:

MVC (Controlador de Modelo de Visión)

Uno de los primeros patrones que dividió la aplicación en tres componentes:

  • Modelo - Datos y Lógica de Negocios
  • Ver - Interfaz de Usuario
  • Controlador – manejo de la entrada de usuario


El principal problema con MVC es el acoplamiento estrecho entre los componentes y la separación no clara de las responsabilidades, lo que complica las pruebas y el mantenimiento.

MVP (Modelo de Presentación)

Una mejora sobre el MVC, donde:

  • Modelo - Datos y Lógica de Negocios
  • Vista - Interfaz de usuario pasiva
  • Presenter - mediador entre el modelo y la vista


MVP resuelve el problema de la probabilidad, pero a menudo conduce a presentadores hinchados y un acoplamiento estrecho entre Presenter y View.

MVVM (Modelo de Visión)

El siguiente paso en la evolución:

  • Modelo - Datos y Lógica de Negocios
  • Ver - Interfaz de Usuario
  • ViewModel — transforma los datos de Modelo en un formato conveniente para ver


MVVM utiliza el concepto de vinculación de datos, que reduce la cantidad de código de boilerplate pero puede causar problemas con el flujo de datos de seguimiento.

MVI (Intento de Modelo-Visión)

Un enfoque moderno que enfatiza:

  • Predictibilidad: un enfoque determinista de la gestión estatal
  • Immutabilidad: el estado no se cambia, sino que se sustituye
  • Flujo de datos unidireccional: secuencia clara y transparente de eventos


MVI es particularmente eficaz para aplicaciones complejas y ricas en datos con numerosas interacciones de usuario y operaciones asíncronas.

Por qué se creó SimpleMVI y su lugar entre otras bibliotecas

SimpleMVI fue desarrollado para proporcionar a los desarrolladores una herramienta simple pero poderosa para implementar el patrón MVI en los proyectos Kotlin Multiplatform.

  1. Se centra en la lógica del dominio, sin imponer soluciones para la capa de interfaz de usuario
  2. Se adhiere al principio de "simplicidad por encima de todo", proporcionando un conjunto mínimo de componentes necesarios
  3. Es optimizado para Kotlin Multiplatform, garantizando la compatibilidad con varias plataformas
  4. Controla estrictamente la seguridad del hilo, garantizando que la interacción con el estado sólo ocurra en el hilo principal
  5. Proporciona una configuración flexible de gestión de errores a través del sistema de configuración


Las principales ventajas de SimpleMVI frente a las alternativas:

  • Menos dependencias y tamaño de biblioteca más pequeño en comparación con soluciones más complejas
  • Límite de entrada más bajo para comprender y usar
  • Enfoque completo de Kotlin utilizando construcciones del lenguaje moderno
  • DSL conveniente para describir la lógica empresarial
  • Separación de responsabilidades entre los componentes


SimpleMVI no tiene como objetivo resolver todos los problemas de arquitectura de aplicaciones, pero proporciona una base fiable para organizar la lógica de negocio que puede integrarse con cualquier solución para UI, navegación y otros aspectos de la aplicación.

Conceptos y componentes básicos de SimpleMVI

SimpleMVI ofrece un enfoque minimalista para implementar la arquitectura MVI, centrándose en tres componentes clave: Store, Actor y Middleware.Cada uno de estos componentes tiene un papel único en asegurar el flujo de datos unidireccional y gestionar el estado de la aplicación.

Tienda - El elemento central de la arquitectura

Definición y función de la tienda

Store es el corazón de SimpleMVI: es un contenedor que contiene el estado de la aplicación, procesa las intenciones y genera efectos secundarios.

public interface Store<in Intent : Any, out State : Any, out SideEffect : Any> {
    // Current state
    public val state: State
    
    // State flow
    public val states: StateFlow<State>
    
    // Side effects flow
    public val sideEffects: Flow<SideEffect>

    // Store initialization
    @MainThread
    public fun init()

    // Intent processing
    @MainThread
    public fun accept(intent: Intent)

    // Store destruction
    @MainThread
    public fun destroy()
}

Gran ciclo de vida

La tienda tiene un ciclo de vida claramente definido:

  1. Creation - instantiating the Store object with necessary dependencies

  2. Initialization - calling the init() method, preparing internal components

  3. Active use - processing intents through the accept(intent) method

  4. Destruction - calling the destroy() method, releasing resources


Es importante entender que:

  • Todos los métodos de la Tienda pública deben ser llamados sólo en el hilo principal (marcado con la anotación @MainThread)
  • Después de llamar destruir(), la Tienda no puede usarse; los intentos de acceder a una Tienda destruida resultarán en un error
  • La tienda debe ser inicializada con el método init() antes de usar

Gestión del Estado

La tienda ofrece las siguientes capacidades para trabajar con el estado:

  • Access to the current state via the state property

  • Observing state changes via the states flow

  • Processing side effects via the sideEffects flow


SimpleMVI utiliza clases de Kotlin Coroutines para la implementación de flujo:StateFlowPara los estados y regularesFlowpara efectos secundarios, asegurando la compatibilidad con los enfoques estándar de la programación reactiva en Kotlin.

Extensión conveniente para la tienda

SimpleMVI proporciona operadores convenientes para trabajar con intenciones:

// Instead of store.accept(intent)
store + MyStore.Intent.LoadData

// Instead of store.accept(intent)
store += MyStore.Intent.LoadData

Actor - Implementación de la lógica empresarial

Principios de trabajo del actor

Actor es el componente responsable de la lógica de negocio en SimpleMVI. Acepta intenciones, las procesa y puede producir un nuevo estado y efectos secundarios. Actor es el mediador entre la interfaz de usuario y los datos de la aplicación.

public interface Actor<Intent : Any, State : Any, out SideEffect : Any> {
    @MainThread
    public fun init(
        scope: CoroutineScope,
        getState: () -> State,
        reduce: (State.() -> State) -> Unit,
        onNewIntent: (Intent) -> Unit,
        postSideEffect: (sideEffect: SideEffect) -> Unit,
    )

    @MainThread
    public fun onIntent(intent: Intent)

    @MainThread
    public fun destroy()
}

Cada participante tiene acceso a:

  • CoroutineScope - para el lanzamiento de operaciones asíncronas
  • Función de estado actual (getState)
  • Función de reducción del estado (reducción)
  • Nueva función de envío de intención (onNewIntent)
  • Función de envío de efectos secundarios (postSideEffect)

Intento de procesamiento

ElonIntent(intent: Intent)El método es llamado por la Tienda cuando recibe una nueva intención y es el principal punto de entrada para la lógica empresarial.

  1. Determina el tipo de intención recibida
  2. Realizar la lógica de negocio necesaria
  3. Actualiza el estado
  4. Generar efectos secundarios si es necesario

DefaultActor y DslActor: diferentes enfoques de implementación

SimpleMVI ofrece dos enfoques diferentes para la implementación de Actor:

DefaultActor - Enfoque Orientado a Objetos

class CounterActor : DefaultActor<CounterIntent, CounterState, CounterSideEffect>() {
    override fun handleIntent(intent: CounterIntent) {
        when (intent) {
            is CounterIntent.Increment -> {
                reduce { copy(count = count + 1) }
            }
            is CounterIntent.Decrement -> {
                reduce { copy(count = count - 1) }
            }
            is CounterIntent.Reset -> {
                reduce { CounterState() }
                sideEffect(CounterSideEffect.CounterReset)
            }
        }
    }
    
    override fun onInit() {
        // Initialization code
    }
    
    override fun onDestroy() {
        // Cleanup code
    }
}

Ventajas de DefaultActor:

  • Enfoque familiar de OOP
  • Conveniente para la lógica de negocio compleja
  • Adecuado para grandes proyectos

DslActor - Enfoque funcional con DSL

val counterActor = actorDsl<CounterIntent, CounterState, CounterSideEffect> {
    onInit {
        // Initialization code
    }
    
    onIntent<CounterIntent.Increment> {
        reduce { copy(count = count + 1) }
    }
    
    onIntent<CounterIntent.Decrement> {
        reduce { copy(count = count - 1) }
    }
    
    onIntent<CounterIntent.Reset> {
        reduce { CounterState() }
        sideEffect(CounterSideEffect.CounterReset)
    }
    
    onDestroy {
        // Cleanup code
    }
}

Ventajas de DslActor:

  • Un enfoque más declarativo
  • Menos códigos de boiler
  • Más adecuado para proyectos pequeños y medianos
  • Tipo de manejo seguro de intención


Ambos enfoques proporcionan la misma funcionalidad, y la elección entre ellos depende de las preferencias del desarrollador y las especificidades del proyecto.

Mediumware - Ampliación de la funcionalidad

Objetivos del Middleware

Middleware en SimpleMVI actúa como un observador de los eventos en la Tienda. Middleware no puede modificar los eventos, pero puede reaccionar a ellos, lo que lo hace ideal para implementar lógica interfuncional como el logging, la analítica o el debugging.

public interface Middleware<Intent : Any, State : Any, SideEffect : Any> {
    // Called when Store is initialized
    public fun onInit(state: State)
    
    // Called when a new intent is received
    public fun onIntent(intent: Intent, state: State)
    
    // Called when state changes
    public fun onStateChanged(oldState: State, newState: State)
    
    // Called when a side effect is generated
    public fun onSideEffect(sideEffect: SideEffect, state: State)
    
    // Called when Store is destroyed
    public fun onDestroy(state: State)
}

Capacidad de logging y debugging

SimpleMVI incluye una implementación de Middleware integrada para el registro —LoggingMiddleware:

val loggingMiddleware = LoggingMiddleware<MyIntent, MyState, MySideEffect>(
    name = "MyStore",
    logger = DefaultLogger
)


LoggingMiddlewarecaptura todos los eventos en la Tienda y los saca al log:


MyStore | Initialization
MyStore | Intent | LoadData
MyStore | Old state | State(isLoading=false, data=null)
MyStore | New state | State(isLoading=true, data=null)
MyStore | SideEffect | ShowLoading
MyStore | Destroying

Esto es útil para el desgaste, ya que le permite rastrear todo el flujo de datos en la aplicación.

Implementación de Custom Middleware

Crear tu propio Middleware es muy simple:

class AnalyticsMiddleware<Intent : Any, State : Any, SideEffect : Any>(
    private val analytics: AnalyticsService
) : Middleware<Intent, State, SideEffect> {
    
    override fun onInit(state: State) {
        analytics.logEvent("store_initialized")
    }
    
    override fun onIntent(intent: Intent, state: State) {
        analytics.logEvent("intent_received", mapOf("intent" to intent.toString()))
    }
    
    override fun onStateChanged(oldState: State, newState: State) {
        analytics.logEvent("state_changed")
    }
    
    override fun onSideEffect(sideEffect: SideEffect, state: State) {
        analytics.logEvent("side_effect", mapOf("effect" to sideEffect.toString()))
    }
    
    override fun onDestroy(state: State) {
        analytics.logEvent("store_destroyed")
    }
}


Middleware se puede combinar, creando una cadena de manejadores:

val store = createStore(
    name = storeName<MyStore>(),
    initialState = MyState(),
    actor = myActor,
    middlewares = listOf(
        loggingMiddleware,
        analyticsMiddleware,
        debugMiddleware
    )
)

Casos de uso clave para Middleware

  1. Logging — recording all events for debugging

  2. Analytics — tracking user actions

  3. Performance metrics — measuring intent processing time

  4. Debugging — visualizing data flow through UI

  5. Testing — verifying the correctness of event sequences


Es importante recordar que Middleware es un observador pasivo y no puede modificar los eventos que recibe.

Trabajar con la biblioteca

Instalación y instalación

Añadir la dependencia a su proyecto:

// build.gradle.kts
implementation("io.github.arttttt.simplemvi:simplemvi:<version>")

Crea tu primera tienda

La forma más sencilla de crear una Tienda es declarar una clase que implementa la interfaz de la Tienda:

class CounterStore : Store<CounterStore.Intent, CounterStore.State, CounterStore.SideEffect> by createStore(
    name = storeName<CounterStore>(),
    initialState = State(),
    actor = actorDsl {
        onIntent<Intent.Increment> {
            reduce { copy(count = count + 1) }
        }
        
        onIntent<Intent.Decrement> {
            reduce { copy(count = count - 1) }
        }
    }
) {
    sealed interface Intent {
        data object Increment : Intent
        data object Decrement : Intent
    }
    
    data class State(val count: Int = 0)
    
    sealed interface SideEffect
}

Uso de la tienda

// Creating an instance
val counterStore = CounterStore()

// Initialization
counterStore.init()

// Sending intents
counterStore.accept(CounterStore.Intent.Increment)
// or using operators
counterStore + CounterStore.Intent.Increment
counterStore += CounterStore.Intent.Decrement

// Getting the current state
val currentState = counterStore.state

// Subscribing to the state flow
val statesJob = launch {
    counterStore.states.collect { state ->
        // Useful work
    }
}

// Subscribing to side effects
val sideEffectsJob = launch {
    counterStore.sideEffects.collect { sideEffect ->
        // Processing side effects
    }
}

// Releasing resources
counterStore.destroy()

Soporte multiplataforma de Kotlin

SimpleMVI soporta varias plataformas a través de Kotlin Multiplatform:

  • Android
  • Los
  • macos
  • Título JS

El uso de mecanismos de aislamiento de código específicos de la plataformaexpect/actual:

// Common code
public expect fun isMainThread(): Boolean

// Android implementation
public actual fun isMainThread(): Boolean {
    return Looper.getMainLooper() == Looper.myLooper()
}

// iOS implementation
public actual fun isMainThread(): Boolean {
    return NSThread.isMainThread
}

// wasm js implementation
public actual fun isMainThread(): Boolean {
    return true // JavaScript is single-threaded
}

El logging se implementa de manera similar para diferentes plataformas:

// Common code
public expect fun logV(tag: String, message: String)

// Android implementation
public actual fun logV(tag: String, message: String) {
    Log.v(tag, message)
}

// iOS/wasm js implementation
public actual fun logV(tag: String, message: String) {
    println("$tag: $message")
}

Ejemplo práctico: Concierto

Definición del modelo de datos

class CounterStore : Store<CounterStore.Intent, CounterStore.State, CounterStore.SideEffect> {
    // Intents - user actions
    sealed interface Intent {
        data object Increment : Intent
        data object Decrement : Intent
        data object Reset : Intent
    }
    
    // State
    data class State(
        val count: Int = 0,
        val isPositive: Boolean = true
    )
    
    // Side effects - one-time events
    sealed interface SideEffect {
        data object CounterReset : SideEffect
    }
}

Gran Implementación

class CounterStore : Store<CounterStore.Intent, CounterStore.State, CounterStore.SideEffect> by createStore(
    name = storeName<CounterStore>(),
    initialState = State(),
    actor = actorDsl {
        onIntent<Intent.Increment> {
            reduce { 
                copy(
                    count = count + 1,
                    isPositive = count + 1 >= 0
                ) 
            }
        }
        
        onIntent<Intent.Decrement> {
            reduce { 
                copy(
                    count = count - 1,
                    isPositive = count - 1 >= 0
                ) 
            }
        }
        
        onIntent<Intent.Reset> {
            reduce { State() }
            sideEffect(SideEffect.CounterReset)
        }
    }
) {
    // Data model defined above
}

Conexión a la UI (Android Exemplo)

class CounterViewModel : ViewModel() {
    private val store = CounterStore()
    
    init {
        // Built-in extension for automatic lifecycle management
        attachStore(store)
    }
    
    val state = store.states.stateIn(
        scope = viewModelScope,
        started = SharingStarted.Eagerly,
        initialValue = store.state
    )
    
    val sideEffects = store.sideEffects
    
    fun increment() {
        store.accept(CounterStore.Intent.Increment)
    }
    
    fun decrement() {
        store.accept(CounterStore.Intent.Decrement)
    }
    
    fun reset() {
        store.accept(CounterStore.Intent.Reset)
    }
}

Características avanzadas

Configuración de la biblioteca

SimpleMVI ofrece un sistema de configuración flexible:

configureSimpleMVI {
    // Strict error handling mode (throws exceptions)
    strictMode = true
    
    // Logger configuration
    logger = object : Logger {
        override fun log(message: String) {
            // Your logging implementation
        }
    }
}

Errores de manejo

  • strictMode = verdadero - la biblioteca funciona en modo estricto y lanza excepciones cuando se detectan errores
  • strictMode = false (default) - la biblioteca funciona en modo lenient y solo registra errores sin interrumpir la ejecución

Actuación errónea

SimpleMVI tiene excepciones especiales:

  • NotOnMainThreadException - cuando intenta llamar a los métodos de la tienda no desde el hilo principal
  • StoreIsNotInitializedExcepción - cuando intenta usar una tienda no inicializada
  • StoreIsAlreadyDestroyedExcepción - cuando intenta usar una tienda ya destruida

Prueba de componentes

Gracias a la separación limpia de responsabilidades, los componentes SimpleMVI son fáciles de probar:

// Example of Store testing
@Test
fun `increment should increase counter by 1`() {
    // Arrange
    val store = CounterStore()
    store.init()
    
    // Act
    store.accept(CounterStore.Intent.Increment)
    
    // Assert
    assertEquals(1, store.state.count)
    assertTrue(store.state.isPositive)
    
    // Cleanup
    store.destroy()
}

Conclusión

A medida que el desarrollo móvil se vuelve cada vez más complejo y los requisitos para la calidad del código y el mantenimiento de la aplicación crecen, la elección de la arquitectura correcta se convierte en una decisión crítica. SimpleMVI ofrece un enfoque moderno y elegante para la organización del código basado en los principios del patrón MVI y adaptado para el desarrollo multiplataforma con Kotlin.

Beneficios de SimpleMVI

Para resumir, se pueden destacar los siguientes puntos fuertes de la biblioteca:

Enfoque minimalista y pragmático

SimpleMVI proporciona sólo los componentes necesarios para implementar el patrón MVI, sin abstracciones y complejidades innecesarias.La biblioteca sigue el principio de "simplicidad por encima de todo", lo que lo hace fácil de entender y usar incluso para los desarrolladores que están acabando de familiarizarse con la arquitectura MVI.

Soporte completo de Kotlin multiplataforma

Construido en Kotlin desde el principio, SimpleMVI está optimizado para el desarrollo multiplataforma.La biblioteca aísla el código específico de la plataforma a través del mecanismo expect/real, asegurando la compatibilidad con Android, iOS, macOS y wasm js.

Gestión del Estado Predecible

El cumplimiento estricto de los principios de inmutabilidad de estado y flujo de datos unidireccional hace que las aplicaciones construidas en SimpleMVI sean más previsibles y menos propensas a errores.

Protección integrada contra problemas comunes

La biblioteca proporciona un control de seguridad estricto del hilo, asegurando que la interacción con el estado ocurra sólo en el hilo principal. Esto evita muchos errores comunes relacionados con el multithreading que pueden ser difíciles de detectar y corregir.

DSL conveniente para la descripción lógica declarativa

Gracias al soporte DSL, SimpleMVI permite describir la lógica empresarial en un estilo declarativo, haciendo el código más legible y comprensible.

Flexibilidad y extensibilidad

A pesar de su enfoque minimalista, SimpleMVI proporciona mecanismos para extender la funcionalidad a través del sistema Middleware. Esto hace que sea fácil agregar capacidades como el logging, la analítica o el borrado sin afectar a la lógica de negocio principal.

Casos típicos de uso

SimpleMVI es particularmente adecuado para los siguientes escenarios:

Proyectos multiplataforma de Kotlin

Si está desarrollando una aplicación que necesita trabajar en múltiples plataformas (Android e iOS, aplicaciones web), SimpleMVI le permite usar un único enfoque arquitectónico y código de lógica empresarial compartido.

Aplicaciones con interacciones complejas de estado y usuario

Para las aplicaciones que gestionan estados complejos y manejan numerosas interacciones de usuarios, el enfoque MVI proporciona una estructura clara y predictibilidad.

Proyectos con énfasis en la probabilidad

Gracias a la clara separación de las responsabilidades entre los componentes y el flujo de datos previsible, las aplicaciones construidas con SimpleMVI son fácilmente testables por unidad.

Migración de proyectos existentes a la arquitectura MVI

SimpleMVI se puede introducir gradualmente, comenzando con módulos o características individuales, por lo que es adecuado para la migración gradual de proyectos existentes a la arquitectura MVI.

Proyectos educativos y prototipos

Debido a su simplicidad y minimalismo, SimpleMVI es adecuado para la enseñanza de principios MVI y para el prototipo rápido.

Recursos para el aprendizaje adicional

Para aquellos que quieran profundizar su conocimiento de la arquitectura SimpleMVI y MVI en general, recomiendo los siguientes recursos:

  • SimpleMVI repositorio de GitHub — código fuente de la biblioteca con ejemplos de uso
  • Documentación SimpleMVI – Documentación oficial con descripción detallada de la API y recomendaciones

Pensamientos finales

SimpleMVI representa una solución equilibrada para organizar la lógica empresarial de las aplicaciones utilizando enfoques modernos a la arquitectura.La biblioteca ofrece una estructura clara y un flujo de datos previsible sin imponer complejidad innecesaria.


Al elegir una arquitectura para su proyecto, recuerde que no hay solución universal adecuada para todos los casos. SimpleMVI puede ser una excelente opción para proyectos donde se valora la simplicidad, la predictibilidad y el soporte multiplataforma, pero para algunos escenarios, otras bibliotecas o enfoques pueden ser más adecuados.


Experimenta, explora diferentes soluciones arquitectónicas y elige lo que mejor se adapte a las necesidades de tu proyecto y equipo.Y recuerda: la mejor arquitectura es aquella que te ayuda a resolver eficazmente las tareas a mano, no aquella que crea complejidad adicional.

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks