Skip to content

Commit

Permalink
💥 Support side-effects
Browse files Browse the repository at this point in the history
  • Loading branch information
Peter Bryant committed Nov 9, 2021
1 parent f39688d commit a5bddb4
Show file tree
Hide file tree
Showing 33 changed files with 333 additions and 160 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import kotlinx.coroutines.flow.Flow
*/

@Composable
fun <B : BlocBase<State>, State> BlocComposer(
fun <B : BlocBase<State, *>, State> BlocComposer(
bloc: B,
transformStates: Flow<State>.() -> Flow<State> = { this },
content: @Composable (State) -> Unit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <B : BlocBase<State>, State> BlocListener(
fun <B : BlocBase<*, SideEffect>, SideEffect> BlocListener(
bloc: B,
transformStates: Flow<State>.() -> Flow<State> = { this },
onState: suspend (State) -> Unit,
transformSideEffects: Flow<SideEffect>.() -> Flow<SideEffect> = { 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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 <State : Any, B : BlocBase<State>> blocSaver(
internal fun <State : Any, B : BlocBase<State, *>> blocSaver(
save: SaverScope.(B) -> State = { it.state },
restore: (State) -> B,
) = Saver(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import kotlinx.coroutines.flow.map
*/

@Composable
fun <B : BlocBase<State>, State, T> BlocSelector(
fun <B : BlocBase<State, *>, State, T> BlocSelector(
bloc: B,
selector: (State) -> T,
content: @Composable (T) -> Unit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <B : BlocBase<State>, State> onCreate(bloc: B) {
super.onCreate(bloc)
Log.i(bloc::class.simpleName, "Created")
}

override fun <B : BlocBase<State>, State> onChange(bloc: B, change: Change<State>) {
override fun <B : BlocBase<State, *>, State> onChange(bloc: B, change: Change<State>) {
super.onChange(bloc, change)
Log.i(bloc::class.simpleName, change.toString())
}

override fun <B : Bloc<Event, State>, Event, State> onEvent(bloc: B, event: Event) {
override fun <B : BlocBase<*, *>> onCreate(bloc: B) {
super.onCreate(bloc)
Log.i(bloc::class.simpleName, "Created")
}

override fun <B : Bloc<Event, *, *>, Event> onEvent(bloc: B, event: Event) {
super.onEvent(bloc, event)
Log.i(bloc::class.simpleName, event.toString())
}

override fun <B : Bloc<Event, State>, Event, State> onTransition(
override fun <B : BlocBase<*, SideEffect>, SideEffect> onSideEffect(
bloc: B,
sideEffect: SideEffect,
) {
super.onSideEffect(bloc, sideEffect)
Log.i(bloc::class.simpleName, sideEffect.toString())
}

override fun <B : Bloc<Event, State, *>, Event, State> onTransition(
bloc: B,
transition: Transition<Event, State>,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <State : Any, B : BlocBase<State>> rememberSaveableBloc(
fun <State : Any, B : BlocBase<State, *>> rememberSaveableBloc(
save: SaverScope.(B) -> State = {
assert(canBeSaved(it))
it.state
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -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")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import com.ptrbrynt.kotlin_bloc.core.Bloc

enum class CounterEvent { Increment, Decrement }

class CounterBloc : Bloc<CounterEvent, Int>(0) {
class CounterBloc : Bloc<CounterEvent, Int, Int>(0) {
init {
on<CounterEvent> { event ->
when (event) {
CounterEvent.Increment -> emit(state + 1)
CounterEvent.Decrement -> emit(state - 1)
}
emitSideEffect(state)
}
}
}
21 changes: 18 additions & 3 deletions core/src/main/java/com/ptrbrynt/kotlin_bloc/core/Bloc.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Event, State>(initial: State) : BlocBase<State>(initial) {
abstract class Bloc<Event, State, SideEffect>(initial: State) :
BlocBase<State, SideEffect>(initial) {
protected val eventFlow = MutableSharedFlow<Event>()

init {
Expand All @@ -30,14 +37,22 @@ abstract class Bloc<Event, State>(initial: State) : BlocBase<State>(initial) {
}

@PublishedApi
internal val emitter = object : Emitter<State> {
internal val emitter = object : Emitter<State, SideEffect> {
override suspend fun emit(state: State) {
mutableChangeFlow.emit(Change(this@Bloc.state, state))
}

override suspend fun emitEach(states: Flow<State>) {
states.onEach { emit(it) }.launchIn(blocScope)
}

override suspend fun emitSideEffect(sideEffect: SideEffect) {
mutableSideEffectFlow.emit(sideEffect)
}

override suspend fun emitSideEffects(sideEffects: Flow<SideEffect>) {
sideEffects.onEach { emitSideEffect(it) }.launchIn(blocScope)
}
}

/**
Expand All @@ -62,7 +77,7 @@ abstract class Bloc<Event, State>(initial: State) : BlocBase<State>(initial) {
* @param E The type of [Event] that this handles
*/
protected inline fun <reified E : Event> on(
noinline mapEventToState: suspend Emitter<State>.(E) -> Unit,
noinline mapEventToState: suspend Emitter<State, SideEffect>.(E) -> Unit,
) {
eventFlow
.transformEvents()
Expand Down
32 changes: 31 additions & 1 deletion core/src/main/java/com/ptrbrynt/kotlin_bloc/core/BlocBase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import kotlinx.coroutines.launch
* @param initial The initial [State]
*/
@Suppress("LeakingThis")
abstract class BlocBase<State>(initial: State) {
abstract class BlocBase<State, SideEffect>(initial: State) {

init {
Bloc.observer.onCreate(this)
Expand All @@ -29,6 +29,18 @@ abstract class BlocBase<State>(initial: State) {
}
}

protected val mutableSideEffectFlow = MutableSharedFlow<SideEffect>().apply {
blocScope.launch {
collect { onSideEffect(it) }
}
}

/**
* The [Flow] of [SideEffect]s
*/
val sideEffectFlow: Flow<SideEffect>
get() = mutableSideEffectFlow

/**
* The current [State] [Flow]
*/
Expand Down Expand Up @@ -61,4 +73,22 @@ abstract class BlocBase<State>(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)
}
}
17 changes: 13 additions & 4 deletions core/src/main/java/com/ptrbrynt/kotlin_bloc/core/BlocObserver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ abstract class BlocObserver {
*
* @param bloc The [Bloc] or [Cubit] which was created.
*/
open fun <B : BlocBase<State>, State> onCreate(bloc: B) {}
open fun <B : BlocBase<*, *>> onCreate(bloc: B) {}

/**
* Called whenever an [event] is `add`ed to any [bloc].
*
* @param bloc The [Bloc] to which the [Event] was `add`ed
* @param event The [Event] added to the [Bloc]
*/
open fun <B : Bloc<Event, State>, Event, State> onEvent(bloc: B, event: Event) {}
open fun <B : Bloc<Event, *, *>, Event> onEvent(bloc: B, event: Event) {}

/**
* Called whenever a [Change] occurs in any [Bloc] or [Cubit].
Expand All @@ -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 <B : BlocBase<State>, State> onChange(bloc: B, change: Change<State>) {}
open fun <B : BlocBase<State, *>, State> onChange(bloc: B, change: Change<State>) {}

/**
* Called whenever a [Transition] occurs in any [Bloc].
Expand All @@ -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 <B : Bloc<Event, State>, Event, State> onTransition(
open fun <B : Bloc<Event, State, *>, Event, State> onTransition(
bloc: B,
transition: Transition<Event, State>,
) {
}

/**
* Called whenever a [SideEffect] is emitted by a [Bloc] or [Cubit].
*/
open fun <B : BlocBase<*, SideEffect>, SideEffect> onSideEffect(
bloc: B,
sideEffect: SideEffect,
) {
}
}
17 changes: 14 additions & 3 deletions core/src/main/java/com/ptrbrynt/kotlin_bloc/core/Cubit.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -19,15 +19,26 @@ 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<State>(initial: State) : BlocBase<State>(initial), Emitter<State> {
abstract class Cubit<State, SideEffect>(initial: State) :
BlocBase<State, SideEffect>(initial), Emitter<State, SideEffect> {
override suspend fun emit(state: State) {
mutableChangeFlow.emit(Change(this.state, state))
}

override suspend fun emitEach(states: Flow<State>) {
states.onEach { emit(it) }.launchIn(blocScope)
}

override suspend fun emitSideEffect(sideEffect: SideEffect) {
mutableSideEffectFlow.emit(sideEffect)
}

override suspend fun emitSideEffects(sideEffects: Flow<SideEffect>) {
sideEffects.onEach { emitSideEffect(it) }.launchIn(blocScope)
}

}
14 changes: 12 additions & 2 deletions core/src/main/java/com/ptrbrynt/kotlin_bloc/core/Emitter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<State> {
interface Emitter<State, SideEffect> {
/**
* Emit a new [State]
*/
Expand All @@ -15,4 +15,14 @@ interface Emitter<State> {
* [emit] each [State] which is emitted by the [states] [Flow].
*/
suspend fun emitEach(states: Flow<State>)

/**
* Emit a new [SideEffect]
*/
suspend fun emitSideEffect(sideEffect: SideEffect)

/**
* Emit a [Flow] of [SideEffect]s.
*/
suspend fun emitSideEffects(sideEffects: Flow<SideEffect>)
}
Loading

0 comments on commit a5bddb4

Please sign in to comment.