Skip to content

Commit

Permalink
Merge pull request #24 from floschu/feature/effect-controller
Browse files Browse the repository at this point in the history
EffectController
  • Loading branch information
floschu authored Sep 1, 2020
2 parents d1f05c2 + 3456a46 commit f5445e5
Show file tree
Hide file tree
Showing 38 changed files with 996 additions and 296 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

- binary compatibility will now be verified and held up on every release.

## `[0.13.0]` - 2020-08-XX

- Add `EffectController` and `CoroutineScope.createEffectController`.
- Rename `Controller.stub()` to `Controller.toStub()` to better reflect what it is doing.

## `[0.12.0]` - 2020-08-24

- Remove `Mutation` generic type from `Controller` interface.
Expand Down
5 changes: 5 additions & 0 deletions buildSrc/src/main/kotlin/Libs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -219,4 +219,9 @@ object Libs {
* http://mockk.io
*/
const val mockk: String = "io.mockk:mockk:" + Versions.mockk

/**
* https://github.com/coil-kt/coil
*/
const val coil: String = "io.coil-kt:coil:" + Versions.coil
}
14 changes: 8 additions & 6 deletions buildSrc/src/main/kotlin/Versions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ object Versions {

const val androidx_fragment: String = "1.2.5"

const val androidx_test: String = "1.2.0"
const val androidx_test: String = "1.3.0"

const val io_ktor: String = "1.4.0"

Expand All @@ -44,9 +44,9 @@ object Versions {

const val gradle_pitest_plugin: String = "1.5.2"

const val constraintlayout: String = "2.0.0"
const val constraintlayout: String = "2.0.1"

const val espresso_core: String = "3.2.0"
const val espresso_core: String = "3.3.0"

const val atomicfu_jvm: String = "0.14.4"

Expand All @@ -56,7 +56,7 @@ object Versions {

const val appcompat: String = "1.2.0"

const val junit_ktx: String = "1.1.1"
const val junit_ktx: String = "1.1.2"

const val material: String = "1.2.0"

Expand All @@ -66,12 +66,14 @@ object Versions {

const val mockk: String = "1.10.0"

const val coil: String = "0.12.0"

/**
* Current version: "6.6"
* Current version: "6.6.1"
* See issue 19: How to update Gradle itself?
* https://github.com/jmfayard/buildSrcVersions/issues/19
*/
const val gradleLatestVersion: String = "6.6"
const val gradleLatestVersion: String = "6.6.1"
}

/**
Expand Down
34 changes: 32 additions & 2 deletions control-core/api/control-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ public final class at/florianschuster/control/ControllerEvent$Completed : at/flo
public final class at/florianschuster/control/ControllerEvent$Created : at/florianschuster/control/ControllerEvent {
}

public final class at/florianschuster/control/ControllerEvent$Effect : at/florianschuster/control/ControllerEvent {
}

public final class at/florianschuster/control/ControllerEvent$Error : at/florianschuster/control/ControllerEvent {
}

Expand Down Expand Up @@ -70,11 +73,37 @@ public final class at/florianschuster/control/ControllerStart$Lazy : at/florians
public static final field INSTANCE Lat/florianschuster/control/ControllerStart$Lazy;
}

public abstract interface class at/florianschuster/control/ControllerStub {
public abstract interface class at/florianschuster/control/ControllerStub : at/florianschuster/control/Controller {
public abstract fun emitState (Ljava/lang/Object;)V
public abstract fun getDispatchedActions ()Ljava/util/List;
}

public abstract interface class at/florianschuster/control/EffectController : at/florianschuster/control/Controller {
public abstract fun getEffects ()Lkotlinx/coroutines/flow/Flow;
}

public final class at/florianschuster/control/EffectControllerKt {
public static final fun createEffectController (Lkotlinx/coroutines/CoroutineScope;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Ljava/lang/String;Lat/florianschuster/control/ControllerLog;Lat/florianschuster/control/ControllerStart;Lkotlinx/coroutines/CoroutineDispatcher;)Lat/florianschuster/control/EffectController;
public static synthetic fun createEffectController$default (Lkotlinx/coroutines/CoroutineScope;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Ljava/lang/String;Lat/florianschuster/control/ControllerLog;Lat/florianschuster/control/ControllerStart;Lkotlinx/coroutines/CoroutineDispatcher;ILjava/lang/Object;)Lat/florianschuster/control/EffectController;
}

public abstract interface class at/florianschuster/control/EffectControllerStub : at/florianschuster/control/ControllerStub {
public abstract fun emitEffect (Ljava/lang/Object;)V
}

public abstract interface class at/florianschuster/control/EffectEmitter {
public abstract fun emitEffect (Ljava/lang/Object;)V
}

public abstract interface class at/florianschuster/control/EffectMutatorContext : at/florianschuster/control/EffectEmitter, at/florianschuster/control/MutatorContext {
}

public abstract interface class at/florianschuster/control/EffectReducerContext : at/florianschuster/control/EffectEmitter, at/florianschuster/control/ReducerContext {
}

public abstract interface class at/florianschuster/control/EffectTransformerContext : at/florianschuster/control/EffectEmitter, at/florianschuster/control/TransformerContext {
}

public final class at/florianschuster/control/ExtensionsKt {
public static final fun bind (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow;
public static final fun distinctMap (Lkotlinx/coroutines/flow/Flow;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow;
Expand All @@ -96,7 +125,8 @@ public abstract interface class at/florianschuster/control/ReducerContext {
}

public final class at/florianschuster/control/StubKt {
public static final fun stub (Lat/florianschuster/control/Controller;)Lat/florianschuster/control/ControllerStub;
public static final fun toStub (Lat/florianschuster/control/Controller;)Lat/florianschuster/control/ControllerStub;
public static final fun toStub (Lat/florianschuster/control/EffectController;)Lat/florianschuster/control/EffectControllerStub;
}

public abstract interface class at/florianschuster/control/TransformerContext {
Expand Down
5 changes: 1 addition & 4 deletions control-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,7 @@ pitest {

// inlined invokeSuspend
"at.florianschuster.control.ControllerImplementation\$stateJob\$1",
"at.florianschuster.control.ControllerImplementation\$stateJob\$1\$2",

// lateinit var isInitialized
"at.florianschuster.control.ControllerImplementation\$stubInitialized\$1"
"at.florianschuster.control.ControllerImplementation\$stateJob\$1\$2"
)
threads.set(4)
jvmArgs.add("-ea")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlin.coroutines.ContinuationInterceptor

/**
* A [Controller] is an UI-independent class that controls the state of a view. The role of a
Expand Down Expand Up @@ -140,7 +139,7 @@ fun <Action, Mutation, State> CoroutineScope.createController(
* [Mutator] and [Reducer] will run on this [CoroutineDispatcher].
*/
dispatcher: CoroutineDispatcher = defaultScopeDispatcher()
): Controller<Action, State> = ControllerImplementation(
): Controller<Action, State> = ControllerImplementation<Action, Mutation, State, Nothing>(
scope = this, dispatcher = dispatcher, controllerStart = controllerStart,

initialState = initialState, mutator = mutator, reducer = reducer,
Expand Down Expand Up @@ -262,7 +261,6 @@ interface ReducerContext
*/
typealias Transformer<Emission> = TransformerContext.(emissions: Flow<Emission>) -> Flow<Emission>


/**
* A context used for a [Transformer]. Does not provide any additional functionality.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package at.florianschuster.control

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow

/**
* A [Controller] that provides a [Flow] of [Effect]'s that can happen during a
* mutation in [EffectMutator], a state reduction in [EffectReducer] or a
* transformation in [EffectTransformer].
*
* An [Effect] could be a one-of UI notification such as a Toast or a Snackbar
* on Android.
*
* Before using this, make sure to look into the [Controller] documentation.
*/
interface EffectController<Action, State, Effect> : Controller<Action, State> {

/**
* [Flow] of [Effect]s. Use this to collect [Effect] emissions.
* The [Flow] is received in a fan-out fashion. One emission will be
* emitted to one collector only.
*/
val effects: Flow<Effect>
}

/**
* Creates an [EffectController] bound to the [CoroutineScope] via [ControllerImplementation].
*
* The principle of the created state machine is the same as with
* [CoroutineScope.createController].
*
* An [Effect] can be emitted either in [mutator], [reducer], [actionsTransformer],
* [mutationsTransformer] or [statesTransformer].
*/
@ExperimentalCoroutinesApi
@FlowPreview
fun <Action, Mutation, State, Effect> CoroutineScope.createEffectController(

/**
* The initial [State] for the internal state machine.
*/
initialState: State,
/**
* See [EffectMutator].
*/
mutator: EffectMutator<Action, Mutation, State, Effect> = { _ -> emptyFlow() },
/**
* See [EffectReducer].
*/
reducer: EffectReducer<Mutation, State, Effect> = { _, previousState -> previousState },

/**
* See [EffectTransformer].
*/
actionsTransformer: EffectTransformer<Action, Effect> = { it },
mutationsTransformer: EffectTransformer<Mutation, Effect> = { it },
statesTransformer: EffectTransformer<State, Effect> = { it },

/**
* Used for [ControllerLog] and as [CoroutineName] for the internal state machine.
*/
tag: String = defaultTag(),
/**
* Log configuration for [ControllerEvent]s. See [ControllerLog].
*/
controllerLog: ControllerLog = ControllerLog.default,

/**
* When the internal state machine [Flow] should be started. See [ControllerStart].
*/
controllerStart: ControllerStart = ControllerStart.Lazy,

/**
* Override to launch the internal state machine [Flow] in a different [CoroutineDispatcher]
* than the one used in the [CoroutineScope.coroutineContext].
*
* [Mutator] and [Reducer] will run on this [CoroutineDispatcher].
*/
dispatcher: CoroutineDispatcher = defaultScopeDispatcher()
): EffectController<Action, State, Effect> = ControllerImplementation(
scope = this, dispatcher = dispatcher, controllerStart = controllerStart,

initialState = initialState, mutator = mutator, reducer = reducer,
actionsTransformer = actionsTransformer,
mutationsTransformer = mutationsTransformer,
statesTransformer = statesTransformer,

tag = tag, controllerLog = controllerLog
)

/**
* An [EffectEmitter] can emit side-effects.
*
* This is implemented by the respective context's of [EffectMutator], [EffectReducer]
* and [EffectTransformer].
*/
interface EffectEmitter<Effect> {

/**
* Emits an [Effect].
*/
fun emitEffect(effect: Effect)
}

/**
* A [Mutator] used in a [EffectController] that is able to emit effects.
*/
typealias EffectMutator<Action, Mutation, State, Effect> =
EffectMutatorContext<Action, State, Effect>.(action: Action) -> Flow<Mutation>

/**
* A [MutatorContext] that additionally provides the functionality of an [EffectEmitter].
* This context is used for an [EffectMutator].
*/
interface EffectMutatorContext<Action, State, Effect> :
MutatorContext<Action, State>, EffectEmitter<Effect>

/**
* A [Reducer] used in a [EffectController] that is able to emit effects.
*/
typealias EffectReducer<Mutation, State, Effect> =
EffectReducerContext<Effect>.(mutation: Mutation, previousState: State) -> State

/**
* A [ReducerContext] that additionally provides the functionality of an [EffectEmitter].
*/
interface EffectReducerContext<Effect> : ReducerContext, EffectEmitter<Effect>

/**
* A [Transformer] used in a [EffectController] that is able to emit effects.
*/
typealias EffectTransformer<Emission, Effect> =
EffectTransformerContext<Effect>.(emissions: Flow<Emission>) -> Flow<Emission>

/**
* A [TransformerContext] that additionally provides the functionality of an [EffectEmitter].
*/
interface EffectTransformerContext<Effect> : TransformerContext, EffectEmitter<Effect>
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import kotlin.coroutines.ContinuationInterceptor
* Helper to fetch the [CoroutineDispatcher] in the [CoroutineScope].
*/
internal fun CoroutineScope.defaultScopeDispatcher(): CoroutineDispatcher {
val interceptor = coroutineContext[ContinuationInterceptor]
checkNotNull(interceptor) { "CoroutineScope does not have an interceptor" }
return interceptor as CoroutineDispatcher
val continuationInterceptor = coroutineContext[ContinuationInterceptor]
checkNotNull(continuationInterceptor) {
"CoroutineScope does not have a ContinuationInterceptor"
}
return continuationInterceptor as CoroutineDispatcher
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ internal sealed class ControllerError(
tag: String,
action: String,
cause: Throwable
) : ControllerError("Mutator error in $tag, action = $action", cause)
) : ControllerError(message = "Mutator error in $tag, action = $action", cause = cause)

/**
* Error during [Reducer].
Expand All @@ -29,4 +29,18 @@ internal sealed class ControllerError(
message = "Reducer error in $tag, previousState = $previousState, mutation = $mutation",
cause = cause
)

/**
* Error during [EffectEmitter.emitEffect].
*/
class Effect(
tag: String,
effect: String
) : ControllerError(
message = "Effect error in $tag, effect = $effect",
cause = IllegalStateException(
"Capacity for effects has been reached. Either too many effects have been triggered " +
"or they might not be consumed."
)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ sealed class ControllerEvent(
) : ControllerEvent(tag, "action: $action")

/**
* When the [Mutator] produces a new [Mutation].
* When the [Mutator] emits a new [Mutation].
*/
class Mutation internal constructor(
tag: String, mutation: String
Expand All @@ -43,6 +43,13 @@ sealed class ControllerEvent(
tag: String, state: String
) : ControllerEvent(tag, "state: $state")

/**
* When an [Effect] is emitted by the [EffectController].
*/
class Effect internal constructor(
tag: String, effect: String
) : ControllerEvent(tag, "effect: $effect")

/**
* When an error happens during the execution of the internal state machine.
*/
Expand Down
Loading

0 comments on commit f5445e5

Please sign in to comment.