Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3.1.0 #123

Merged
merged 7 commits into from
Dec 23, 2024
Merged

3.1.0 #123

Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,12 @@ jobs:

- name: Generate docs
run: ./gradlew dokkaGenerate
continue-on-error: true

- name: Make javadoc dir
run: mkdir -p ./docs/javadocs

- name: Move docs to the parent docs dir
run: cp -r ./build/dokka/html ./docs/javadocs/
continue-on-error: true

- name: Create sample app distributable
run: ./gradlew wasmJsBrowserDistribution --no-configuration-cache
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ jobs:

- name: Publish new plugin version
run: ./gradlew debugger:ideplugin:publishPlugin --no-configuration-cache
continue-on-error: true # TODO: Remove once verified works
env:
CHANGELOG: ${{steps.build_changelog.outputs.changelog}}

Expand Down
6 changes: 6 additions & 0 deletions .idea/AndroidProjectSystem.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 8 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
![GitHub last commit](https://img.shields.io/github/last-commit/respawn-app/FlowMVI)
![Issues](https://img.shields.io/github/issues/respawn-app/FlowMVI)
![GitHub top language](https://img.shields.io/github/languages/top/respawn-app/flowMVI)
[![CodeFactor](https://www.codefactor.io/repository/github/respawn-app/flowMVI/badge)](https://www.codefactor.io/repository/github/respawn-app/flowMVI)
[![AndroidWeekly #563](https://androidweekly.net/issues/issue-563/badge)](https://androidweekly.net/issues/issue-563/)
[![Slack channel](https://img.shields.io/badge/Chat-Slack-orange.svg?style=flat&logo=slack)](https://kotlinlang.slack.com/messages/flowmvi/)

Expand Down Expand Up @@ -122,12 +121,12 @@ All you have to do is:

```kotlin
sealed interface State : MVIState {

data object Loading : State
data class Error(val e: Exception) : State
data class Content(val counter: Int = 0) : State
}


sealed interface Intent : MVIIntent {
data object ClickedCounter : Intent
}
Expand All @@ -141,25 +140,19 @@ sealed interface Action : MVIAction {

```kotlin
val counterStore = store(initial = State.Loading, scope = coroutineScope) {

install(analyticsPlugin) // install plugins you need

// install plugins you need
install(analyticsPlugin)

// recover from errors
recover { e: Exception ->
recover { e: Exception -> // recover from errors
updateState { State.Error(e) }
null
}

// load data
init {
init { // load data
updateState {
State.Content(counter = repository.loadCounter())
}
}

// respond to events
reduce { intent: Intent ->
reduce { intent: Intent -> // respond to events
when (intent) {
is ClickedCounter -> updateState<State.Content, _> {
action(ShowMessage("Incremented!"))
Expand Down Expand Up @@ -320,7 +313,7 @@ Enjoy testable UI and free `@Preview`s.

### Android Support

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

```kotlin
val module = module { // Koin example
Expand Down Expand Up @@ -412,7 +405,7 @@ Begin by reading the [Quickstart Guide](https://opensource.respawn.pro/FlowMVI/#
## License

```
Copyright 2022-2024 Respawn Team and contributors
Copyright 2022-2025 Respawn Team and contributors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ object Config {
const val majorRelease = 3
const val minorRelease = 1
const val patch = 0
const val postfix = "-beta06" // include dash (-)
const val postfix = "" // include dash (-)
const val versionCode = 9

const val majorVersionName = "$majorRelease.$minorRelease.$patch"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ 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 @@ -107,6 +106,3 @@ 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
@@ -0,0 +1,36 @@
package pro.respawn.flowmvi.test.store

import io.kotest.assertions.throwables.shouldNotThrowAny
import io.kotest.core.spec.style.FreeSpec
import pro.respawn.flowmvi.logging.log
import pro.respawn.flowmvi.plugins.init
import pro.respawn.flowmvi.plugins.recover
import pro.respawn.flowmvi.test.subscribeAndTest
import pro.respawn.flowmvi.util.asUnconfined
import pro.respawn.flowmvi.util.idle
import pro.respawn.flowmvi.util.testStore

class NestedRecoverTest : FreeSpec({
asUnconfined()
"Given a store that throws during state update in init" - {
val store = testStore {
init {
updateState {
throw IllegalArgumentException()
}
}
recover {
log { "Caught exception $it" }
null
}
}

"then the store must not throw" {
shouldNotThrowAny {
store.subscribeAndTest {
idle()
}
}
}
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.collections.shouldBeSingleton
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.nulls.shouldBeNull
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.CancellationException
Expand Down Expand Up @@ -46,6 +47,7 @@ class StoreExceptionsTest : FreeSpec({
idle()
plugin.starts shouldBe 1
plugin.exceptions.shouldContainExactly(e)
isActive shouldBe true
}
}

Expand Down Expand Up @@ -118,6 +120,9 @@ class StoreExceptionsTest : FreeSpec({
}
"and store that handles exceptions" - {
val store = testStore(plugin) {
init {
currentCoroutineContext()[RecoverModule].shouldBeNull()
}
recover {
currentCoroutineContext()[RecoverModule].shouldNotBeNull()
null
Expand Down
33 changes: 23 additions & 10 deletions debugger/ideplugin/src/main/resources/LiveTemplates.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,44 @@
<template name="fmvic" value="import pro.respawn.flowmvi.api.Container&#10;import pro.respawn.flowmvi.api.PipelineContext&#10;import pro.respawn.flowmvi.dsl.store&#10;import pro.respawn.flowmvi.dsl.updateState&#10;import pro.respawn.flowmvi.dsl.withState&#10;import pro.respawn.flowmvi.plugins.recover&#10;import pro.respawn.flowmvi.plugins.reduce&#10;&#10;private typealias Ctx = PipelineContext&lt;$NAME$State, $NAME$Intent, $NAME$Action&gt;&#10;&#10;internal class $NAME$Container(&#10; $PARAMS$&#10;) : Container&lt;$NAME$State, $NAME$Intent, $NAME$Action&gt; {&#10;&#10; override val store = store(initial = $NAME$State.Loading) {&#10; configure {&#10; name = &quot;$NAME$&quot;&#10; }&#10; recover {&#10; updateState { $NAME$State.Error(it) }&#10; null&#10; }&#10; reduce { intent -&gt;&#10; when(intent) {&#10; $END$&#10; else -&gt; TODO()&#10; }&#10; }&#10; }&#10;}" shortcut="ENTER" description="FlowMVI Container" toReformat="true" toShortenFQNames="true">
<variable name="NAME" expression="camelCase(String)" defaultValue="" alwaysStopAt="true" />
<variable name="PARAMS" expression="" defaultValue=" " alwaysStopAt="false" />
<context />
<context>
<option name="KOTLIN_TOPLEVEL" value="true" />
</context>
</template>
<template name="fmvis" value="import androidx.compose.runtime.Composable&#10;import pro.respawn.flowmvi.compose.dsl.subscribe&#10;import pro.respawn.flowmvi.compose.preview.EmptyReceiver&#10;import androidx.compose.runtime.getValue&#10;import pro.respawn.flowmvi.api.IntentReceiver&#10;&#10;@Composable&#10;fun $NAME$Screen(&#10; container: $NAME$Container,&#10;) = with(container.store) {&#10;&#10; val state by subscribe { action -&gt;&#10; when(action) {&#10; else -&gt; TODO()&#10; }&#10; }&#10;&#10; $NAME$ScreenContent(state)&#10;}&#10;&#10;@Composable&#10;private fun IntentReceiver&lt;$NAME$Intent&gt;.$NAME$ScreenContent(&#10; state: $NAME$State,&#10;) {&#10; when(state) {&#10; $END$&#10; else -&gt; TODO()&#10; }&#10;}&#10;&#10;@Composable&#10;@Preview&#10;private fun $NAME$ScreenPreview() = EmptyReceiver { &#10; $NAME$ScreenContent(TODO()) &#10;}&#10;" shortcut="ENTER" description="FlowMVI Composable Screen" toReformat="true" toShortenFQNames="true">
<template name="fmvis" value="@Composable&#10;fun $NAME$Screen(&#10; container: $NAME$Container,&#10;) = with(container.store) {&#10;&#10; val state by subscribe { action -&gt;&#10; when(action) {&#10; else -&gt; TODO()&#10; }&#10; }&#10;&#10; $NAME$ScreenContent(state)&#10;}&#10;&#10;@Composable&#10;private fun IntentReceiver&lt;$NAME$Intent&gt;.$NAME$ScreenContent(&#10; state: $NAME$State,&#10;) {&#10; when(state) {&#10; $END$&#10; else -&gt; TODO()&#10; }&#10;}&#10;&#10;@Composable&#10;@Preview&#10;private fun $NAME$ScreenPreview() = EmptyReceiver { &#10; $NAME$ScreenContent(TODO()) &#10;}&#10;" shortcut="ENTER" description="FlowMVI Composable Screen" toReformat="true" toShortenFQNames="true">
<variable name="NAME" expression="camelCase(String)" defaultValue="" alwaysStopAt="true" />
<context />
<context>
<option name="KOTLIN_TOPLEVEL" value="true" />
</context>
</template>
<template name="fmvip" value="/**&#10; * TODO: Add documentation&#10; **/&#10;@FlowMVIDSL&#10;fun &lt;S: MVIState, I: MVIIntent, A: MVIAction&gt; $NAME$Plugin(&#10; name: String? = &quot;$PluginName$Plugin&quot;,&#10;) = plugin&lt;S, I, A&gt; {&#10; this.name = name&#10; &#10; $END$&#10;}&#10;&#10;/**&#10; * Install a new [$NAME$Plugin]. &#10; * &#10;**/&#10;@FlowMVIDSL&#10;fun &lt;S: MVIState, I: MVIIntent, A: MVIAction&gt; StoreBuilder&lt;S, I, A&gt;.$NAME$(&#10; name: String? = &quot;$PluginName$Plugin&quot;,&#10;) = install($NAME$Plugin(name))" shortcut="ENTER" description="FlowMVI Plugin" toReformat="true" toShortenFQNames="true">
<template name="fmvip" value="/**&#10; * TODO: Add documentation&#10; **/&#10;@FlowMVIDSL&#10;fun &lt;S: MVIState, I: MVIIntent, A: MVIAction&gt; $NAME$Plugin(&#10; name: String? = &quot;$PluginName$Plugin&quot;,&#10;) = plugin&lt;S, I, A&gt; {&#10; this.name = name&#10; &#10; $END$&#10;}&#10;&#10;/**&#10; * Install a new [$NAME$Plugin].&#10;**/&#10;@FlowMVIDSL&#10;fun &lt;S: MVIState, I: MVIIntent, A: MVIAction&gt; StoreBuilder&lt;S, I, A&gt;.$NAME$(&#10; name: String? = &quot;$PluginName$Plugin&quot;,&#10;) = install($NAME$Plugin(name))" shortcut="ENTER" description="FlowMVI Plugin" toReformat="true" toShortenFQNames="true">
<variable name="NAME" expression="kotlinFunctionName()" defaultValue="" alwaysStopAt="true" />
<variable name="PluginName" expression="capitalize(NAME)" defaultValue="" alwaysStopAt="false" />
<context />
<context>
<option name="KOTLIN_TOPLEVEL" value="true" />
</context>
</template>
<template name="fmvilp" value="/**&#10; * TODO: Add documentation&#10; **/&#10;@FlowMVIDSL&#10;fun &lt;S: MVIState, I: MVIIntent, A: MVIAction&gt; $NAME$Plugin(&#10; name: String? = &quot;$PluginName$Plugin&quot;,&#10;) = lazyPlugin&lt;S, I, A&gt; {&#10; this.name = name&#10; &#10; $END$&#10;}&#10;&#10;/**&#10; * Install a new [$NAME$Plugin]. &#10; * &#10;**/&#10;@FlowMVIDSL&#10;fun &lt;S: MVIState, I: MVIIntent, A: MVIAction&gt; StoreBuilder&lt;S, I, A&gt;.$NAME$(&#10; name: String? = &quot;$PluginName$Plugin&quot;,&#10;) = install($NAME$Plugin(name))" shortcut="ENTER" description="FlowMVI Lazy Plugin" toReformat="true" toShortenFQNames="true">
<template name="fmvilp" value="/**&#10; * TODO: Add documentation&#10; **/&#10;@FlowMVIDSL&#10;fun &lt;S: MVIState, I: MVIIntent, A: MVIAction&gt; $NAME$Plugin(&#10; name: String? = &quot;$PluginName$Plugin&quot;,&#10;) = lazyPlugin&lt;S, I, A&gt; {&#10; this.name = name&#10; &#10; $END$&#10;}&#10;&#10;/**&#10; * Install a new [$NAME$Plugin]. &#10;**/&#10;@FlowMVIDSL&#10;fun &lt;S: MVIState, I: MVIIntent, A: MVIAction&gt; StoreBuilder&lt;S, I, A&gt;.$NAME$(&#10; name: String? = &quot;$PluginName$Plugin&quot;,&#10;) = install($NAME$Plugin(name))" shortcut="ENTER" description="FlowMVI Lazy Plugin" toReformat="true" toShortenFQNames="true">
<variable name="NAME" expression="kotlinFunctionName()" defaultValue="" alwaysStopAt="true" />
<variable name="PluginName" expression="capitalize(NAME)" defaultValue="" alwaysStopAt="false" />
<context />
<context>
<option name="KOTLIN_CLASS" value="true" />
<option name="KOTLIN_TOPLEVEL" value="true" />
</context>
</template>
<template name="fmvim" value="internal sealed interface $NAME$State : MVIState {&#10; data object Loading : $NAME$State&#10; data class Error(val e: Exception?) : $NAME$State&#10; $END$&#10;}&#10;&#10;internal sealed interface $NAME$Intent : MVIIntent {&#10;&#10;}&#10;&#10;internal sealed interface $NAME$Action : MVIAction {&#10;&#10;}" shortcut="ENTER" description="FlowMVI Model definition" toReformat="true" toShortenFQNames="true">
<variable name="NAME" expression="camelCase(String)" defaultValue="" alwaysStopAt="true" />
<context />
<context>
<option name="KOTLIN_CLASS" value="true" />
<option name="KOTLIN_OBJECT_DECLARATION" value="true" />
<option name="KOTLIN_TOPLEVEL" value="true" />
</context>
</template>
<template name="fmvid" value="/**&#10; * TODO: Document the decorator&#10; */&#10;@FlowMVIDSL&#10;fun &lt;S : MVIState, I : MVIIntent, A : MVIAction&gt; $NAME$Decorator(&#10; name: String? = &quot;$DecoratorName$Decorator&quot;,&#10;) = decorator&lt;S, I, A&gt; {&#10; this.name = name&#10; $END$&#10;}&#10;&#10;/**&#10; * Installs a new [$NAME$Decorator].&#10; */&#10;@FlowMVIDSL&#10;fun &lt;S : MVIState, I : MVIIntent, A : MVIAction&gt; StoreBuilder&lt;S, I, A&gt;.$NAME$(&#10; name: String? = &quot;$DecoratorName$Decorator&quot;,&#10;) = install($NAME$Decorator(name))&#10;" shortcut="ENTER" description="Create a new Decorator" toReformat="true" toShortenFQNames="true">
<template name="fmvid" value="/**&#10; * TODO: Document the decorator&#10; */&#10;@FlowMVIDSL&#10;fun &lt;S : MVIState, I : MVIIntent, A : MVIAction&gt; $NAME$Decorator(&#10; name: String? = &quot;$DecoratorName$Decorator&quot;,&#10;) = decorator&lt;S, I, A&gt; {&#10; this.name = name&#10; $END$&#10;}&#10;&#10;/**&#10; * Installs a new [$NAME$Decorator].&#10; */&#10;@FlowMVIDSL&#10;fun &lt;S : MVIState, I : MVIIntent, A : MVIAction&gt; StoreBuilder&lt;S, I, A&gt;.$NAME$(&#10; name: String? = &quot;$DecoratorName$Decorator&quot;,&#10;) = install($NAME$Decorator(name))&#10;" shortcut="ENTER" description="FlowMVI Decorator" toReformat="true" toShortenFQNames="true">
<variable name="NAME" expression="kotlinFunctionName()" defaultValue="" alwaysStopAt="true" />
<variable name="DecoratorName" expression="capitalize(NAME)" defaultValue="" alwaysStopAt="false" />
<context>
<option name="KOTLIN_TOPLEVEL" value="true" />
<option name="KOTLIN_CLASS" value="true" />
</context>
</template>
</templateSet>
2 changes: 1 addition & 1 deletion docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
release=false
```
* Make sure you have these installed:
* Android Studio latest Canary or Beta, depending on the current project's AGP (yes, we're on the edge).
* Android Studio latest Stable or Beta, depending on the current project's AGP.
* Kotlin Multiplatform suite (run `kdoctor` to verify proper setup)
* Detekt plugin
* Kotest plugin
Expand Down
20 changes: 12 additions & 8 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ First of all, here's how the library works:

![Maven Central](https://img.shields.io/maven-central/v/pro.respawn.flowmvi/core?label=Maven%20Central)

<details open>
<details>
<summary>Version catalogs</summary>

```toml
Expand Down Expand Up @@ -308,7 +308,7 @@ configure {
actionShareBehavior = ActionShareBehavior.Distribute()
onOverflow = BufferOverflow.DROP_OLDEST
intentCapacity = Channel.UNLIMITED
atomicStateUpdates = true
stateStrategy = StateStrategy.Atomic(reentrant = true)
allowIdleSubscriptions = false
logger = if (debuggable) PlatformStoreLogger else NoOpStoreLogger
verifyPlugins = debuggable
Expand Down Expand Up @@ -343,12 +343,16 @@ configure {
* `Channel.CONFLATED` - A buffer of 1
* `Channel.RENDEZVOUS` - Zero buffer (all events not ready to be processed are dropped)
* `Channel.BUFFERED` - Default system buffer capacity
* `atomicStateUpdates` - Enables transaction serialization for state updates, making state updates atomic and
suspendable. Synchronizes state updates, allowing only **one** client to read and/or update the state at a time. All
other clients that attempt to get the state will wait in a FIFO queue and suspend the parent coroutine. For one-time
usage of non-atomic updates, see `updateStateImmediate`. Learn
more [here](https://proandroiddev.com/how-to-safely-update-state-in-your-kotlin-apps-bf51ccebe2ef).
Has a small performance impact because of coroutine context switching and mutex usage when enabled.
* `stateStrategy` - Strategy for serializing state transactions. Choose one of the following:
* `Atomic(reentrant = false)` - Enables transaction serialization for state updates, making state updates atomic and
suspendable. Synchronizes state updates, allowing only **one** client to read and/or update the state at a time.
All other clients that attempt to get the state will wait in a FIFO queue and suspend the parent coroutine. For
one-time usage of non-atomic updates, see `updateStateImmediate`. Recommended for most cases.
* `Atomic(reentrant = true)` - Same as above, but allows nested state updates
without causing a deadlock, like this: `updateState { updateState { } }`. This strategy is 15x slower than other
options, but still negligible for managing UI and other non-performance-critical tasks. This is the default.
* `Immediate` - 2 times faster than atomic with no reentrancy, but provides no state consistency guarantees
and no thread-safety. Equivalent to always using `updateStateImmediate`.
* `allowIdleSubscriptions` - A flag to indicate that clients may subscribe to this store even while it is not started.
If you intend to stop and restart your store while the subscribers are present, set this to `true`. By default, will
use the opposite value of the `debuggable` parameter (`true` on production).
Expand Down
Loading
Loading