diff --git a/README.md b/README.md index 33fb18e4..8cf06394 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ FlowMVI is a Kotlin Multiplatform architectural framework based on coroutines. It enables you to extend your business logic with reusable plugins, handle errors, -achieve thread-safety, and more. It takes about 10 minutes to try it. +achieve thread-safety, and more. It takes about 10 minutes and 50 lines of code to get started. ## Quickstart: @@ -90,61 +90,80 @@ Instead, this library focuses on building a supporting infrastructure to enable Here's what you get: * Powerful Plug-In system to automate processes and **reuse any business logic** you desire - * Create automatic analytics handlers, websocket connections, error handling mechanisms, or anything else once and - reuse them throughout your whole project automatically -* Automatically **recover from any errors** and prevent crashes +* Automatically **recover from any errors** and report them to analytics. * Build fully **async, reactive and parallel apps** - with no manual thread synchronization required! -* Create business logic components with pluggable UI using **0 platform code** +* Create **multiplatform business logic** components with pluggable UI * Automatic multiplatform system **lifecycle handling** -* Out of the box **debugging, logging, testing, undo/redo, caching and long-running tasks** support -* Debounce, retry, batch, throttle, conflate any operations automatically. +* Out of the box **debugging, logging, caching and long-running tasks** support +* Debounce, retry, batch, throttle, conflate, monitor, **modify any operations** automatically * **Compress, persist, and restore state** automatically on any platform -* No base classes, complicated interfaces, or factories of factories - logic is **declarative and built with a DSL** -* Restartable, reusable business logic components with no external dependencies or dedicated lifecycles. -* Create compile-time safe state machines with a readable DSL. Forget about casts, inconsistent states, and `null`s -* First class Compose Multiplatform support optimized for performance and ease of use -* Use both MVVM+ (functional) or MVI (model-driven) style of programming -* Share, distribute, or disable side-effects based on your team's needs -* Dedicated remote debugger and code generation IDEA/AS plugin and app for Windows, Linux, MacOS -* Integration with popular libraries, such as [Decompose (Essenty)](https://github.com/arkivanov/Decompose) -* The core library depends on kotlin coroutines. Nothing else -* Core library is fully covered by tests -* Minimal performance overhead, equal to using a simple Channel, with regular benchmarking +* **No base classes, complicated interfaces**, or factories of factories - logic is declarative and built with a DSL +* Build **Restartable, reusable business logic components** with no external dependencies or dedicated lifecycles +* Create **compile-time safe state machines** with a readable DSL. Forget about casts, inconsistent states, and `null`s +* First class **Compose Multiplatform support** optimized for performance and ease of use +* Use both **MVVM+** (functional) or **MVI** (model-driven) style of programming +* Share, distribute, disable, **manage side-effects** based on your team's needs +* Dedicated **IDE Plugin for debugging and codegen** and app for Windows, Linux, MacOS +* **Integration with popular libraries**, such as [Decompose (Essenty)](https://github.com/arkivanov/Decompose) +* The core **library has no dependencies** except kotlin coroutines. +* Core library is fully covered by **hundreds of tests** +* **Minimal performance overhead**, equal to using a simple Channel, with regular benchmarking +* Collect, monitor and report **performance metrics** automatically (upcoming). +* **Test any business logic** using clean, declarative DSL. * Learn more by exploring the [sample app](https://opensource.respawn.pro/FlowMVI/sample/) in your browser ## How does it look? -
-Define a contract +It is insanely **easy to get started**. All you have to do is: + +### 1. Define a contract: ```kotlin -sealed interface CounterState : MVIState { - data object Loading : CounterState - data class Error(val e: Exception) : CounterState - - @Serializable - data class DisplayingCounter( - val timer: Int, - val counter: Int, - ) : CounterState +data class State( + val counter: Int = 0, +) : MVIState + +sealed interface Intent : MVIIntent { + data object ClickedCounter : Intent } -sealed interface CounterIntent : MVIIntent { - data object ClickedCounter : CounterIntent +// or use mvvm+ style +typealias IntentIntent = LambdaIntent + +sealed interface Action : MVIAction { + data class ShowMessage(val message: String) : Action } +``` + +### 2. Declare your business logic: -sealed interface CounterAction : MVIAction { - data class ShowMessage(val message: String) : CounterAction +```kotlin +val store = store<_, _>(initial = State(), scope = coroutineScope) { + + reduce { intent -> + when (intent) { + is ClickedCounter -> updateState { + action(ShowMessage("Added to counter!")) + + copy(counter = counter + 1) + } + } + } } + +store.intent(ClickedCounter) + ``` -
+Want to have advanced configuration with tons of features, persistent state, or interceptors? +No problem, your logic's complexity now scales **linearly**. Adding a new feature is as simple as calling a function. -Then define your business logic: +
+Example advanced configuration ```kotlin class CounterContainer( - private val repo: CounterRepository, + private val repo: CounterRepository, // inject dependencies ) { val store = store(initial = Loading) { @@ -152,39 +171,40 @@ class CounterContainer( actionShareBehavior = ActionShareBehavior.Distribute() debuggable = true - // makes the store fully async, parallel and thread-safe + // make the store fully async, parallel and thread-safe parallelIntents = true coroutineContext = Dispatchers.Default atomicStateUpdates = true } + // out of the box logging and IDE debugging enableLogging() enableRemoteDebugging() - // allows to undo any operation + // undo / redo any operation val undoRedo = undoRedo() - // manages long-running jobs + // manage long-running jobs val jobManager = manageJobs() - // saves and restores the state automatically + // save and restore the state automatically serializeState( path = repo.cacheFile("counter"), serializer = DisplayingCounter.serializer(), ) - // performs long-running tasks on startup + // perform long-running tasks on startup init { repo.startTimer() } - // handles any errors + // handle any errors recover { e: Exception -> action(ShowMessage(e.message)) null } - // saves resources when there are no subscribers + // observe streams and save resources when there are no subscribers whileSubscribed { repo.timer.collect { updateState { @@ -193,7 +213,7 @@ class CounterContainer( } } - // lazily evaluates and caches values, even when the method is suspending. + // lazily evaluate and cache values, even when the method is suspending. val pagingData by cache { repo.getPagedDataSuspending() } @@ -201,21 +221,26 @@ class CounterContainer( // testable reducer as a function reduce { intent: CounterIntent -> when (intent) { + // typed state update prevents races and allows using sealed class hierarchies for LCE is ClickedCounter -> updateState { copy(counter = counter + 1) } } } - // builds custom plugins on the fly + // build custom plugins on the fly install { onStop { repo.stopTimer() } } + + // and 50+ more options to choose from... } } ``` -### Extend your logic with plugins! +
+ +### 3. Extend your logic with plugins! Powerful DSL allows you to hook into various events and amend any part of your logic: @@ -230,12 +255,6 @@ fun analyticsPlugin(analytics: Analytics) = plugin analytics.logError(e) } - onSubscribe { - analytics.logEngagementStart() - } - onUnsubscribe { - analytics.logEngagementEnd() - } onStop { analytics.logScreenLeave() } @@ -244,7 +263,7 @@ fun analyticsPlugin(analytics: Analytics) = plugin store( } } +/** + * * Build a new [Store] using [StoreBuilder] but disallow using [MVIAction]s. + * The store is **not** launched, but is created eagerly, with all its plugins. + * + * If your code doesn't compile, you are looking for another overload with three type parameters, i.e: + * `store<_, _, _>()` + */ +@FlowMVIDSL +@JvmName("noActionStore") +// https://youtrack.jetbrains.com/issue/KT-16255 +@Suppress( + "INVISIBLE_MEMBER", + "INVISIBLE_REFERENCE", +) +@kotlin.internal.LowPriorityInOverloadResolution +public inline fun store( + initial: S, + scope: CoroutineScope, + @BuilderInference configure: BuildStore, +): Store = store(initial, scope) { + configure() + configure { + actionShareBehavior = ActionShareBehavior.Disabled + } +} + /** * Build a new [Store] using [StoreBuilder]. * The store is created lazily, with all its plugins. @@ -80,3 +107,6 @@ public inline fun lazyStore( mode: LazyThreadSafetyMode = LazyThreadSafetyMode.SYNCHRONIZED, @BuilderInference crossinline configure: BuildStore, ): Lazy> = lazy(mode) { store(initial, configure).apply { start(scope) } } + +public inline val Store.immutable: ImmutableStore + get() = this diff --git a/core/src/commonMain/kotlin/pro/respawn/flowmvi/util/CappedMutableList.kt b/core/src/commonMain/kotlin/pro/respawn/flowmvi/util/CappedMutableList.kt index f69ceabd..a01b941f 100644 --- a/core/src/commonMain/kotlin/pro/respawn/flowmvi/util/CappedMutableList.kt +++ b/core/src/commonMain/kotlin/pro/respawn/flowmvi/util/CappedMutableList.kt @@ -35,8 +35,7 @@ internal class CappedMutableList( } private fun removeOverflowing() { - while (size > maxSize) { - removeFirst() - } + // do not use removeFirst because of https://jakewharton.com/kotlins-jdk-release-compatibility-flag/ + while (size > maxSize) removeAt(0) } } diff --git a/debugger/ideplugin/build.gradle.kts b/debugger/ideplugin/build.gradle.kts index 899296d5..16ae1d52 100644 --- a/debugger/ideplugin/build.gradle.kts +++ b/debugger/ideplugin/build.gradle.kts @@ -41,7 +41,7 @@ intellijPlatform { pluginVerification { ides { props["plugin.local.ide.path"]?.toString()?.let(::local) ?: ide( - IntelliJPlatformType.AndroidStudio, + IntelliJPlatformType.IntellijIdeaCommunity, libs.versions.intellij.idea.get() ) } diff --git a/docs/plugins/prebuilt.md b/docs/plugins/prebuilt.md index 17639011..a717baa8 100644 --- a/docs/plugins/prebuilt.md +++ b/docs/plugins/prebuilt.md @@ -1,5 +1,15 @@ # Getting started with plugins +FlowMVI is built entirely based on Plugins! +Plugins form a chain of responsibility (called _Pipeline_) and +execute _in the order they were installed_ into the Store. +This allows you to assemble business logic like a lego by placing the "bricks" in the order you want, and transparently +inject some logic into any store at any point. + +Here's how the Plugin chain works: + +![](../images/chart.png) + ## Plugin Ordering !> The order of plugins matters! Changing the order of plugins may completely change how your store works. diff --git a/docs/quickstart.md b/docs/quickstart.md index 33e0fa9c..89da8527 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -14,8 +14,6 @@ First of all, here's how the library works: ---- -![](images/chart.png) - ## Step 1: Configure the library ### 1.1: Add dependencies