Skip to content

Commit

Permalink
3.1.0-beta06 (#120)
Browse files Browse the repository at this point in the history
  • Loading branch information
Nek-12 authored Dec 17, 2024
2 parents 5494041 + 17ad83d commit 13ccefa
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 68 deletions.
134 changes: 78 additions & 56 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -90,101 +90,121 @@ 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?

<details>
<summary>Define a contract</summary>
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<State, Action>

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)

```

</details>
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:
<details>
<summary>Example advanced configuration</summary>

```kotlin
class CounterContainer(
private val repo: CounterRepository,
private val repo: CounterRepository, // inject dependencies
) {
val store = store<CounterState, CounterIntent, CounterAction>(initial = Loading) {

configure {
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<DisplayingCounter, _> {
Expand All @@ -193,29 +213,34 @@ 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()
}

// 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<DisplayingCounter, _> {
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!
</details>

### 3. Extend your logic with plugins!

Powerful DSL allows you to hook into various events and amend any part of your logic:

Expand All @@ -230,12 +255,6 @@ fun analyticsPlugin(analytics: Analytics) = plugin<MVIState, MVIIntent, MVIActio
onException { e ->
analytics.logError(e)
}
onSubscribe {
analytics.logEngagementStart()
}
onUnsubscribe {
analytics.logEngagementEnd()
}
onStop {
analytics.logScreenLeave()
}
Expand All @@ -244,7 +263,7 @@ fun analyticsPlugin(analytics: Analytics) = plugin<MVIState, MVIIntent, MVIActio

Never write analytics, debugging, or state persistence code again.

### Compose Multiplatform:
### 4. Build UI using Compose:

![badge][badge-android] ![badge][badge-ios] ![badge][badge-mac] ![badge][badge-jvm] ![badge][badge-wasm] ![badge][badge-js]

Expand Down Expand Up @@ -274,7 +293,7 @@ fun CounterScreen() {

Enjoy testable UI and free `@Previews`.

### Android support:
### 5. Android support:

No more subclassing `ViewModel`. Use `StoreViewModel` instead and make your business logic multiplatform.

Expand Down Expand Up @@ -305,7 +324,7 @@ class ScreenFragment : Fragment() {

## Testing DSL

Finally stop doing UI tests and replace them with unit tests:
Finally stop writing UI tests and replace them with unit tests:

### Test Stores

Expand Down Expand Up @@ -346,6 +365,8 @@ timerPlugin(timer).test(Loading) {

[![Plugin](https://img.shields.io/jetbrains/plugin/v/25766?style=flat)](https://plugins.jetbrains.com/plugin/25766-flowmvi)

IDE plugin generates entire features in 4 keystrokes and lets you debug and control your app remotely:

https://github.com/user-attachments/assets/05f8efdb-d125-4c4a-9bda-79875f22578f

## People love the library:
Expand All @@ -360,7 +381,8 @@ https://github.com/user-attachments/assets/05f8efdb-d125-4c4a-9bda-79875f22578f

## Ready to try?

Start with reading the [Quickstart Guide](https://opensource.respawn.pro/FlowMVI/#/quickstart).
It takes 10 minutes to get started. Begin by reading
the [Quickstart Guide](https://opensource.respawn.pro/FlowMVI/#/quickstart).

----

Expand Down
13 changes: 7 additions & 6 deletions buildSrc/src/main/kotlin/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ object Config {
const val minorRelease = 1
const val patch = 0
const val postfix = "-beta05" // include dash (-)

const val versionCode = 8

const val majorVersionName = "$majorRelease.$minorRelease.$patch"
const val versionName = "$majorVersionName$postfix"
const val url = "https://github.com/respawn-app/FlowMVI"
Expand All @@ -39,7 +39,8 @@ object Config {
const val vendorId = "respawn-app"
const val name = "FlowMVI"

// endregion
val jvmTarget = JvmTarget.JVM_11
val javaVersion = JavaVersion.VERSION_11
val optIns = listOf(
"kotlinx.coroutines.ExperimentalCoroutinesApi",
"kotlinx.coroutines.FlowPreview",
Expand All @@ -64,16 +65,14 @@ object Config {
add("-Xcontext-receivers")
add("-Xstring-concat=inline")
add("-Xlambdas=indy")
add("-Xjdk-release=${jvmTarget.target}")
}

val jvmTarget = JvmTarget.JVM_11
val javaVersion = JavaVersion.VERSION_11
// android
const val compileSdk = 35
const val targetSdk = compileSdk
const val minSdk = 21
const val appMinSdk = 26

// android
const val namespace = artifactId
const val testRunner = "androidx.test.runner.AndroidJUnitRunner"
const val isMinifyEnabledRelease = false
Expand Down Expand Up @@ -140,4 +139,6 @@ Note - the plugin's version (latest is $versionName) is synced with the version
If you are using a severely outdated version of either library or the plugin, you may run into issues.
"""
}

// endregion
}
30 changes: 30 additions & 0 deletions core/src/commonMain/kotlin/pro/respawn/flowmvi/dsl/StoreDsl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package pro.respawn.flowmvi.dsl
import kotlinx.coroutines.CoroutineScope
import pro.respawn.flowmvi.api.ActionShareBehavior
import pro.respawn.flowmvi.api.FlowMVIDSL
import pro.respawn.flowmvi.api.ImmutableStore
import pro.respawn.flowmvi.api.MVIAction
import pro.respawn.flowmvi.api.MVIIntent
import pro.respawn.flowmvi.api.MVIState
Expand Down Expand Up @@ -57,6 +58,32 @@ public inline fun <S : MVIState, I : MVIIntent> 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 <S : MVIState, I : MVIIntent> store(
initial: S,
scope: CoroutineScope,
@BuilderInference configure: BuildStore<S, I, Nothing>,
): Store<S, I, Nothing> = store(initial, scope) {
configure()
configure {
actionShareBehavior = ActionShareBehavior.Disabled
}
}

/**
* Build a new [Store] using [StoreBuilder].
* The store is created lazily, with all its plugins.
Expand All @@ -80,3 +107,6 @@ public inline fun <S : MVIState, I : MVIIntent, A : MVIAction> lazyStore(
mode: LazyThreadSafetyMode = LazyThreadSafetyMode.SYNCHRONIZED,
@BuilderInference crossinline configure: BuildStore<S, I, A>,
): Lazy<Store<S, I, A>> = lazy(mode) { store(initial, configure).apply { start(scope) } }

public inline val <S : MVIState, I : MVIIntent, A : MVIAction> Store<S, I, A>.immutable: ImmutableStore<S, I, A>
get() = this
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ internal class CappedMutableList<T>(
}

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)
}
}
2 changes: 1 addition & 1 deletion debugger/ideplugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
)
}
Expand Down
10 changes: 10 additions & 0 deletions docs/plugins/prebuilt.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Loading

0 comments on commit 13ccefa

Please sign in to comment.