All Projects → DrewCarlson → mobius.kt

DrewCarlson / mobius.kt

Licence: Apache-2.0 license
Kotlin Multiplatform framework for managing state evolution and side-effects

Programming Languages

kotlin
9241 projects

Projects that are alternatives of or similar to mobius.kt

Notflix
Kotlin Multiplatform playground
Stars: ✭ 272 (+597.44%)
Mutual labels:  kotlin-coroutines, kotlin-multiplatform
KMP-NativeCoroutines
Library to use Kotlin Coroutines from Swift code in KMP apps
Stars: ✭ 502 (+1187.18%)
Mutual labels:  kotlin-coroutines, kotlin-multiplatform
tv-maniac
Tv-Maniac is a Multiplatform app (Android & iOS) for viewing TV Shows from TMDB.
Stars: ✭ 55 (+41.03%)
Mutual labels:  kotlin-coroutines, kotlin-multiplatform
sweekt
🍭 Some sugar to sweeten Kotlin development.
Stars: ✭ 35 (-10.26%)
Mutual labels:  kotlin-coroutines, kotlin-multiplatform
Penicillin
Modern powerful Twitter API wrapper for Kotlin Multiplatform. #PureKotlin
Stars: ✭ 91 (+133.33%)
Mutual labels:  kotlin-coroutines, kotlin-multiplatform
Splitties
A collection of hand-crafted extensions for your Kotlin projects.
Stars: ✭ 1,945 (+4887.18%)
Mutual labels:  kotlin-coroutines, kotlin-multiplatform
kotlin-everywhere
Kotlin/Everywhere Beijing 2019
Stars: ✭ 31 (-20.51%)
Mutual labels:  kotlin-coroutines, kotlin-multiplatform
Eiffel
Redux-inspired Android architecture library leveraging Architecture Components and Kotlin Coroutines
Stars: ✭ 203 (+420.51%)
Mutual labels:  state-management, kotlin-coroutines
Compressor
An android image compression library.
Stars: ✭ 6,745 (+17194.87%)
Mutual labels:  kotlin-coroutines
StarWars
Minimal GraphQL based Jetpack Compose, Wear Compose and SwiftUI Kotlin Multiplatform sample (using StarWars endpoint - https://graphql.org/swapi-graphql)
Stars: ✭ 165 (+323.08%)
Mutual labels:  kotlin-multiplatform
random-users-details
Random Users details in flutter from randomusers api
Stars: ✭ 14 (-64.1%)
Mutual labels:  state-management
blueprint
Architectural frameworks and toolkits for bootstrapping modern Android codebases.
Stars: ✭ 57 (+46.15%)
Mutual labels:  kotlin-coroutines
xoid
Framework-agnostic state management library designed for simplicity and scalability ⚛
Stars: ✭ 96 (+146.15%)
Mutual labels:  state-management
vue-class-state
面向对象风格的vue状态管理
Stars: ✭ 14 (-64.1%)
Mutual labels:  state-management
Delish
Delish, a Food Recipes App in Jetpack Compose and Hilt based on modern Android tech-stacks and MVI clean architecture.
Stars: ✭ 356 (+812.82%)
Mutual labels:  kotlin-coroutines
react-context
(つ°ヮ°)つ Understanding React Context
Stars: ✭ 11 (-71.79%)
Mutual labels:  state-management
trikot.patron
Kotlin Multiplatform Sample Project using Trikot libraries
Stars: ✭ 13 (-66.67%)
Mutual labels:  kotlin-multiplatform
kmm-production-sample
This is an open-source, mobile, cross-platform application built with Kotlin Multiplatform Mobile. It's a simple RSS reader, and you can download it from the App Store and Google Play. It's been designed to demonstrate how KMM can be used in real production projects.
Stars: ✭ 1,476 (+3684.62%)
Mutual labels:  kotlin-multiplatform
Scout
Scout is a kotlin multiplatform application that allows users to search and save games to lists to be browsed later.
Stars: ✭ 28 (-28.21%)
Mutual labels:  kotlin-multiplatform
LinuxCommandLibrary
1M+ downloads Linux reference app with basics, tips and formatted man pages
Stars: ✭ 333 (+753.85%)
Mutual labels:  kotlin-multiplatform

Mobius.kt

Maven Central Codecov

Kotlin Multiplatform framework for managing state evolution and side-effects, based on spotify/Mobius.

What is 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.

Mobius Loop

Mobius Loop Diagram

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.

Example

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.

Simple Loop Example (Click to expand)
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.

Loop Controller Example (Click to expand)
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()

Modules

Testing

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).

Behavior testing DSL Example (Click to expand)
// 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

Coroutines and Flows are supported with the mobiuskt-coroutines module (See Download).

Coroutine Module Example (Click to expand)
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)

Update Generator

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:

Loop components (Click to expand)
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 {
  // ...
}
Generated output (Click to expand)
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:

Kotlin Gradle Script - JVM/Android (Click to expand)
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")
}
Kotlin Gradle Script - Multiplatform (Click to expand)
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.

Notes

External dependencies

Mobius.kt depends on kotlinx.atomicfu for object synchronization, this results in a runtime dependency for Kotlin/Native targets only.

Language Support

MobiusLoops 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.

Useful Libraries

Kopykat: When writing Update functions you will typically use the copy method provided by data classes 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 classes 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.

Kotlin/Native

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 Effects into and Events 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.

Coroutine Example (Click to expand)
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)
                }
            }
        }
    }
}

Download

Maven Central Sonatype Nexus (Snapshots)

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")
}
Toml
[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" }
Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].