Skip to content

Commit

Permalink
Merge pull request #8 from Nek-12/1.0.0-alpha05
Browse files Browse the repository at this point in the history
1.0.0-alpha05
  • Loading branch information
Nek-12 authored Dec 10, 2022
2 parents a6d4dce + c07827e commit fcbc8dd
Show file tree
Hide file tree
Showing 15 changed files with 100 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ abstract class MVIViewModel<S : MVIState, I : MVIIntent, A : MVIAction>(
protected open val store: MVIStore<S, I, A> by launchedStore(
scope = viewModelScope,
initial = initialState,
behavior = ActionShareBehavior.DISTRIBUTE, // required for lifecycle awareness
behavior = ActionShareBehavior.Distribute(), // required for lifecycle awareness
recover = { recover(it) },
reduce = { reduce(it) },
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.nek12.flowMVI.sample.view

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nek12.flowMVI.ActionShareBehavior.SHARE
import com.nek12.flowMVI.ActionShareBehavior
import com.nek12.flowMVI.ReducerScope
import com.nek12.flowMVI.launchedStore
import com.nek12.flowMVI.sample.R
Expand All @@ -23,7 +23,7 @@ class NoBaseClassViewModel : ViewModel() {
val store by launchedStore<State, Intent, Action>(
scope = viewModelScope,
initial = DisplayingContent(0),
behavior = SHARE
behavior = ActionShareBehavior.Share(),
) { reduce(it) }

private suspend fun ReducerScope<State, Intent, Action>.reduce(intent: Intent) {
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ plugins {
}

rootProject.group = "com.nek12.flowmvi"
rootProject.version = "1.0.0-alpha04"
rootProject.version = "1.0.0-alpha05"

atomicfu {
dependenciesVersion = libs.versions.kotlinx.atomicfu.get()
Expand Down
3 changes: 3 additions & 0 deletions buildSrc/src/main/kotlin/com.nek12.android-library.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ plugins {
android {
compileSdk = 33

resourcePrefix = "flowmvi_"

defaultConfig {
minSdk = 22
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
Expand Down Expand Up @@ -41,6 +43,7 @@ android {
}

kotlin {
explicitApi()
sourceSets.main {
kotlin.srcDir("build/generated/ksp/main/kotlin")
}
Expand Down
5 changes: 5 additions & 0 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

val compileKotlin: KotlinCompile by tasks

@Suppress("DSL_SCOPE_VIOLATION")
plugins {
kotlin("jvm")
`maven-publish`
Expand All @@ -13,6 +14,10 @@ java {
targetCompatibility = JavaVersion.VERSION_11
}

kotlin {
explicitApi()
}

compileKotlin.kotlinOptions {
jvmTarget = "11"
}
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/com/nek12/flowMVI/DelicateStoreApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ package com.nek12.flowMVI
)
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
annotation class DelicateStoreApi
public annotation class DelicateStoreApi
77 changes: 41 additions & 36 deletions core/src/main/java/com/nek12/flowMVI/MVIApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,69 +12,69 @@ import kotlin.coroutines.EmptyCoroutineContext
* The state of the view / consumer.
* The state must be comparable and immutable (most likely a data class)
*/
interface MVIState
public interface MVIState

/**
* User interaction or other event that happens in the UI layer.
* Must be immutable.
*/
interface MVIIntent
public interface MVIIntent

/**
* A single, one-shot, side-effect of processing an [MVIIntent], sent by [MVIProvider].
* Consumed in the ui-layer as a one-time action.
* Must be immutable.
*/
interface MVIAction
public interface MVIAction

/**
* An operation that processes incoming [MVIIntent]s
*/
typealias Reducer<S, I, A> = suspend ReducerScope<S, I, A>.(intent: I) -> Unit
public typealias Reducer<S, I, A> = suspend ReducerScope<S, I, A>.(intent: I) -> Unit

/**
* An operation that handles exceptions when processing [MVIIntent]s
*/
typealias Recover<S> = (e: Exception) -> S
public typealias Recover<S> = (e: Exception) -> S

/**
* An entity that handles [MVIIntent]s sent by UI layer and manages UI [states].
* This is usually the business logic unit
*/
interface MVIProvider<out S : MVIState, in I : MVIIntent, out A : MVIAction> {
public interface MVIProvider<out S : MVIState, in I : MVIIntent, out A : MVIAction> {

/**
* Should be called when a UI event happens that produces an [intent].
* @See MVIIntent
*/
fun send(intent: I)
public fun send(intent: I)

/**
* A flow of UI states to be handled by the [MVISubscriber].
*/
val states: StateFlow<S>
public val states: StateFlow<S>

/**
* A flow of [MVIAction]s to be handled by the [MVISubscriber],
* usually resulting in one-shot events.
* How actions are distributed depends on [ActionShareBehavior].
*/
val actions: Flow<A>
public val actions: Flow<A>

// the library does not support java, and Kotlin does not allow
// overridable @JvmName because of java interop so its' safe to suppress this
// will be solved by context receivers
@Suppress("INAPPLICABLE_JVM_NAME")
@JvmName("sendIntent")
fun I.send() = send(this)
public fun I.send(): Unit = send(this)
}

/**
* A central business logic unit for handling [MVIIntent]s, [MVIAction]s, and [MVIState]s.
* A store functions independently of any subscribers.
* MVIStore is the base implementation of [MVIProvider].
*/
interface MVIStore<S : MVIState, in I : MVIIntent, A : MVIAction> : MVIProvider<S, I, A> {
public interface MVIStore<S : MVIState, in I : MVIIntent, A : MVIAction> : MVIProvider<S, I, A> {

/**
* Send a new UI side-effect to be processed by subscribers, only once.
Expand All @@ -83,7 +83,7 @@ interface MVIStore<S : MVIState, in I : MVIIntent, A : MVIAction> : MVIProvider<
* How actions will be distributed depends on [ActionShareBehavior].
* @See MVIProvider
*/
fun send(action: A)
public fun send(action: A)

/**
* Starts store intent processing in a new coroutine on the parent thread.
Expand All @@ -92,15 +92,15 @@ interface MVIStore<S : MVIState, in I : MVIIntent, A : MVIAction> : MVIProvider<
* Although not advised, store can experimentally be launched multiple times, provided you cancel the job used before.
* @return a [Job] that the store is running on that can be cancelled later.
*/
fun start(scope: CoroutineScope): Job
public fun start(scope: CoroutineScope): Job

/**
* Obtain or set the current state in an unsafe manner.
* This property is not thread-safe and parallel state updates will introduce a race condition when not
* handled properly.
*/
@DelicateStoreApi
val state: S
public val state: S

/**
* Obtain the current [MVIStore.state] and update it with the result of [transform].
Expand All @@ -111,7 +111,7 @@ interface MVIStore<S : MVIState, in I : MVIIntent, A : MVIAction> : MVIProvider<
* If you want to operate on a state of particular subtype, use the typed version of this function.
* @see [withState]
*/
suspend fun updateState(transform: suspend S.() -> S): S
public suspend fun updateState(transform: suspend S.() -> S): S

/**
* Obtain the current state and operate on it, returning [R].
Expand All @@ -135,15 +135,15 @@ interface MVIStore<S : MVIState, in I : MVIIntent, A : MVIAction> : MVIProvider<
*
* @returns the value of [R], i.e. the result of the block.
*/
suspend fun <R> withState(block: suspend S.() -> R): R
public suspend fun <R> withState(block: suspend S.() -> R): R

/**
* Launch a new coroutine using given [scope],
* and use either provided [recover] block or the [MVIStore]'s recover block.
* Exceptions thrown in the [block] or in the nested coroutines will be handled by [recover].
* This function does not update or obtain the state, for that, use [withState] or [updateState] inside [block].
*/
fun launchRecovering(
public fun launchRecovering(
scope: CoroutineScope,
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
Expand All @@ -159,44 +159,44 @@ interface MVIStore<S : MVIState, in I : MVIIntent, A : MVIAction> : MVIProvider<
* @see MVIProvider
* @See MVISubscriber
*/
interface MVIView<S : MVIState, in I : MVIIntent, A : MVIAction> : MVISubscriber<S, A> {
public interface MVIView<S : MVIState, in I : MVIIntent, A : MVIAction> : MVISubscriber<S, A> {

/**
* Provider, an object that handles business logic.
* @See MVIProvider
*/
val provider: MVIProvider<S, I, A>
public val provider: MVIProvider<S, I, A>

/**
* Send an intent for the [provider] to process e.g. a user click.
*/
fun send(intent: I) = provider.send(intent)
public fun send(intent: I): Unit = provider.send(intent)

@Suppress("INAPPLICABLE_JVM_NAME")
@JvmName("sendAction")
fun I.send() = send(this)
public fun I.send(): Unit = send(this)
}

/**
* A generic subscriber of [MVIProvider] that [consume]s [MVIAction]s and [render]s [MVIState]s of types [A] and [S].
*/
interface MVISubscriber<in S : MVIState, in A : MVIAction> {
public interface MVISubscriber<in S : MVIState, in A : MVIAction> {

/**
* Render a new [state].
* This function will be called each time a new state is received.
*
* This function should be idempotent and should not send any intents.
*/
fun render(state: S)
public fun render(state: S)

/**
* Consume a one-time side-effect emitted by [MVIProvider].
*
* This function is called each time an [MVIAction] arrives.
* This function may send intents under the promise that no loops will occur.
*/
fun consume(action: A)
public fun consume(action: A)
}

/**
Expand All @@ -205,7 +205,7 @@ interface MVISubscriber<in S : MVIState, in A : MVIAction> {
* When in doubt, use the default one, and change if you have issues.
* @see MVIStore
*/
enum class ActionShareBehavior {
public sealed interface ActionShareBehavior {

/**
* Actions will be distributed to all subscribers equally. Each subscriber will receive a reference to a single
Expand All @@ -214,7 +214,7 @@ enum class ActionShareBehavior {
* action entirely (i.e. other subscribers won't receive it when they "return" if they weren't present at the
* time of emission).
*/
SHARE,
public data class Share(val buffer: Int = DefaultBufferSize, val replay: Int = 0) : ActionShareBehavior

/**
* Fan-out behavior means that multiple subscribers are allowed,
Expand All @@ -224,7 +224,7 @@ enum class ActionShareBehavior {
*
* **This is the default**.
*/
DISTRIBUTE,
public data class Distribute(val buffer: Int = DefaultBufferSize) : ActionShareBehavior

/**
* Restricts the count of subscribers to 1.
Expand All @@ -233,7 +233,12 @@ enum class ActionShareBehavior {
*
* **Resubscriptions are not allowed too, including lifecycle-aware collection**.
*/
RESTRICT
public data class Restrict(val buffer: Int = DefaultBufferSize) : ActionShareBehavior

public companion object {

public const val DefaultBufferSize: Int = 64
}
}

/**
Expand All @@ -242,25 +247,25 @@ enum class ActionShareBehavior {
* Throwing when in this scope will result in recover() of the parent store being called.
* Child coroutines should handle their exceptions independently, unless using [launchRecovering].
*/
interface ReducerScope<S : MVIState, in I : MVIIntent, A : MVIAction> {
public interface ReducerScope<S : MVIState, in I : MVIIntent, A : MVIAction> {

/**
* A coroutine scope the intent processing runs on. This is a child scope that is used when
* [MVIStore.start] is called.
*
* **Cancelling the scope will cancel the [MVIStore.start] (intent processing)**.
*/
val scope: CoroutineScope
public val scope: CoroutineScope

/**
* Delegates to [MVIStore.send]
*/
fun send(action: A)
public fun send(action: A)

/**
* Delegates to [MVIStore.launchRecovering]
*/
fun launchRecovering(
public fun launchRecovering(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
recover: (Recover<S>)? = null,
Expand All @@ -270,25 +275,25 @@ interface ReducerScope<S : MVIState, in I : MVIIntent, A : MVIAction> {
/**
* Delegates to [MVIStore.withState]
*/
suspend fun <R> withState(block: suspend S.() -> R): R
public suspend fun <R> withState(block: suspend S.() -> R): R

/**
* Delegates to [MVIStore.updateState]
* @see MVIStore.updateState
* @see [withState]
*/
suspend fun updateState(transform: suspend S.() -> S): S
public suspend fun updateState(transform: suspend S.() -> S): S

/**
* Delegates to [MVIStore.state]
*/
@DelicateStoreApi
val state: S
public val state: S

// the library does not support java, and Kotlin does not allow
// overridable @JvmName because of java interop so its' safe to suppress this
// will be solved by context receivers
@Suppress("INAPPLICABLE_JVM_NAME")
@JvmName("sendAction")
fun A.send() = send(this)
public fun A.send(): Unit = send(this)
}
Loading

0 comments on commit fcbc8dd

Please sign in to comment.