From a5bddb405fa6e7ace8de241dfe07879a0ba27257 Mon Sep 17 00:00:00 2001 From: Peter Bryant Date: Tue, 9 Nov 2021 15:12:40 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=A5=20Support=20side-effects?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin_bloc/compose/BlocComposer.kt | 2 +- .../kotlin_bloc/compose/BlocListener.kt | 32 ++++---- .../ptrbrynt/kotlin_bloc/compose/BlocSaver.kt | 2 +- .../kotlin_bloc/compose/BlocSelector.kt | 2 +- .../compose/LoggingBlocObserver.kt | 25 +++--- .../compose/RememberSaveableBloc.kt | 2 +- .../compose/LoggingBlocObserverTest.kt | 3 +- .../kotlin_bloc/compose/blocs/CounterBloc.kt | 3 +- .../com/ptrbrynt/kotlin_bloc/core/Bloc.kt | 21 ++++- .../com/ptrbrynt/kotlin_bloc/core/BlocBase.kt | 32 +++++++- .../ptrbrynt/kotlin_bloc/core/BlocObserver.kt | 17 +++- .../com/ptrbrynt/kotlin_bloc/core/Cubit.kt | 17 +++- .../com/ptrbrynt/kotlin_bloc/core/Emitter.kt | 14 +++- .../kotlin_bloc/core/blocs/CounterBloc.kt | 2 +- .../kotlin_bloc/core/blocs/MultiFlowBloc.kt | 2 +- .../kotlin_bloc/core/blocs/SeededBloc.kt | 2 +- .../kotlin_bloc/core/cubits/CounterCubit.kt | 3 +- docs/bloc-compose.md | 69 ++++------------ docs/bloc-test.md | 12 ++- docs/core-concepts.md | 81 ++++++++++++++----- docs/getting-started.md | 6 +- docs/tutorials/counter.md | 14 ++-- docs/tutorials/todo-list.md | 4 +- .../kotlin_bloc/sample/MainActivity.kt | 2 +- .../sample/ui/blocs/CounterBloc.kt | 2 +- .../sample/ui/cubits/CounterCubit.kt | 2 +- .../kotlin_bloc/sample/MainActivityTest.kt | 12 +-- .../com/ptrbrynt/kotlin_bloc/test/BlocTest.kt | 73 ++++++++++++++++- .../com/ptrbrynt/kotlin_bloc/test/MockBloc.kt | 8 +- .../ptrbrynt/kotlin_bloc/test/WhenListen.kt | 15 +++- .../ptrbrynt/kotlin_bloc/test/MockBlocTest.kt | 8 +- .../test/blocs/counter/CounterBloc.kt | 2 +- .../kotlin_bloc/test/cubits/CounterCubit.kt | 2 +- 33 files changed, 333 insertions(+), 160 deletions(-) diff --git a/compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/BlocComposer.kt b/compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/BlocComposer.kt index ea590fa..740da97 100644 --- a/compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/BlocComposer.kt +++ b/compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/BlocComposer.kt @@ -39,7 +39,7 @@ import kotlinx.coroutines.flow.Flow */ @Composable -fun , State> BlocComposer( +fun , State> BlocComposer( bloc: B, transformStates: Flow.() -> Flow = { this }, content: @Composable (State) -> Unit, diff --git a/compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/BlocListener.kt b/compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/BlocListener.kt index e13abf7..640d94e 100644 --- a/compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/BlocListener.kt +++ b/compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/BlocListener.kt @@ -8,51 +8,51 @@ import com.ptrbrynt.kotlin_bloc.core.BlocBase import kotlinx.coroutines.flow.Flow /** - * Takes a [bloc] and an [onState] callback and invokes [onState] in response to `state` changes - * in the [bloc]. + * Takes a [bloc] and an [onSideEffect] callback and invokes [onSideEffect] in response to side + * effects in the [bloc]. * - * It should be used for side-effects resulting from new `state`s being emitted by the [bloc] e.g. + * It should be used for side-effects resulting from new side-effects being emitted by the [bloc] e.g. * navigation, showing a snackbar etc. * * If you want to build composables in response to new states, use [BlocComposer] * * ```kotlin - * BlocListener(bloc) { state -> - * // React to the new state here + * BlocListener(bloc) { sideEffect -> + * // React to the new side effect here * } * ``` * - * * An optional [transformStates] can be implemented for more granular control over + * * An optional [transformSideEffects] can be implemented for more granular control over * the frequency and specificity with which transitions occur. * - * For example, to debounce the state changes: + * For example, to debounce the side effects: * * ```kotlin * BlocListener( * myBloc, - * transformStates = { this.debounce(1000) }, + * transformSideEffects = { this.debounce(1000) }, * ) { - * // React to the new state here + * // React to the new side-effect here * } * ``` * * @param bloc The bloc or cubit that the [BlocListener] will interact with. - * @param onState The callback function which will be invoked whenever a new `state` is emitted by the [bloc]. - * @param transformStates Provides more granular control over the [State] flow. + * @param onSideEffect The callback function which will be invoked whenever a new `state` is emitted by the [bloc]. + * @param transformSideEffects Provides more granular control over the [State] flow. * @see BlocComposer */ @Composable -fun , State> BlocListener( +fun , SideEffect> BlocListener( bloc: B, - transformStates: Flow.() -> Flow = { this }, - onState: suspend (State) -> Unit, + transformSideEffects: Flow.() -> Flow = { this }, + onSideEffect: suspend (SideEffect) -> Unit, ) { - val state by bloc.stateFlow.transformStates().collectAsState(initial = null) + val state by bloc.sideEffectFlow.transformSideEffects().collectAsState(initial = null) state?.let { LaunchedEffect(it) { - onState(it) + onSideEffect(it) } } } diff --git a/compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/BlocSaver.kt b/compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/BlocSaver.kt index 8fb14b8..1828359 100644 --- a/compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/BlocSaver.kt +++ b/compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/BlocSaver.kt @@ -8,7 +8,7 @@ import com.ptrbrynt.kotlin_bloc.core.BlocBase * A [Saver] which enables the state of a Bloc or Cubit to be saved * and restored. */ -internal fun > blocSaver( +internal fun > blocSaver( save: SaverScope.(B) -> State = { it.state }, restore: (State) -> B, ) = Saver( diff --git a/compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/BlocSelector.kt b/compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/BlocSelector.kt index c0b6ca9..104235d 100644 --- a/compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/BlocSelector.kt +++ b/compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/BlocSelector.kt @@ -27,7 +27,7 @@ import kotlinx.coroutines.flow.map */ @Composable -fun , State, T> BlocSelector( +fun , State, T> BlocSelector( bloc: B, selector: (State) -> T, content: @Composable (T) -> Unit, diff --git a/compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/LoggingBlocObserver.kt b/compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/LoggingBlocObserver.kt index ded3c51..90925f2 100644 --- a/compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/LoggingBlocObserver.kt +++ b/compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/LoggingBlocObserver.kt @@ -10,25 +10,32 @@ import com.ptrbrynt.kotlin_bloc.core.Transition /** * A [BlocObserver] which logs all bloc events to the console. */ - class LoggingBlocObserver : BlocObserver() { - override fun , State> onCreate(bloc: B) { - super.onCreate(bloc) - Log.i(bloc::class.simpleName, "Created") - } - - override fun , State> onChange(bloc: B, change: Change) { + override fun , State> onChange(bloc: B, change: Change) { super.onChange(bloc, change) Log.i(bloc::class.simpleName, change.toString()) } - override fun , Event, State> onEvent(bloc: B, event: Event) { + override fun > onCreate(bloc: B) { + super.onCreate(bloc) + Log.i(bloc::class.simpleName, "Created") + } + + override fun , Event> onEvent(bloc: B, event: Event) { super.onEvent(bloc, event) Log.i(bloc::class.simpleName, event.toString()) } - override fun , Event, State> onTransition( + override fun , SideEffect> onSideEffect( + bloc: B, + sideEffect: SideEffect, + ) { + super.onSideEffect(bloc, sideEffect) + Log.i(bloc::class.simpleName, sideEffect.toString()) + } + + override fun , Event, State> onTransition( bloc: B, transition: Transition, ) { diff --git a/compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/RememberSaveableBloc.kt b/compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/RememberSaveableBloc.kt index d4cc067..484c69c 100644 --- a/compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/RememberSaveableBloc.kt +++ b/compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/RememberSaveableBloc.kt @@ -45,7 +45,7 @@ import com.ptrbrynt.kotlin_bloc.core.BlocBase * @throws AssertionError if no [save] parameter is provided and the [State] type cannot be saved. */ @Composable -fun > rememberSaveableBloc( +fun > rememberSaveableBloc( save: SaverScope.(B) -> State = { assert(canBeSaved(it)) it.state diff --git a/compose/src/test/java/com/ptrbrynt/kotlin_bloc/compose/LoggingBlocObserverTest.kt b/compose/src/test/java/com/ptrbrynt/kotlin_bloc/compose/LoggingBlocObserverTest.kt index a038150..a898c8e 100644 --- a/compose/src/test/java/com/ptrbrynt/kotlin_bloc/compose/LoggingBlocObserverTest.kt +++ b/compose/src/test/java/com/ptrbrynt/kotlin_bloc/compose/LoggingBlocObserverTest.kt @@ -7,12 +7,12 @@ import com.ptrbrynt.kotlin_bloc.compose.blocs.CounterEvent import com.ptrbrynt.kotlin_bloc.core.Bloc import io.mockk.mockk import io.mockk.verifyOrder -import java.io.PrintStream import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config import org.robolectric.shadows.ShadowLog +import java.io.PrintStream @RunWith(AndroidJUnit4::class) @Config(shadows = [ShadowLog::class]) @@ -38,6 +38,7 @@ class LoggingBlocObserverTest { stream.println("I/CounterBloc: Increment") stream.println("I/CounterBloc: Change(state=0, newState=1)") stream.println("I/CounterBloc: Transition(state=0, event=Increment, newState=1)") + stream.println("I/CounterBloc: 1") } } } diff --git a/compose/src/test/java/com/ptrbrynt/kotlin_bloc/compose/blocs/CounterBloc.kt b/compose/src/test/java/com/ptrbrynt/kotlin_bloc/compose/blocs/CounterBloc.kt index cbe7321..69ab83b 100644 --- a/compose/src/test/java/com/ptrbrynt/kotlin_bloc/compose/blocs/CounterBloc.kt +++ b/compose/src/test/java/com/ptrbrynt/kotlin_bloc/compose/blocs/CounterBloc.kt @@ -4,13 +4,14 @@ import com.ptrbrynt.kotlin_bloc.core.Bloc enum class CounterEvent { Increment, Decrement } -class CounterBloc : Bloc(0) { +class CounterBloc : Bloc(0) { init { on { event -> when (event) { CounterEvent.Increment -> emit(state + 1) CounterEvent.Decrement -> emit(state - 1) } + emitSideEffect(state) } } } diff --git a/core/src/main/java/com/ptrbrynt/kotlin_bloc/core/Bloc.kt b/core/src/main/java/com/ptrbrynt/kotlin_bloc/core/Bloc.kt index b77d53b..f1c3c43 100644 --- a/core/src/main/java/com/ptrbrynt/kotlin_bloc/core/Bloc.kt +++ b/core/src/main/java/com/ptrbrynt/kotlin_bloc/core/Bloc.kt @@ -12,11 +12,18 @@ import kotlinx.coroutines.launch * Takes [Event]s as input and transforms them into a [Flow] * of [State]s as an output. * + * Also emits a [Flow] of [SideEffect]s, which are non-state outputs emitted as a result of new + * [Event]s. + * * @param initial The initial [State] + * @param Event The type of event this can receive + * @param State The type of state this emits + * @param SideEffect The type of side-effect this can emit * @see Cubit */ @Suppress("LeakingThis") -abstract class Bloc(initial: State) : BlocBase(initial) { +abstract class Bloc(initial: State) : + BlocBase(initial) { protected val eventFlow = MutableSharedFlow() init { @@ -30,7 +37,7 @@ abstract class Bloc(initial: State) : BlocBase(initial) { } @PublishedApi - internal val emitter = object : Emitter { + internal val emitter = object : Emitter { override suspend fun emit(state: State) { mutableChangeFlow.emit(Change(this@Bloc.state, state)) } @@ -38,6 +45,14 @@ abstract class Bloc(initial: State) : BlocBase(initial) { override suspend fun emitEach(states: Flow) { states.onEach { emit(it) }.launchIn(blocScope) } + + override suspend fun emitSideEffect(sideEffect: SideEffect) { + mutableSideEffectFlow.emit(sideEffect) + } + + override suspend fun emitSideEffects(sideEffects: Flow) { + sideEffects.onEach { emitSideEffect(it) }.launchIn(blocScope) + } } /** @@ -62,7 +77,7 @@ abstract class Bloc(initial: State) : BlocBase(initial) { * @param E The type of [Event] that this handles */ protected inline fun on( - noinline mapEventToState: suspend Emitter.(E) -> Unit, + noinline mapEventToState: suspend Emitter.(E) -> Unit, ) { eventFlow .transformEvents() diff --git a/core/src/main/java/com/ptrbrynt/kotlin_bloc/core/BlocBase.kt b/core/src/main/java/com/ptrbrynt/kotlin_bloc/core/BlocBase.kt index 8d05c38..f53e530 100644 --- a/core/src/main/java/com/ptrbrynt/kotlin_bloc/core/BlocBase.kt +++ b/core/src/main/java/com/ptrbrynt/kotlin_bloc/core/BlocBase.kt @@ -14,7 +14,7 @@ import kotlinx.coroutines.launch * @param initial The initial [State] */ @Suppress("LeakingThis") -abstract class BlocBase(initial: State) { +abstract class BlocBase(initial: State) { init { Bloc.observer.onCreate(this) @@ -29,6 +29,18 @@ abstract class BlocBase(initial: State) { } } + protected val mutableSideEffectFlow = MutableSharedFlow().apply { + blocScope.launch { + collect { onSideEffect(it) } + } + } + + /** + * The [Flow] of [SideEffect]s + */ + val sideEffectFlow: Flow + get() = mutableSideEffectFlow + /** * The current [State] [Flow] */ @@ -61,4 +73,22 @@ abstract class BlocBase(initial: State) { Bloc.observer.onChange(this, change) this.state = change.newState } + + /** + * Called whenever a [SideEffect] is emitted. + * + * **Note: `super.onSideEffect` should always be called first.** + * + * ```kotlin + * override fun onSideEffect(sideEffect: SideEffect) { + * // Always call super.onSideEffect first + * super.onSideEffect(sideEffect) + * + * // Custom logic goes here + * } + * ``` + */ + open fun onSideEffect(sideEffect: SideEffect) { + Bloc.observer.onSideEffect(this, sideEffect) + } } diff --git a/core/src/main/java/com/ptrbrynt/kotlin_bloc/core/BlocObserver.kt b/core/src/main/java/com/ptrbrynt/kotlin_bloc/core/BlocObserver.kt index c7a7fa7..6ea4c01 100644 --- a/core/src/main/java/com/ptrbrynt/kotlin_bloc/core/BlocObserver.kt +++ b/core/src/main/java/com/ptrbrynt/kotlin_bloc/core/BlocObserver.kt @@ -12,7 +12,7 @@ abstract class BlocObserver { * * @param bloc The [Bloc] or [Cubit] which was created. */ - open fun , State> onCreate(bloc: B) {} + open fun > onCreate(bloc: B) {} /** * Called whenever an [event] is `add`ed to any [bloc]. @@ -20,7 +20,7 @@ abstract class BlocObserver { * @param bloc The [Bloc] to which the [Event] was `add`ed * @param event The [Event] added to the [Bloc] */ - open fun , Event, State> onEvent(bloc: B, event: Event) {} + open fun , Event> onEvent(bloc: B, event: Event) {} /** * Called whenever a [Change] occurs in any [Bloc] or [Cubit]. @@ -31,7 +31,7 @@ abstract class BlocObserver { * @param bloc The [Bloc] or [Cubit] which emitted the [change] * @param change The [Change] that occurred within the [bloc] */ - open fun , State> onChange(bloc: B, change: Change) {} + open fun , State> onChange(bloc: B, change: Change) {} /** * Called whenever a [Transition] occurs in any [Bloc]. @@ -43,9 +43,18 @@ abstract class BlocObserver { * @param bloc The [Bloc] in which the [transition] occurred * @param transition The [Transition] which occurred within the [bloc] */ - open fun , Event, State> onTransition( + open fun , Event, State> onTransition( bloc: B, transition: Transition, ) { } + + /** + * Called whenever a [SideEffect] is emitted by a [Bloc] or [Cubit]. + */ + open fun , SideEffect> onSideEffect( + bloc: B, + sideEffect: SideEffect, + ) { + } } diff --git a/core/src/main/java/com/ptrbrynt/kotlin_bloc/core/Cubit.kt b/core/src/main/java/com/ptrbrynt/kotlin_bloc/core/Cubit.kt index 82dee0c..723421b 100644 --- a/core/src/main/java/com/ptrbrynt/kotlin_bloc/core/Cubit.kt +++ b/core/src/main/java/com/ptrbrynt/kotlin_bloc/core/Cubit.kt @@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.onEach /** * A [Cubit] is similar to a [Bloc] but has no notion of events, - * instead relying on methods to [emit] [State]s. + * instead relying on methods to [emit] [State]s and [SideEffect]s. * * Every [Cubit] requires an initial state, which will be the state * of the [Cubit] before [emit] has been called. @@ -19,10 +19,12 @@ import kotlinx.coroutines.flow.onEach * ``` * * @param initial The initial [State] + * @param State The type of state this emits + * @param SideEffect The type of side-effect this can emit * @see Bloc */ - -abstract class Cubit(initial: State) : BlocBase(initial), Emitter { +abstract class Cubit(initial: State) : + BlocBase(initial), Emitter { override suspend fun emit(state: State) { mutableChangeFlow.emit(Change(this.state, state)) } @@ -30,4 +32,13 @@ abstract class Cubit(initial: State) : BlocBase(initial), Emitter< override suspend fun emitEach(states: Flow) { states.onEach { emit(it) }.launchIn(blocScope) } + + override suspend fun emitSideEffect(sideEffect: SideEffect) { + mutableSideEffectFlow.emit(sideEffect) + } + + override suspend fun emitSideEffects(sideEffects: Flow) { + sideEffects.onEach { emitSideEffect(it) }.launchIn(blocScope) + } + } diff --git a/core/src/main/java/com/ptrbrynt/kotlin_bloc/core/Emitter.kt b/core/src/main/java/com/ptrbrynt/kotlin_bloc/core/Emitter.kt index 471bd3e..f708e7c 100644 --- a/core/src/main/java/com/ptrbrynt/kotlin_bloc/core/Emitter.kt +++ b/core/src/main/java/com/ptrbrynt/kotlin_bloc/core/Emitter.kt @@ -3,9 +3,9 @@ package com.ptrbrynt.kotlin_bloc.core import kotlinx.coroutines.flow.Flow /** - * Interface which can be implemented on any object which can [emit] a [State]. + * Interface which can be implemented on any object which can [emit] a [State] or a [SideEffect] */ -interface Emitter { +interface Emitter { /** * Emit a new [State] */ @@ -15,4 +15,14 @@ interface Emitter { * [emit] each [State] which is emitted by the [states] [Flow]. */ suspend fun emitEach(states: Flow) + + /** + * Emit a new [SideEffect] + */ + suspend fun emitSideEffect(sideEffect: SideEffect) + + /** + * Emit a [Flow] of [SideEffect]s. + */ + suspend fun emitSideEffects(sideEffects: Flow) } diff --git a/core/src/test/java/com/ptrbrynt/kotlin_bloc/core/blocs/CounterBloc.kt b/core/src/test/java/com/ptrbrynt/kotlin_bloc/core/blocs/CounterBloc.kt index 1dfbd6c..33a8575 100644 --- a/core/src/test/java/com/ptrbrynt/kotlin_bloc/core/blocs/CounterBloc.kt +++ b/core/src/test/java/com/ptrbrynt/kotlin_bloc/core/blocs/CounterBloc.kt @@ -14,7 +14,7 @@ object Decrement : CounterEvent() open class CounterBloc( private val onTransitionCallback: ((Transition) -> Unit)? = null, private val onEventCallback: ((CounterEvent) -> Unit)? = null, -) : Bloc(0) { +) : Bloc(0) { init { on { diff --git a/core/src/test/java/com/ptrbrynt/kotlin_bloc/core/blocs/MultiFlowBloc.kt b/core/src/test/java/com/ptrbrynt/kotlin_bloc/core/blocs/MultiFlowBloc.kt index 681b6b5..7923890 100644 --- a/core/src/test/java/com/ptrbrynt/kotlin_bloc/core/blocs/MultiFlowBloc.kt +++ b/core/src/test/java/com/ptrbrynt/kotlin_bloc/core/blocs/MultiFlowBloc.kt @@ -12,7 +12,7 @@ data class MultiFlowNumberAdded(val number: Int) : MultiFlowEvent() @ExperimentalCoroutinesApi -class MultiFlowBloc : Bloc>(emptyList()) { +class MultiFlowBloc : Bloc, Unit>(emptyList()) { private val numbers = MutableStateFlow(emptyList()) init { diff --git a/core/src/test/java/com/ptrbrynt/kotlin_bloc/core/blocs/SeededBloc.kt b/core/src/test/java/com/ptrbrynt/kotlin_bloc/core/blocs/SeededBloc.kt index cad052d..fdf7cf9 100644 --- a/core/src/test/java/com/ptrbrynt/kotlin_bloc/core/blocs/SeededBloc.kt +++ b/core/src/test/java/com/ptrbrynt/kotlin_bloc/core/blocs/SeededBloc.kt @@ -2,7 +2,7 @@ package com.ptrbrynt.kotlin_bloc.core.blocs import com.ptrbrynt.kotlin_bloc.core.Bloc -class SeededBloc(private val seed: List, initial: Int) : Bloc(initial) { +class SeededBloc(private val seed: List, initial: Int) : Bloc(initial) { init { on { for (value in seed) { diff --git a/core/src/test/java/com/ptrbrynt/kotlin_bloc/core/cubits/CounterCubit.kt b/core/src/test/java/com/ptrbrynt/kotlin_bloc/core/cubits/CounterCubit.kt index 3e12180..932f031 100644 --- a/core/src/test/java/com/ptrbrynt/kotlin_bloc/core/cubits/CounterCubit.kt +++ b/core/src/test/java/com/ptrbrynt/kotlin_bloc/core/cubits/CounterCubit.kt @@ -3,7 +3,8 @@ package com.ptrbrynt.kotlin_bloc.core.cubits import com.ptrbrynt.kotlin_bloc.core.Change import com.ptrbrynt.kotlin_bloc.core.Cubit -class CounterCubit(private val onChangeCallback: ((Change) -> Unit)? = null) : Cubit(0) { +class CounterCubit(private val onChangeCallback: ((Change) -> Unit)? = null) : + Cubit(0) { suspend fun increment() = emit(state + 1) suspend fun decrement() = emit(state - 1) diff --git a/docs/bloc-compose.md b/docs/bloc-compose.md index 07ea387..beff120 100644 --- a/docs/bloc-compose.md +++ b/docs/bloc-compose.md @@ -33,29 +33,27 @@ BlocComposer( ### BlocListener -`BlocListener` is a composable which takes a `bloc` and an `onState` callback. `onState` is invoked whenever the `bloc` emits a new state. - -`BlocListener` should be used to handle [side-effects](https://developer.android.com/jetpack/compose/side-effects) e.g. showing a dialog, or navigation. +`BlocListener` is a composable which takes a `bloc` and an `onSideEffect` callback. `onSideEffect` is invoked whenever the `bloc` emits a new side-effect. ```kotlin val bloc = remember { CounterBloc() } -BlocListener(bloc) { state -> - // Do something here based on the bloc state +BlocListener(bloc) { sideEffect -> + // Do something here based on the side effect } ``` -For fine-grained control over when the `onState` callback is invoked, you can add a `transformStates`. This allows you to transform the flow of states emitted by the `bloc`. For example, you could filter out certain states, or debounce the flow to prevent state changes from happening too quickly. +For fine-grained control over when the `onSideEffect` callback is invoked, you can add a `transformSideEffects`. This allows you to transform the flow of side-effects emitted by the `bloc`. For example, you could filter out certain side-effects, or debounce the flow to prevent side-effect emissions from happening too quickly. ```kotlin val bloc = remember { CounterBloc() } BlocListener( bloc, - // Only react to even numbers, at most once per second - transformStates = { this.filter { it % 2 == 0 }.debounce(1000) }, -) { state -> - // Do something here based on the bloc state + // Only react at most once per second + transformSideEffects = { this.debounce(1000) }, +) { sideEffect -> + // Do something here based on the side effect } ``` @@ -68,7 +66,8 @@ Let's take a look at how to use `BlocComposer` to hook up a `Counter` widget to ```kotlin enum class CounterEvent { Incremented } -class CounterBloc: Bloc(0) { +@Parcelize +class CounterBloc: Bloc(0), Parcelable { init { on { event -> when (event) { @@ -83,9 +82,9 @@ class CounterBloc: Bloc(0) { ```kotlin @Composable -fun Counter() { - val bloc = remember { CounterBloc() } - +fun Counter( + bloc = rememberSaveable { CounterBloc() }, +) { Scaffold( floatingActionButton = { FloatingActionButton( @@ -110,44 +109,4 @@ fun Counter() { At this point, we have successfully separated our presentation layer from our business logic layer. Notice that the `Counter` composable knows nothing about what happens when a user taps the buttons. The widget simply tells the `CounterBloc` that the user has pressed the Increment button. -## Surviving process recreation - -The above examples use the `remember` method to persist the instance of `CounterBloc` through re-compositions. However, if the activity/process is recreated (e.g. by the screen being rotated), the `state` of the `CounterBloc` is lost and it reverts to its initial state. - -We can prevent this by using the `rememberSaveableBloc` method. - -There are a couple of prerequisites: - -1. Your `State` class must support being saved in a `Bundle`. In other words, it should be a primitive or a `Parcelable`. - * You can use the [`@Parcelize`](https://github.com/Kotlin/KEEP/blob/master/proposals/extensions/android-parcelable.md) annotation to easily make your state class parcelable -2. Your Bloc must take its initial state as a constructor argument. - -So let's tweak our `CounterBloc` to support being saved: - -```kotlin -enum class CounterEvent { Incremented } - -class CounterBloc(initial: Int): Bloc(initial) { - init { - on { event -> - when (event) { - CounterEvent.Incremented -> emit(state + 1) - } - } - } -} -``` - -?> Since our state is an `Int`, it is already saveable in a `Bundle`. - -We can now use the `rememberSaveableBloc` method to persist the current `state` through configuration changes: - -```kotlin -@Composable -fun Counter() { - val bloc = rememberSaveableBloc(initialState = 0) { CounterBloc(it) } - - // Use the bloc as normal -} -``` - +?> Notice that we have used `rememberSaveable` to ensure the bloc survives recomposition and configuration changes. In order to make this work we have also used the `Parcelize` annotation to make `CounterBloc` a `Parcelable` class. diff --git a/docs/bloc-test.md b/docs/bloc-test.md index 275c68b..ad63801 100644 --- a/docs/bloc-test.md +++ b/docs/bloc-test.md @@ -19,9 +19,9 @@ class CounterBlocTest { add(Incremented) add(Decremented) }, - // The expect parameter should be a list of functions. Each function takes the next state + // The expected parameter should be a list of functions. Each function takes the next state // emitted and should return a boolean indicating whether that state is correct. - expect = listOf( + expected = listOf( { equals(1) }, { equals(2) }, { equals(1) }, @@ -36,6 +36,8 @@ As well as the parameters shown above, `blocTest` also provides the following op * `setUp`: A function executed before `build` which can be used to set up dependencies or do any other initialization * `tearDown`: A function executed as the final step of the test, which can be used to perform cleanup * `skip`: An optional `Int` which indicates the number of states to ignore before beginning to make assertions +* `skipSideEffects`: An optional `Int` which indicates the number of side-effects to ignore before beginning to make assertions +* `expectedSideEffects`: Like `expected`, but for side-effects. * `verify`: A function executed after the assertion step, which can be used to perform additional checks and verification ## `mockBloc`, `mockCubit` and `whenListen` @@ -50,7 +52,7 @@ The `whenListen` method is provided as a simple way of stubbing the state flow e class MyComposableTest { @Test fun testWithMockCubit() { - val cubit = mockCubit() // Here, Int represents the state type + val cubit = mockCubit() // Here, Int represents the state type and Unit is the side-effect type whenListen(cubit, flowOf(1,2,3)) // This line sets up the cubit to emit the numbers 1, 2, and 3 in that order. @@ -59,10 +61,12 @@ class MyComposableTest { @Test fun testWithMockBloc() { - val bloc = mockBloc() + val bloc = mockBloc() whenListen(bloc, flowOf(1,2,3), initialState = 0) // You can pass an optional initial state. + whenListenToSideEffects(bloc, flowOf("hello")) // You can also stub side-effect emission + // Do some testing... } } diff --git a/docs/core-concepts.md b/docs/core-concepts.md index bfdf465..e8e8e02 100644 --- a/docs/core-concepts.md +++ b/docs/core-concepts.md @@ -17,15 +17,17 @@ A `Cubit` can expose functions which can be called to trigger state changes. We can create a `CounterCubit` like this: ```kotlin -class CounterCubit: Cubit(0) +class CounterCubit: Cubit(0) ``` When creating a `Cubit`, we need to define the type of state which the `Cubit` will be managing. In the case of the `CounterCubit` above, the state can be represented via an `Int`, but in more complex cases it may be necessary to use a `class` instead of a primitive type. -The second thing we need to do when creating a `Cubit` is specify the initial state. We do this by passing the initial state into the constructor of the `Cubit` subclass. In the snippet above, we are setting the initial state to `0` internally, but we could also allow the `CounterCubit` to be more flexible by accepting a value in its own constructor: +When creating a `Cubit`, we also need to define a type for possible side-effects of the `Cubit`. A side-effect is anything you want the `Cubit` to emit that is **not** a state. In this case, because we aren't going to use any side-effects, we have defined the side-effect type as `Unit`. + +The final thing we need to do when creating a `Cubit` is specify the initial state. We do this by passing the initial state into the constructor of the `Cubit` subclass. In the snippet above, we are setting the initial state to `0` internally, but we could also allow the `CounterCubit` to be more flexible by accepting a value in its own constructor: ```kotlin -class CounterCubit(initial: Int): Cubit(initial) +class CounterCubit(initial: Int): Cubit(initial) ``` This would allow us to instantiate different `CounterCubit` instances with different initial states, like this: @@ -40,7 +42,7 @@ val cubitB = CounterCubit(10) // State starts at 10 > Each `Cubit` has the ability to output a new state via `emit`. ```kotlin -class CounterCubit : Cubit(0) { +class CounterCubit : Cubit(0) { suspend fun increment() = emit(state + 1) } ``` @@ -118,14 +120,14 @@ suspend fun main() { ### Creating a Bloc -Creating a `Bloc` is similar to creating a `Cubit` except, in addition to defining the state we'll be managin, we must also define the event type that the `Bloc` will be able to process. +Creating a `Bloc` is similar to creating a `Cubit` except, in addition to defining the state we'll be managing and the type of possible side-effects, we must also define the event type that the `Bloc` will be able to process. > Events are input to a Bloc. They are commonly added in response to user interactions such as button presses, or lifecycle events like page loads. ```kotlin enum class CounterEvent { Incremented } -class CounterBloc: Bloc(0) { +class CounterBloc: Bloc(0) { // ... } ``` @@ -139,7 +141,7 @@ We can then use `on` to handle the `CounterEvent.Incremented` even ```kotlin enum class CounterEvent { Incremented } -class CounterBloc: Bloc(0) { +class CounterBloc: Bloc(0) { init { on { when (event) { @@ -178,12 +180,47 @@ Just like `Cubit`, `Bloc` provides the ability to receive a `Flow` of `state`s. suspend fun main() { val bloc = CounterBloc() scope.launch { - bloc.collect { println(it) } + bloc.stateFlow.collect { println(it) } } bloc.add(CounterEvent.Incremented) } ``` +## Side-effects + +`Bloc`s and `Cubit`s can each emit side-effects. These are secondary results of handling Bloc events or calling Cubit methods which are distinct from states. + +Side-effects can be used to trigger things like sending a notification to the user interface. They can be emitted using the `emitSideEffect` and `emitSideEffects` methods. + +Let's add a side-effect to our `CounterCubit` which emits a `String` each time the state is changed to an even number. + +```kotlin +class CounterCubit : Cubit(0) { // Notice we have defined the type of Side Effect as String + suspend fun increment() { + emit(state + 1) + if (state % 2 == 0) { + emitSideEffect("Here's an even number!") + } + } +} +``` + +We can then observe side-effects emitted by the Cubit: + +```kotlin +suspend fun main() { + val cubit = CounterCubit() + scope.launch { + cubit.sideEffectFlow.collect { println(it) } + } + cubit.increment() + cubit.increment() + // Side-effect will be emitted here. +} +``` + + + ## BlocObserver Most applications will include a potentially large number of `Bloc`s and `Cubit`s. Implementing observation on every one of these classes individually can be cumbserome, especially if you want to do the same thing for each `Bloc` and `Cubit`. @@ -196,22 +233,30 @@ To that end, the library includes a way of creating a global `BlocObserver`. */ class LoggingBlocObserver : BlocObserver() { - override fun , State> onCreate(bloc: B) { - super.onCreate(bloc) - Log.i(bloc::class.simpleName, "Created") - } - - override fun , State> onChange(bloc: B, change: Change) { + override fun , State> onChange(bloc: B, change: Change) { super.onChange(bloc, change) Log.i(bloc::class.simpleName, change.toString()) } - override fun , Event, State> onEvent(bloc: B, event: Event) { + override fun > onCreate(bloc: B) { + super.onCreate(bloc) + Log.i(bloc::class.simpleName, "Created") + } + + override fun , Event> onEvent(bloc: B, event: Event) { super.onEvent(bloc, event) Log.i(bloc::class.simpleName, event.toString()) } - override fun , Event, State> onTransition( + override fun , SideEffect> onSideEffect( + bloc: B, + sideEffect: SideEffect, + ) { + super.onSideEffect(bloc, sideEffect) + Log.i(bloc::class.simpleName, sideEffect.toString()) + } + + override fun , Event, State> onTransition( bloc: B, transition: Transition, ) { @@ -252,7 +297,7 @@ Here's an example of a `CounterCubit`, and a `CounterBloc` with equivalent funct ##### CounterCubit ```kotlin -class CounterCubit : Cubit(0) { +class CounterCubit : Cubit(0) { suspend fun increment = emit(state + 1) } ``` @@ -262,7 +307,7 @@ class CounterCubit : Cubit(0) { ```kotlin enum class CounterEvent { Increment } -class CounterBloc : Bloc(0) { +class CounterBloc : Bloc(0) { init { on { when (event) { diff --git a/docs/getting-started.md b/docs/getting-started.md index 5ef44ed..6671e28 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -30,10 +30,10 @@ dependencies { // ... // Choose EITHER: - implementation 'com.github.ptrbrynt.KotlinBloc:compose:1.1.0' // For Jetpack Compose apps - implementation 'com.github.ptrbrynt.KotlinBloc:core:1.1.0' // The pure Kotlin library, for other stuff + implementation 'com.github.ptrbrynt.KotlinBloc:compose:2.0.0' // For Jetpack Compose apps + implementation 'com.github.ptrbrynt.KotlinBloc:core:2.0.0' // The pure Kotlin library, for other stuff // Optional test helpers: - testImplementation 'com.github.ptrbrynt.KotlinBloc:test:1.1.0' + testImplementation 'com.github.ptrbrynt.KotlinBloc:test:2.0.0' } ``` diff --git a/docs/tutorials/counter.md b/docs/tutorials/counter.md index 1222568..5349668 100644 --- a/docs/tutorials/counter.md +++ b/docs/tutorials/counter.md @@ -29,7 +29,7 @@ Finally, just add the following dependency to your module-level `build.gradle` f ```groovy dependencies { // ... - implementation 'com.github.ptrbrynt.KotlinBloc:compose:1.0' + implementation 'com.github.ptrbrynt.KotlinBloc:compose:2.0.0' } ``` @@ -47,9 +47,9 @@ Let's create a new class called `CounterObserver`: */ class CounterObserver : BlocObserver() { - override fun , State> onChange(bloc: B, change: Change) { + override fun , State> onChange(bloc: B, change: Change) { super.onChange(bloc, change) - Log.i(bloc::class.simpleName, "$change") + Log.i(bloc::class.simpleName, change.toString()) } } ``` @@ -72,10 +72,11 @@ Let's now create a `CounterCubit.kt` file, and implement our cubit class. It will expose an `increment` method which will add `1` to the current state. -The type of state the `CounterCubit` is managing will just be an `Int`, and the initial state will be `0`. +The type of state the `CounterCubit` is managing will just be an `Int`, and the initial state will be `0`. We won't be using side-effects, so we can set the side-effect type to `Unit`. ```kotlin -class CounterCubit : Cubit(0) { +@Parcelize +class CounterCubit : Cubit(0), Parcelable { suspend fun increment() = emit(state + 1) } ``` @@ -88,11 +89,10 @@ Let's create a new file called `Counter.kt`. This will contain our composable fu /** * A widget which reacts to the provided [CounterCubit] and notifies it in response to user input. */ - @Composable fun Counter( scope: CoroutineScope = rememberCoroutineScope(), - cubit: CounterCubit = remember { CounterCubit() }, + cubit: CounterCubit = rememberSaveable { CounterCubit() }, ) { Scaffold( topBar = { diff --git a/docs/tutorials/todo-list.md b/docs/tutorials/todo-list.md index d3f6956..916142e 100644 --- a/docs/tutorials/todo-list.md +++ b/docs/tutorials/todo-list.md @@ -31,7 +31,7 @@ Finally, just add the following dependencies to your module-level `build.gradle` ```groovy dependencies { // ... - implementation 'com.github.ptrbrynt.KotlinBloc:compose:1.0' + implementation 'com.github.ptrbrynt.KotlinBloc:compose:2.0.0' implementation "androidx.navigation:navigation-compose:2.4.0-alpha09" @@ -147,7 +147,7 @@ data class TodosLoadSuccess(val todos: List) : TodosState() Now we have our events and states, we can create our `TodosBloc` class: ```kotlin -class TodosBloc(private val todoDao: TodoDao) : Bloc(TodosLoading) { +class TodosBloc(private val todoDao: TodoDao) : Bloc(TodosLoading) { init { on { emitEach(todoDao.getAllTodos().map { TodosLoadSuccess(it) }) diff --git a/sample/src/main/java/com/ptrbrynt/kotlin_bloc/sample/MainActivity.kt b/sample/src/main/java/com/ptrbrynt/kotlin_bloc/sample/MainActivity.kt index be49cc5..7b71758 100644 --- a/sample/src/main/java/com/ptrbrynt/kotlin_bloc/sample/MainActivity.kt +++ b/sample/src/main/java/com/ptrbrynt/kotlin_bloc/sample/MainActivity.kt @@ -70,7 +70,7 @@ fun CubitCounter( @Composable fun CounterBase( - bloc: BlocBase, + bloc: BlocBase, onIncrement: () -> Unit, scaffoldState: ScaffoldState = rememberScaffoldState(), ) { diff --git a/sample/src/main/java/com/ptrbrynt/kotlin_bloc/sample/ui/blocs/CounterBloc.kt b/sample/src/main/java/com/ptrbrynt/kotlin_bloc/sample/ui/blocs/CounterBloc.kt index ed1fd62..c81648f 100644 --- a/sample/src/main/java/com/ptrbrynt/kotlin_bloc/sample/ui/blocs/CounterBloc.kt +++ b/sample/src/main/java/com/ptrbrynt/kotlin_bloc/sample/ui/blocs/CounterBloc.kt @@ -4,7 +4,7 @@ import com.ptrbrynt.kotlin_bloc.core.Bloc enum class CounterEvent { Increment, Decrement } -class CounterBloc(initial: Int) : Bloc(initial) { +class CounterBloc(initial: Int) : Bloc(initial) { init { on { event -> diff --git a/sample/src/main/java/com/ptrbrynt/kotlin_bloc/sample/ui/cubits/CounterCubit.kt b/sample/src/main/java/com/ptrbrynt/kotlin_bloc/sample/ui/cubits/CounterCubit.kt index 468b2a4..c69f4aa 100644 --- a/sample/src/main/java/com/ptrbrynt/kotlin_bloc/sample/ui/cubits/CounterCubit.kt +++ b/sample/src/main/java/com/ptrbrynt/kotlin_bloc/sample/ui/cubits/CounterCubit.kt @@ -3,7 +3,7 @@ package com.ptrbrynt.kotlin_bloc.sample.ui.cubits import com.ptrbrynt.kotlin_bloc.core.Cubit import kotlinx.coroutines.launch -class CounterCubit : Cubit(0) { +class CounterCubit : Cubit(0) { fun increment() { blocScope.launch { emit(state + 1) } } diff --git a/sample/src/test/java/com/ptrbrynt/kotlin_bloc/sample/MainActivityTest.kt b/sample/src/test/java/com/ptrbrynt/kotlin_bloc/sample/MainActivityTest.kt index 32db47d..a8662f4 100644 --- a/sample/src/test/java/com/ptrbrynt/kotlin_bloc/sample/MainActivityTest.kt +++ b/sample/src/test/java/com/ptrbrynt/kotlin_bloc/sample/MainActivityTest.kt @@ -27,7 +27,7 @@ class MainActivityTest { @Test fun blocCounterDisplaysBlocState() { - val bloc = mockBloc() + val bloc = mockBloc() val flow = MutableStateFlow(1) @@ -48,7 +48,7 @@ class MainActivityTest { @Test fun blocCounterIncrementButtonAddsIncrement() { - val bloc = mockBloc() + val bloc = mockBloc() whenListen(bloc, emptyFlow(), initialState = 1) @@ -65,7 +65,7 @@ class MainActivityTest { @Test fun cubitCounterDisplaysBlocState() { - val cubit = mockCubit() + val cubit = mockCubit() val flow = MutableStateFlow(1) @@ -86,7 +86,7 @@ class MainActivityTest { @Test fun cubitCounterIncrementButtonCallsIncrement() { - val cubit = mockCubit() + val cubit = mockCubit() whenListen(cubit, emptyFlow(), initialState = 1) @@ -103,7 +103,7 @@ class MainActivityTest { @Test fun blocSelectorCounterDisplaysBlocState() { - val bloc = mockBloc() + val bloc = mockBloc() val flow = MutableStateFlow(1) @@ -124,7 +124,7 @@ class MainActivityTest { @Test fun blocSelectorCounterIncrementButtonAddsIncrement() { - val bloc = mockBloc() + val bloc = mockBloc() whenListen(bloc, emptyFlow(), initialState = 1) diff --git a/test/src/main/java/com/ptrbrynt/kotlin_bloc/test/BlocTest.kt b/test/src/main/java/com/ptrbrynt/kotlin_bloc/test/BlocTest.kt index 31f6d95..6110b78 100644 --- a/test/src/main/java/com/ptrbrynt/kotlin_bloc/test/BlocTest.kt +++ b/test/src/main/java/com/ptrbrynt/kotlin_bloc/test/BlocTest.kt @@ -5,6 +5,7 @@ import com.ptrbrynt.kotlin_bloc.core.BlocBase import kotlin.time.ExperimentalTime typealias StateAssertion = State.() -> Boolean +typealias SideEffectAssertion = SideEffect.() -> Boolean /** * Handles asserting that a `bloc` emits the [expected] states (in order) after [act] has been @@ -20,28 +21,66 @@ typealias StateAssertion = State.() -> Boolean * * @param skip An optional [Int] which can be used to skip any number of states. Defaults to `0`. * + * @param skipSideEffects An optional [Int] which can be used to skip any number of side effects. + * Defaults to `0`. + * * @param expected A list of [StateAssertion]s which will be run on each newly emitted state in * order. Use this to check the correctness of each state emitted by the `bloc` under test. * + * @param expectedSideEffects A list of [SideEffectAssertion]s which will be run on each emitted + * side-effect in order. Use this to check the correctness of each side-effect emitted by the + * `bloc` under test. + * * @param verify An optional callback which is invoked after all [expected] states have been * emitted, and can be used for additional verification/assertions. * * @param tearDown Can be used to execute any code after the test has run. */ @ExperimentalTime -suspend fun , State> testBloc( +suspend fun , State, SideEffect> testBloc( setUp: suspend () -> Unit = {}, build: () -> B, act: suspend B.() -> Unit = {}, skip: Int = 0, - expected: List>, + skipSideEffects: Int = 0, + expected: List> = emptyList(), + expectedSideEffects: List> = emptyList(), verify: B.() -> Unit = {}, tearDown: suspend () -> Unit = {}, ) { assert(skip >= 0) + assert(skipSideEffects >= 0) setUp() + + testStates( + build = build, + act = act, + skip = skip, + expected = expected, + verify = verify, + ) + + testSideEffects( + build = build, + act = act, + skip = skipSideEffects, + expected = expectedSideEffects, + verify = verify, + ) + + tearDown() +} + +@ExperimentalTime +private suspend fun , State, SideEffect> testStates( + build: () -> B, + act: suspend B.() -> Unit = {}, + skip: Int = 0, + expected: List> = emptyList(), + verify: B.() -> Unit = {}, +) { val bloc = build() bloc.stateFlow.test { @@ -60,6 +99,32 @@ suspend fun , State> testBloc( } bloc.verify() - - tearDown() } + +@ExperimentalTime +private suspend fun , State, SideEffect> testSideEffects( + build: () -> B, + act: suspend B.() -> Unit = {}, + skip: Int = 0, + expected: List> = emptyList(), + verify: B.() -> Unit = {}, +) { + val bloc = build() + + bloc.sideEffectFlow.test { + bloc.act() + + for (i in 0 until skip) { + awaitItem() + } + + for (assertion in expected) { + val item = awaitItem() + assert(assertion(item)) + } + + cancelAndIgnoreRemainingEvents() + } + + bloc.verify() +} \ No newline at end of file diff --git a/test/src/main/java/com/ptrbrynt/kotlin_bloc/test/MockBloc.kt b/test/src/main/java/com/ptrbrynt/kotlin_bloc/test/MockBloc.kt index e7c0dcb..d2eee6f 100644 --- a/test/src/main/java/com/ptrbrynt/kotlin_bloc/test/MockBloc.kt +++ b/test/src/main/java/com/ptrbrynt/kotlin_bloc/test/MockBloc.kt @@ -20,7 +20,7 @@ import kotlin.reflect.KClass * @param block Block to execute after the mock has been created, with the mock as the receiver * @see mockk */ -inline fun , reified State> mockCubit( +inline fun , reified State, reified SideEffect> mockCubit( name: String? = null, relaxed: Boolean = false, vararg moreInterfaces: KClass<*>, @@ -34,7 +34,9 @@ inline fun , reified State> mockCubit( relaxUnitFun = relaxUnitFun ) { every { stateFlow } answers { emptyFlow() } + every { sideEffectFlow } answers { emptyFlow() } coEvery { emit(any()) } returns Unit + coEvery { emitSideEffect(any()) } returns Unit block() } } @@ -51,7 +53,7 @@ inline fun , reified State> mockCubit( * @param block Block to execute after the mock has been created, with the mock as the receiver * @see mockk */ -inline fun , reified Event, reified State> mockBloc( +inline fun , reified Event, State, SideEffect> mockBloc( name: String? = null, relaxed: Boolean = false, vararg moreInterfaces: KClass<*>, @@ -65,8 +67,8 @@ inline fun , reified Event, reified State> mockBl relaxUnitFun = relaxUnitFun, ) { every { stateFlow } answers { emptyFlow() } + every { sideEffectFlow } answers { emptyFlow() } every { add(any()) } returns Unit - block() } } diff --git a/test/src/main/java/com/ptrbrynt/kotlin_bloc/test/WhenListen.kt b/test/src/main/java/com/ptrbrynt/kotlin_bloc/test/WhenListen.kt index f79a345..ca98ea4 100644 --- a/test/src/main/java/com/ptrbrynt/kotlin_bloc/test/WhenListen.kt +++ b/test/src/main/java/com/ptrbrynt/kotlin_bloc/test/WhenListen.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.onEach * [whenListen] also handles stubbing the `state` of the [bloc] to stay in sync with the emitted * state. */ -fun , State> whenListen( +fun , State> whenListen( bloc: B, states: Flow, initialState: State? = null, @@ -27,3 +27,16 @@ fun , State> whenListen( every { bloc.state } returns initialState } } + +/** + * Creates a stub response for the `sideEffectFlow` property of the given [bloc]. + * + * Use [whenListenToSideEffects] when you want to return a canned [Flow] of [SideEffect]s + * for a [bloc] instance. + */ +fun , SideEffect> whenListenToSideEffects( + bloc: B, + sideEffects: Flow, +) { + every { bloc.sideEffectFlow } answers { sideEffects } +} diff --git a/test/src/test/java/com/ptrbrynt/kotlin_bloc/test/MockBlocTest.kt b/test/src/test/java/com/ptrbrynt/kotlin_bloc/test/MockBlocTest.kt index 6e2ba27..76c85ff 100644 --- a/test/src/test/java/com/ptrbrynt/kotlin_bloc/test/MockBlocTest.kt +++ b/test/src/test/java/com/ptrbrynt/kotlin_bloc/test/MockBlocTest.kt @@ -15,7 +15,7 @@ class MockBlocTest { @Test fun mockCubitTest() = runBlocking { - val cubit = mockCubit() + val cubit = mockCubit() whenListen(cubit, flowOf(0, 1, 2)) @@ -33,7 +33,7 @@ class MockBlocTest { @Test fun mockCubitTestWithInitialState() = runBlocking { - val cubit = mockCubit() + val cubit = mockCubit() whenListen(cubit, flowOf(0, 1, 2), initialState = 1) @@ -53,7 +53,7 @@ class MockBlocTest { @Test fun mockBlocTest() = runBlocking { - val bloc = mockBloc() + val bloc = mockBloc() whenListen(bloc, flowOf(0, 1, 2)) @@ -71,7 +71,7 @@ class MockBlocTest { @Test fun mockBlocTestWithInitialState() = runBlocking { - val bloc = mockBloc() + val bloc = mockBloc() whenListen(bloc, flowOf(0, 1, 2), initialState = 2) diff --git a/test/src/test/java/com/ptrbrynt/kotlin_bloc/test/blocs/counter/CounterBloc.kt b/test/src/test/java/com/ptrbrynt/kotlin_bloc/test/blocs/counter/CounterBloc.kt index 47dea53..8da149d 100644 --- a/test/src/test/java/com/ptrbrynt/kotlin_bloc/test/blocs/counter/CounterBloc.kt +++ b/test/src/test/java/com/ptrbrynt/kotlin_bloc/test/blocs/counter/CounterBloc.kt @@ -2,7 +2,7 @@ package com.ptrbrynt.kotlin_bloc.test.blocs.counter import com.ptrbrynt.kotlin_bloc.core.Bloc -class CounterBloc : Bloc(0) { +class CounterBloc : Bloc(0) { init { on { emit(state + 1) diff --git a/test/src/test/java/com/ptrbrynt/kotlin_bloc/test/cubits/CounterCubit.kt b/test/src/test/java/com/ptrbrynt/kotlin_bloc/test/cubits/CounterCubit.kt index 28cbca2..8bb5e85 100644 --- a/test/src/test/java/com/ptrbrynt/kotlin_bloc/test/cubits/CounterCubit.kt +++ b/test/src/test/java/com/ptrbrynt/kotlin_bloc/test/cubits/CounterCubit.kt @@ -2,7 +2,7 @@ package com.ptrbrynt.kotlin_bloc.test.cubits import com.ptrbrynt.kotlin_bloc.core.Cubit -class CounterCubit : Cubit(0) { +class CounterCubit : Cubit(0) { suspend fun increment() = emit(state + 1) }