Security News
Weekly Downloads Now Available in npm Package Search Results
Socket's package search now displays weekly downloads for npm packages, helping developers quickly assess popularity and make more informed decisions.
org.drewcarlson:mobiuskt-core-watchosx86
Advanced tools
Kotlin multiplatform state managment framework.
Kotlin Multiplatform framework for managing state evolution and side-effects, based on spotify/Mobius.
The core construct provided by Mobius is the Mobius Loop, best described by the official documentation. (Embedded below)
A Mobius loop is a part of an application, usually including a user interface. In a Spotify context, there is usually one loop per feature such as “the album page”, “login flow”, etc., but a loop can also be UI-less and for instance be tied to the lifecycle of an application or a user session.
A Mobius loop receives Events, which are passed to an Update function together with the current Model. As a result of running the Update function, the Model might change, and Effects might get dispatched. The Model can be observed by the user interface, and the Effects are received and executed by an Effect Handler.
'Pure' in the diagram refers to pure functions, functions whose output only depends on their inputs, and whose execution has no observable side effects. See Pure vs Impure Functions for more details.
(Source: Spotify/Mobius - Concepts > Mobius Loop)
By combining Mobius Loops with Kotlin's MPP features, mobius.kt allows you to write and test pure functions (application and/or business logic) in Kotlin and deploy them everywhere. This leaves impure functions to be written in multiplatform Kotlin code or the target platform's primary language (Js, Java, Objective-c/Swift), depending on your use-case.
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)
To create a simple loop use loopFactory.startFrom(model)
which returns a MobiusLoop
with two states: running and disposed.
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()
Alternatively a loop can be managed with a MobiusLoop.Controller
, giving the loop a more flexible lifecycle.
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()
The mobiuskt-test
module provides a DSL for behavior driven tests and a light re-implementation of Hamcrest style APIs to test mobius loops (See Download).
// Note that `update` is from the README example above
UpdateSpec(update)
.given(0) // given model of 0
.whenEvent(Event.ADD) // when Event.Add occurs
.then(assertThatNext(hasModel())) // assert the Next object contains any model
// No AssertionError, test passed.
UpdateSpec(update)
.given(0)
.whenEvent(Event.ADD)
.then(assertThatNext(hasModel(-1)))
// AssertionError: expected -1 but received 1, test failed.
For more details on the available matchers, see the API documentation.
Coroutines and Flows are supported with the mobiuskt-coroutines
module (See Download).
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)
Using KSP, mobiuskt-update-generator
provides code generation to reduce manual boilerplate when writing complex Update
functions.
Given a sealed class Event
declaration, this module generates an interface defining update methods for each Event
subclass and an exhaustive when
block in the update
method.
See the following example loop components with the @GenerateUpdate
annotation applied to the Update function class definition, including the TestGeneratedUpdate
parent:
data class TestModel(
val counter: Int,
)
sealed class TestEvent {
object Increment : TestEvent()
object Decrement : TestEvent()
data class SetValue(val newCounter: Int) : TestEvent()
}
sealed class TestEffect {}
@GenerateUpdate
object TestUpdate : Update<TestModel, TestEvent, TestEffect>, TestGeneratedUpdate {
// ...
}
interface TestGeneratedUpdate : Update<TestModel, TestEvent, TestEffect> {
override fun update(model: TestModel, event: TestEvent): Next<TestModel, TestEffect> {
return when (event) {
TestEvent.Increment -> increment(model)
TestEvent.Decrement -> decrement(model)
is TestEvent.SetValue -> setValue(model, event)
}
}
fun increment(model: TestModel): Next<TestModel, TestEffect>
fun decrement(model: TestModel): Next<TestModel, TestEffect>
fun setValue(model: TestModel, event: TestEvent.SetValue): Next<TestModel, TestEffect>
}
Use the following kts gradle configuration to apply the Update generator in your project:
plugins {
kotlin("jvm") // or kotlin("android")
id("com.google.devtools.ksp") version "<KSP-Version>"
}
kotlin {
sourceSets.main {
kotlin.srcDir("build/generated/ksp/$name/kotlin")
}
}
dependencies {
implementation("org.drewcarlson:mobiuskt-update-generator-api:$mobiuskt_version")
ksp("org.drewcarlson:mobiuskt-update-generator:$mobiuskt_version")
}
plugins {
kotlin("multiplatform")
id("com.google.devtools.ksp") version "<KSP-Version>"
}
kotlin {
sourceSets {
val commonMain by getting {
kotlin.srcDir("build/generated/ksp/$name/kotlin")
dependencies {
implementation("org.drewcarlson:mobiuskt-update-generator-api:$mobiuskt_version")
}
}
}
}
// Note this must be in a top-level `dependencies` block, not `kotlin { sourceSets { .. } }`
dependencies {
add("kspMetadata", "org.drewcarlson:mobiuskt-update-generator:$mobiuskt_version")
}
// This ensures that when compiling for any target, your `commonMain` sources are
// scanned and code is generated to `build/generated/ksp/commonMain` instead of a
// directory for the specific target. See https://github.com/google/ksp/issues/567
if (plugins.hasPlugin("com.google.devtools.ksp")) {
val ktCompileTasks = tasks.withType<org.jetbrains.kotlin.gradle.dsl.KotlinCompile<*>>()
val jvmCompileTasks = tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile>()
(ktCompileTasks + jvmCompileTasks).forEach { task ->
if (task.name != "kspCommonMainKotlinMetadata") {
task.dependsOn("kspCommonMainKotlinMetadata")
}
}
}
For more details see the official KSP documentation.
Mobius.kt depends on kotlinx.atomicfu for object synchronization, this results in a runtime dependency for Kotlin/Native targets only.
MobiusLoop
s can be created and managed in Javascript, Swift, and Java code without major interoperability concerns.
Using Mobius.kt for shared logic does not require consuming projects to be written in or know about Kotlin.
Kopykat: When writing Update
functions you will typically use the copy
method
provided by data class
es to create updated model instances. The standard copy
method is adequate in simple cases but
can quickly clutter your Update
functions. Kopykat provides generated builder copy
methods which provide instance
variables to set instead of a long list of function parameters.
redacted-compiler-plugin:
data class
es provide a toString
in Model classes which make Logging simple and useful in Mobius.kt.
When Model's contain sensitive information you do not want logged, overriding and keeping the toString
method updated
is tedious. With Redacted, you can annotate individual properties with @Redacted
to omit the actual data from the
standard toString
implementation.
Mobius.kt supports Kotlin/Native's new memory manager and as of Kotlin 1.7.20 it is enabled by default. The following notes are relevant only to the original memory manager where state shared across threads cannot be mutated.
A MobiusLoop
is single-threaded on native targets and cannot be frozen.
Generally this is acceptable behavior, even when the loop exists on the main thread.
If required, Effect Handlers are responsible for passing Effect
s into and Event
s out of a background thread.
Coroutines and Flows are ideal for handing Effects in the background with the mobiuskt-coroutines
module or manual example below.
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)
}
}
}
}
}
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-test:$MOBIUS_VERSION")
implementation("org.drewcarlson:mobiuskt-extras:$MOBIUS_VERSION")
implementation("org.drewcarlson:mobiuskt-coroutines:$MOBIUS_VERSION")
// Update Spec Generator:
implementation("org.drewcarlson:mobiuskt-update-generator-api:$mobiuskt_version")
ksp("org.drewcarlson:mobiuskt-update-generator:$mobiuskt_version")
}
[versions]
mobiuskt = "1.0.0-rc02"
[libraries]
mobiuskt-core = { module = "org.drewcarlson:mobiuskt-core", version.ref = "mobiuskt" }
mobiuskt-test = { module = "org.drewcarlson:mobiuskt-test", version.ref = "mobiuskt" }
mobiuskt-extras = { module = "org.drewcarlson:mobiuskt-extras", version.ref = "mobiuskt" }
mobiuskt-coroutines = { module = "org.drewcarlson:mobiuskt-coroutines", version.ref = "mobiuskt" }
mobiuskt-updateGenerator = { module = "org.drewcarlson:mobiuskt-update-generator", version.ref = "mobiuskt" }
mobiuskt-updateGenerator-api = { module = "org.drewcarlson:mobiuskt-update-generator-api", version.ref = "mobiuskt" }
FAQs
Kotlin multiplatform state managment framework.
We found that org.drewcarlson:mobiuskt-core-watchosx86 demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Socket's package search now displays weekly downloads for npm packages, helping developers quickly assess popularity and make more informed decisions.
Security News
A Stanford study reveals 9.5% of engineers contribute almost nothing, costing tech $90B annually, with remote work fueling the rise of "ghost engineers."
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.