Multiplataforma Kotlin Mobius implementación.
¿Qué es Mobius?
La construcción principal proporcionada por Mobius es Mobius Loop, mejor descrito por la documentación oficial. (Incrustado a continuación)
Un bucle de Mobius es parte de una aplicación, que generalmente incluye una interfaz de usuario. En un contexto de Spotify, generalmente hay un bucle para funciones como “página de álbum”, “flujo de inicio de sesión”, etc., pero un bucle también puede estar sin una interfaz de usuario y, por ejemplo, estar vinculado a la vida del bucle de un aplicación o sesión de usuario.
Bucle de Mobius
Un bucle de Mobius recibe Eventos, que han pasado a un Actualizar trabajar junto con la corriente Plantilla. Como resultado de ejecutar la función de actualización, el modelo puede cambiar y Efectos podría ser enviado. El modelo se puede ver desde la interfaz de usuario y los efectos son recibidos y ejecutados por un Administrador de efectos.
“Puras” en el diagrama se refiere a funciones puras, funciones cuya salida depende solo de sus entradas y cuya ejecución no tiene efectos secundarios observables. Para ver Funciones puras vs impuras para más detalles.
(Fuente: Spotify / Mobius – Conceptos> Mobius Loop)
Combinando este concepto con las capacidades MPP de Kotlin, mobius.kt le permite escribir y probar todas sus funciones puras (aplicación y / o lógica empresarial) en Kotlin e implementarlas en todas partes. Esto deja las funciones impuras para la plataforma nativa, que puede escribirse en su idioma principal (Js, Java, Objective-c / Swift) o en Kotlin.
Ejemplo
typealias Model = Int
enum class Event { ADD, SUB, RESET }
typealias Effect = Unit
val update = Update<Model, Event, Effect> { model, event ->
when (event) {
Event.ADD -> next(model + 1)
Event.SUB -> next(model - 1)
Event.RESET -> next(0)
}
}
val effectHandler = Connectable<Effect, Event> { output ->
object : Connection<Effect> {
override fun accept(value: Effect) = Unit
override fun dispose() = Unit
}
}
val loopFactory = Mobius.loop(update, effectHandler)
Para crear un bucle simple, use loopFactory.startFrom(model)
que devuelve un MobiusLoop
con dos estados: funcionando y dispuesto.
Ejemplo de bucle simple (haga clic para expandir)
val loop = loopFactory.startFrom(0)
val observerRef: Disposable = loop.observer { model ->
println("Model: $model")
}
loop.dispatchEvent(Event.ADD) // Model: 1
loop.dispatchEvent(Event.ADD) // Model: 2
loop.dispatchEvent(Event.RESET) // Model: 0
loop.dispatchEvent(Event.SUB) // Model: -1
loop.dispose()
Alternativamente, un bucle se puede gestionar con un MobiusLoop.Controller
, dando al ciclo un ciclo de vida más flexible.
Ejemplo de controlador de bucle (haga clic para expandir)
val loopController = Mobius.controller(loopFactory, 0)
loopController.connect { output ->
buttonAdd.onClick { output.accept(Event.ADD) }
buttonSub.onClick { output.accept(Event.SUB) }
buttonReset.onClick { output.accept(Event.RESET) }
object : Consumer<Model> {
override fun accept(value: Model) {
println(value.toString())
}
override fun dispose() {
buttonAdd.removeOnClick()
buttonSub.removeOnClick()
buttonReset.removeOnClick()
}
}
}
loopController.start()
loopController.dispatchEvent(Event.ADD) // Output: 1
loopController.dispatchEvent(Event.ADD) // Output: 2
loopController.dispatchEvent(Event.RESET) // Output: 0
loopController.dispatchEvent(Event.SUB) // Output: -1
loopController.stop()
// Loop could be started again with `loopController.start()`
loopController.disconnect()
Notas
Dependencias externas
Mobius.kt depende de kotlinx.atomicfu para la sincronización de objetos, esto da como resultado una dependencia del tiempo de ejecución solo para objetivos Kotlin / Native.
Soporte de corrutina
Coroutines y streams son compatibles con el mobiuskt-coroutines
módulo (ver Descargar).
Ejemplo de módulo de rutina (haga clic para expandir)
val effectHandler = subtypeEffectHandler<Effect, Event> {
// suspend () -> Unit
addAction<Effect.SubType1> { }
// suspend (Effect) -> Unit
addConsumer<Effect.SubType2> { effect -> }
// suspend (Effect) -> Event
addFunction<Effect.SubType3> { effect -> Event.Result() }
// FlowCollector<Event>.(Effect) -> Unit
addValueCollector<Effect.SubType4> { effect ->
emit(Event.Result())
emitAll(createEventFlow())
}
addLatestValueCollector<Effect.SubType5> {
// Like `addValueCollector` but cancels the previous
// running work when a new Effect instance arrives.
}
// Transform Flow<Effect> into Flow<Event>
addTransformer<Effect.SubType6> { effects ->
effects.map { effect -> Event.Result() }
}
}
val loopFactory = FlowMobius.loop(update, effectHandler)
Ayuda de idioma
MobiusLoop
se pueden crear y administrar en código Javascript, Swift y Java sin mayores problemas de interoperabilidad. El uso de Mobius.kt para la lógica compartida no requiere que se escriban proyectos de consumo o que conozcan a Kotlin.
Kotlin / nativo
Kotlin / nativos nuevo administrador de memoria generalmente es compatible, pero puede conducir a un mayor uso de la memoria y, en casos raros, a errores de tiempo de ejecución retrasados. Las siguientes notas solo son relevantes para el administrador de memoria original donde el estado compartido no se puede cambiar.
A MobiusLoop
es de un solo subproceso en destinos nativos y no se puede congelado. Este es un comportamiento generalmente aceptable, incluso cuando el bucle existe en el hilo principal. Si es necesario, los gerentes de facturas son responsables del cambio. Effect
s en e Event
está fuera de un hilo de fondo.
Coroutines y Streams son ideales para pasar efectos de fondo con el mobiuskt-coroutines
formulario o manual de muestra a continuación.
Ejemplo de corrutina (haga clic para expandir)
Connectable<Effect, Event> { output: Consumer<Event> ->
object : Connection<Effect> {
// Use a dispatcher for the Loop's thread, i.e. Dispatcher.Main
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private val effectFlow = MutableSharedFlow<Effect.Subtype2>(
onBufferOverflow = BufferOverflow.SUSPEND
)
init {
effectFlow
.debounce(200)
.mapLatest { effect -> handleSubtype2(effect) }
.onEach { event -> output.accept(event) }
.launchIn(scope)
}
override fun accept(value: Effect) {
scope.launch {
when (value) {
is Effect.Subtype1 -> output.accept(handleSubtype1(value))
is Effect.Subtype2 -> effectFlow.emit(value)
}
}
}
override fun dispose() {
scope.cancel()
}
private suspend fun handleSubtype1(effect: Effect.Subtype1): Event {
return withContext(Dispatcher.Default) {
// Captured variables are automatically frozen, DO NOT access `output` here!
try {
val result = longRunningSuspendFun(effect.data)
Event.Success(result)
} catch (e: Throwable) {
Event.Error(e)
}
}
}
private suspend fun handleSubtype2(effect: Effect.Subtype2): Event {
return withDispatcher(Dispatcher.Default) {
try {
val result = throttledSuspendFun(effect.data)
Event.Success(result)
} catch (e: Throwable) {
Event.Error(e)
}
}
}
}
}
Descargar
repositories {
mavenCentral()
// Or snapshots
maven("https://s01.oss.sonatype.org/content/repositories/snapshots/")
}
dependencies {
implementation("org.drewcarlson:mobiuskt-core:$MOBIUS_VERSION")
implementation("org.drewcarlson:mobiuskt-extras:$MOBIUS_VERSION")
implementation("org.drewcarlson:mobiuskt-android:$MOBIUS_VERSION")
implementation("org.drewcarlson:mobiuskt-coroutines:$MOBIUS_VERSION")
}
.