Orbit: la librería MVI de Kotlin
Orbit es la forma más pequeña de hacer MVI en Kotlin: una capa diminuta y ergonómica construida sobre coroutines y Flow, con soporte Multiplatform. En lugar de imponer un runtime propio y un vocabulario ceremonioso, expone apenas cuatro piezas —ContainerHost, container, intent y el par reduce/postSideEffect— y delega el resto en el lenguaje. Esta lección sitúa Orbit dentro de la familia MVI, presenta su superficie mínima de API y defiende su tesis de diseño: una buena librería MVI debe poner nombre a un patrón y quitarse de en medio, porque las corrutinas ya resuelven mejor lo que otras librerías reimplementan como motor.
Cada nivel anterior te dejó una pieza del rompecabezas del estado: el flujo unidireccional, el reducer puro, la separación entre estado de cliente y de servidor. Orbit es lo que ocurre cuando alguien toma esas ideas, las comprime a su mínima expresión y las envuelve en la maquinaria asíncrona nativa de Kotlin. No es un framework que te pida aprender un universo nuevo: es una convención diminuta —cuatro nombres— montada sobre coroutines y Flow. Su tesis roza la provocación: casi todo lo que otras librerías MVI implementan como motor propio, el lenguaje ya lo resuelve mejor con corrutinas, y el trabajo honesto de una librería MVI es poner nombre a un patrón y luego apartarse.
- Situar Orbit dentro de la familia MVI y frente al MVI artesanal ceremonioso.
- Reconocer las cuatro piezas de su API:
ContainerHost,container,intenty el parreduce/postSideEffect. - Entender qué significa que sea Kotlin Multiplatform y por qué se apoya en
Flow. - Interiorizar la tesis: una capa fina sobre coroutines, no un runtime nuevo.
MVI, comprimido a una librería diminuta
MVI —Model-View-Intent— es la última vuelta de tuerca del flujo unidireccional que estudiaste con Redux: la vista emite intenciones, cada intención produce un estado nuevo e inmutable, y la vista se redibuja como función pura de ese estado. La diferencia entre las muchas encarnaciones de MVI no está en esa idea, que es común, sino en cuánta maquinaria te obligan a escribir para respetarla. El MVI artesanal de la década pasada pedía sellar cada intención en una clase, enrutarla por un reductor gigante, cablear a mano el Flow de estado y montar un canal aparte para los eventos: decenas de líneas de andamiaje antes de la primera línea de lógica.
Orbit parte de la observación contraria. Kotlin ya trae corrutinas para la asincronía, StateFlow para el estado observable y sealed interface para modelar intenciones y eventos con exhaustividad. Si el lenguaje ya resuelve las partes difíciles, la librería solo tiene que ofrecer un vocabulario mínimo y coherente que las una. Por eso Orbit es deliberadamente pequeño: su superficie completa cabe en una tarjeta, y lo que no está en la librería —cómo lanzas trabajo, cómo esperas, cómo cancelas— es Kotlin idiomático, no una API que memorizar.
import androidx.lifecycle.ViewModel
import org.orbitmvi.orbit.ContainerHost
import org.orbitmvi.orbit.syntax.simple.intent
import org.orbitmvi.orbit.syntax.simple.postSideEffect
import org.orbitmvi.orbit.syntax.simple.reduce
import org.orbitmvi.orbit.viewmodel.container
data class ContadorState(val total: Int = 0)
sealed interface ContadorSideEffect {
data class Toast(val texto: String) : ContadorSideEffect
}
class ContadorViewModel : ContainerHost<ContadorState, ContadorSideEffect>, ViewModel() {
override val container = container<ContadorState, ContadorSideEffect>(ContadorState())
fun sumar(valor: Int) = intent {
postSideEffect(ContadorSideEffect.Toast("Sumando $valor"))
reduce { state.copy(total = state.total + valor) }
}
}
Eso es un feature MVI entero y funcional: un estado, un tipo de eventos, una operación que dispara un efecto y transforma el estado. No hay creadores de acciones, ni switch, ni cableado de flujos. La ceremonia desapareció, pero el modelo mental —intención, estado inmutable, evento de una vez— sigue intacto.
Las cuatro piezas de la API
ContainerHost
La interfaz que implementa tu clase de lógica —casi siempre un ViewModel—. La marca como dueña de un estado y un tipo de side effect, y desbloquea el DSL intent.
container
La fábrica que crea la unidad que guarda el estado y expone los dos flujos observables: el de estado y el de side effects. Es el corazón que verás en detalle en la lección siguiente.
intent
Abre una corrutina donde vive una operación de negocio. Dentro puedes suspender, llamar repositorios y orquestar trabajo asíncrono como en cualquier corrutina.
reduce / postSideEffect
reduce transforma el estado de forma secuencial y segura; postSideEffect emite un evento de una sola vez. Son las dos únicas formas de producir salida.
La economía es intencionada: con solo estos cuatro nombres expresas cualquier feature. ContainerHost declara el contrato de tipos, container lo materializa, intent es la puerta a la asincronía, y reduce frente a postSideEffect encarna la distinción más importante de todo el nivel —lo que es estado retenido frente a lo que es un evento efímero—. No hay una quinta pieza escondida esperando en la curva de aprendizaje.
Multiplataforma, sobre coroutines y Flow
Orbit es Kotlin Multiplatform: el mismo ViewModel con su container, sus intent y sus reduce compila para Android, iOS, escritorio y web, porque no depende de nada específico de una plataforma. El estado se expone como StateFlow<STATE> y los side effects como Flow<SIDE_EFFECT>, dos tipos de la biblioteca de corrutinas que existen igual en todos los objetivos. La capa de interfaz —Compose, SwiftUI, vistas Android— solo colecciona esos flujos; la lógica es común y se comparte sin cambios.
Apoyarse en Flow no es un detalle de implementación, es la decisión de diseño central. Significa que Orbit no inventa su propio sistema de suscripción, su propio scheduler ni su propio modelo de cancelación: reutiliza el de corrutinas, que ya es idiomático, probado y conocido por cualquiera que escriba Kotlin. La consecuencia práctica es que integrar Orbit con el resto de tu app —otro Flow, una llamada suspend, un viewModelScope— no requiere adaptadores: todo habla el mismo idioma asíncrono.
flowchart LR V[Vista] -->|intent| VM[ContainerHost / ViewModel] VM -->|reduce| C[container] C -->|stateFlow| V VM -->|postSideEffect| C C -->|sideEffectFlow evento unico| V style C fill:#cba6f7,color:#11111b style VM fill:#89b4fa,color:#11111b
Como intent es una corrutina y reduce es determinista, probar un feature Orbit es directo: la librería orbit-test reproduce las intenciones y afirma sobre el estado y los efectos resultantes. Un expectInitialState(), invocar la intención, y comprobar el expectState { copy(total = 5) } y el expectSideEffect(...) esperados. La misma pequeñez que hace el código legible lo hace testeable.
Detente en lo que Orbit demuestra, porque trasciende a esta librería concreta. Durante años se creyó que hacer MVI bien exigía un runtime robusto: un despachador de acciones, un bus de eventos, un motor de suscripción, un modelo de hilos propio. Orbit prueba que casi todo eso era accidental, no esencial. Cuando el lenguaje anfitrión ya tiene corrutinas para la asincronía, StateFlow para el estado observable, sealed interface para la exhaustividad y data class con copy para la inmutabilidad barata, el trabajo real de una librería MVI se encoge hasta caber en cuatro nombres. Orbit no es potente por lo que añade, sino por lo que se atreve a no añadir: es una convención sobre coroutines, no un motor por encima de ellas. Interioriza esta forma de juzgar herramientas —preguntar no qué hace una librería, sino cuánto de lo que hace ya lo hacía el lenguaje— y sabrás distinguir una abstracción que multiplica tu poder de una que solo multiplica tu superficie de aprendizaje. La grandeza de Orbit está en su renuncia. Todo el resto del nivel es desplegar, pieza por pieza, esas cuatro renuncias bien elegidas.
- Reescribe mentalmente el
ContadorViewModelde arriba como MVI artesanal: enumera cada clase, canal y flujo que tendrías que declarar a mano y compáralo con las líneas reales de Orbit. - Identifica en el ejemplo dónde termina la API de Orbit y dónde empieza Kotlin puro: subraya qué palabras son de la librería y cuáles del lenguaje.
- Argumenta por qué exponer el estado como
StateFlowy no como un tipo propio de la librería es lo que habilita el soporte Multiplatform. - Nombra, sin mirar, las cuatro piezas de la API y la responsabilidad de cada una.
- Anticipa la lección 4: explica por qué crees que
reduceypostSideEffectson dos operaciones distintas y no una sola forma de producir salida.