Skip to content

Commit a5bddb4

Browse files
author
Peter Bryant
committed
💥 Support side-effects
1 parent f39688d commit a5bddb4

File tree

33 files changed

+333
-160
lines changed

33 files changed

+333
-160
lines changed

compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/BlocComposer.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import kotlinx.coroutines.flow.Flow
3939
*/
4040

4141
@Composable
42-
fun <B : BlocBase<State>, State> BlocComposer(
42+
fun <B : BlocBase<State, *>, State> BlocComposer(
4343
bloc: B,
4444
transformStates: Flow<State>.() -> Flow<State> = { this },
4545
content: @Composable (State) -> Unit,

compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/BlocListener.kt

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,51 +8,51 @@ import com.ptrbrynt.kotlin_bloc.core.BlocBase
88
import kotlinx.coroutines.flow.Flow
99

1010
/**
11-
* Takes a [bloc] and an [onState] callback and invokes [onState] in response to `state` changes
12-
* in the [bloc].
11+
* Takes a [bloc] and an [onSideEffect] callback and invokes [onSideEffect] in response to side
12+
* effects in the [bloc].
1313
*
14-
* It should be used for side-effects resulting from new `state`s being emitted by the [bloc] e.g.
14+
* It should be used for side-effects resulting from new side-effects being emitted by the [bloc] e.g.
1515
* navigation, showing a snackbar etc.
1616
*
1717
* If you want to build composables in response to new states, use [BlocComposer]
1818
*
1919
* ```kotlin
20-
* BlocListener(bloc) { state ->
21-
* // React to the new state here
20+
* BlocListener(bloc) { sideEffect ->
21+
* // React to the new side effect here
2222
* }
2323
* ```
2424
*
25-
* * An optional [transformStates] can be implemented for more granular control over
25+
* * An optional [transformSideEffects] can be implemented for more granular control over
2626
* the frequency and specificity with which transitions occur.
2727
*
28-
* For example, to debounce the state changes:
28+
* For example, to debounce the side effects:
2929
*
3030
* ```kotlin
3131
* BlocListener(
3232
* myBloc,
33-
* transformStates = { this.debounce(1000) },
33+
* transformSideEffects = { this.debounce(1000) },
3434
* ) {
35-
* // React to the new state here
35+
* // React to the new side-effect here
3636
* }
3737
* ```
3838
*
3939
* @param bloc The bloc or cubit that the [BlocListener] will interact with.
40-
* @param onState The callback function which will be invoked whenever a new `state` is emitted by the [bloc].
41-
* @param transformStates Provides more granular control over the [State] flow.
40+
* @param onSideEffect The callback function which will be invoked whenever a new `state` is emitted by the [bloc].
41+
* @param transformSideEffects Provides more granular control over the [State] flow.
4242
* @see BlocComposer
4343
*/
4444

4545
@Composable
46-
fun <B : BlocBase<State>, State> BlocListener(
46+
fun <B : BlocBase<*, SideEffect>, SideEffect> BlocListener(
4747
bloc: B,
48-
transformStates: Flow<State>.() -> Flow<State> = { this },
49-
onState: suspend (State) -> Unit,
48+
transformSideEffects: Flow<SideEffect>.() -> Flow<SideEffect> = { this },
49+
onSideEffect: suspend (SideEffect) -> Unit,
5050
) {
51-
val state by bloc.stateFlow.transformStates().collectAsState(initial = null)
51+
val state by bloc.sideEffectFlow.transformSideEffects().collectAsState(initial = null)
5252

5353
state?.let {
5454
LaunchedEffect(it) {
55-
onState(it)
55+
onSideEffect(it)
5656
}
5757
}
5858
}

compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/BlocSaver.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import com.ptrbrynt.kotlin_bloc.core.BlocBase
88
* A [Saver] which enables the state of a Bloc or Cubit to be saved
99
* and restored.
1010
*/
11-
internal fun <State : Any, B : BlocBase<State>> blocSaver(
11+
internal fun <State : Any, B : BlocBase<State, *>> blocSaver(
1212
save: SaverScope.(B) -> State = { it.state },
1313
restore: (State) -> B,
1414
) = Saver(

compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/BlocSelector.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import kotlinx.coroutines.flow.map
2727
*/
2828

2929
@Composable
30-
fun <B : BlocBase<State>, State, T> BlocSelector(
30+
fun <B : BlocBase<State, *>, State, T> BlocSelector(
3131
bloc: B,
3232
selector: (State) -> T,
3333
content: @Composable (T) -> Unit,

compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/LoggingBlocObserver.kt

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,32 @@ import com.ptrbrynt.kotlin_bloc.core.Transition
1010
/**
1111
* A [BlocObserver] which logs all bloc events to the console.
1212
*/
13-
1413
class LoggingBlocObserver : BlocObserver() {
1514

16-
override fun <B : BlocBase<State>, State> onCreate(bloc: B) {
17-
super.onCreate(bloc)
18-
Log.i(bloc::class.simpleName, "Created")
19-
}
20-
21-
override fun <B : BlocBase<State>, State> onChange(bloc: B, change: Change<State>) {
15+
override fun <B : BlocBase<State, *>, State> onChange(bloc: B, change: Change<State>) {
2216
super.onChange(bloc, change)
2317
Log.i(bloc::class.simpleName, change.toString())
2418
}
2519

26-
override fun <B : Bloc<Event, State>, Event, State> onEvent(bloc: B, event: Event) {
20+
override fun <B : BlocBase<*, *>> onCreate(bloc: B) {
21+
super.onCreate(bloc)
22+
Log.i(bloc::class.simpleName, "Created")
23+
}
24+
25+
override fun <B : Bloc<Event, *, *>, Event> onEvent(bloc: B, event: Event) {
2726
super.onEvent(bloc, event)
2827
Log.i(bloc::class.simpleName, event.toString())
2928
}
3029

31-
override fun <B : Bloc<Event, State>, Event, State> onTransition(
30+
override fun <B : BlocBase<*, SideEffect>, SideEffect> onSideEffect(
31+
bloc: B,
32+
sideEffect: SideEffect,
33+
) {
34+
super.onSideEffect(bloc, sideEffect)
35+
Log.i(bloc::class.simpleName, sideEffect.toString())
36+
}
37+
38+
override fun <B : Bloc<Event, State, *>, Event, State> onTransition(
3239
bloc: B,
3340
transition: Transition<Event, State>,
3441
) {

compose/src/main/java/com/ptrbrynt/kotlin_bloc/compose/RememberSaveableBloc.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import com.ptrbrynt.kotlin_bloc.core.BlocBase
4545
* @throws AssertionError if no [save] parameter is provided and the [State] type cannot be saved.
4646
*/
4747
@Composable
48-
fun <State : Any, B : BlocBase<State>> rememberSaveableBloc(
48+
fun <State : Any, B : BlocBase<State, *>> rememberSaveableBloc(
4949
save: SaverScope.(B) -> State = {
5050
assert(canBeSaved(it))
5151
it.state

compose/src/test/java/com/ptrbrynt/kotlin_bloc/compose/LoggingBlocObserverTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ import com.ptrbrynt.kotlin_bloc.compose.blocs.CounterEvent
77
import com.ptrbrynt.kotlin_bloc.core.Bloc
88
import io.mockk.mockk
99
import io.mockk.verifyOrder
10-
import java.io.PrintStream
1110
import org.junit.Rule
1211
import org.junit.Test
1312
import org.junit.runner.RunWith
1413
import org.robolectric.annotation.Config
1514
import org.robolectric.shadows.ShadowLog
15+
import java.io.PrintStream
1616

1717
@RunWith(AndroidJUnit4::class)
1818
@Config(shadows = [ShadowLog::class])
@@ -38,6 +38,7 @@ class LoggingBlocObserverTest {
3838
stream.println("I/CounterBloc: Increment")
3939
stream.println("I/CounterBloc: Change(state=0, newState=1)")
4040
stream.println("I/CounterBloc: Transition(state=0, event=Increment, newState=1)")
41+
stream.println("I/CounterBloc: 1")
4142
}
4243
}
4344
}

compose/src/test/java/com/ptrbrynt/kotlin_bloc/compose/blocs/CounterBloc.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import com.ptrbrynt.kotlin_bloc.core.Bloc
44

55
enum class CounterEvent { Increment, Decrement }
66

7-
class CounterBloc : Bloc<CounterEvent, Int>(0) {
7+
class CounterBloc : Bloc<CounterEvent, Int, Int>(0) {
88
init {
99
on<CounterEvent> { event ->
1010
when (event) {
1111
CounterEvent.Increment -> emit(state + 1)
1212
CounterEvent.Decrement -> emit(state - 1)
1313
}
14+
emitSideEffect(state)
1415
}
1516
}
1617
}

core/src/main/java/com/ptrbrynt/kotlin_bloc/core/Bloc.kt

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,18 @@ import kotlinx.coroutines.launch
1212
* Takes [Event]s as input and transforms them into a [Flow]
1313
* of [State]s as an output.
1414
*
15+
* Also emits a [Flow] of [SideEffect]s, which are non-state outputs emitted as a result of new
16+
* [Event]s.
17+
*
1518
* @param initial The initial [State]
19+
* @param Event The type of event this can receive
20+
* @param State The type of state this emits
21+
* @param SideEffect The type of side-effect this can emit
1622
* @see Cubit
1723
*/
1824
@Suppress("LeakingThis")
19-
abstract class Bloc<Event, State>(initial: State) : BlocBase<State>(initial) {
25+
abstract class Bloc<Event, State, SideEffect>(initial: State) :
26+
BlocBase<State, SideEffect>(initial) {
2027
protected val eventFlow = MutableSharedFlow<Event>()
2128

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

3239
@PublishedApi
33-
internal val emitter = object : Emitter<State> {
40+
internal val emitter = object : Emitter<State, SideEffect> {
3441
override suspend fun emit(state: State) {
3542
mutableChangeFlow.emit(Change(this@Bloc.state, state))
3643
}
3744

3845
override suspend fun emitEach(states: Flow<State>) {
3946
states.onEach { emit(it) }.launchIn(blocScope)
4047
}
48+
49+
override suspend fun emitSideEffect(sideEffect: SideEffect) {
50+
mutableSideEffectFlow.emit(sideEffect)
51+
}
52+
53+
override suspend fun emitSideEffects(sideEffects: Flow<SideEffect>) {
54+
sideEffects.onEach { emitSideEffect(it) }.launchIn(blocScope)
55+
}
4156
}
4257

4358
/**
@@ -62,7 +77,7 @@ abstract class Bloc<Event, State>(initial: State) : BlocBase<State>(initial) {
6277
* @param E The type of [Event] that this handles
6378
*/
6479
protected inline fun <reified E : Event> on(
65-
noinline mapEventToState: suspend Emitter<State>.(E) -> Unit,
80+
noinline mapEventToState: suspend Emitter<State, SideEffect>.(E) -> Unit,
6681
) {
6782
eventFlow
6883
.transformEvents()

core/src/main/java/com/ptrbrynt/kotlin_bloc/core/BlocBase.kt

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import kotlinx.coroutines.launch
1414
* @param initial The initial [State]
1515
*/
1616
@Suppress("LeakingThis")
17-
abstract class BlocBase<State>(initial: State) {
17+
abstract class BlocBase<State, SideEffect>(initial: State) {
1818

1919
init {
2020
Bloc.observer.onCreate(this)
@@ -29,6 +29,18 @@ abstract class BlocBase<State>(initial: State) {
2929
}
3030
}
3131

32+
protected val mutableSideEffectFlow = MutableSharedFlow<SideEffect>().apply {
33+
blocScope.launch {
34+
collect { onSideEffect(it) }
35+
}
36+
}
37+
38+
/**
39+
* The [Flow] of [SideEffect]s
40+
*/
41+
val sideEffectFlow: Flow<SideEffect>
42+
get() = mutableSideEffectFlow
43+
3244
/**
3345
* The current [State] [Flow]
3446
*/
@@ -61,4 +73,22 @@ abstract class BlocBase<State>(initial: State) {
6173
Bloc.observer.onChange(this, change)
6274
this.state = change.newState
6375
}
76+
77+
/**
78+
* Called whenever a [SideEffect] is emitted.
79+
*
80+
* **Note: `super.onSideEffect` should always be called first.**
81+
*
82+
* ```kotlin
83+
* override fun onSideEffect(sideEffect: SideEffect) {
84+
* // Always call super.onSideEffect first
85+
* super.onSideEffect(sideEffect)
86+
*
87+
* // Custom logic goes here
88+
* }
89+
* ```
90+
*/
91+
open fun onSideEffect(sideEffect: SideEffect) {
92+
Bloc.observer.onSideEffect(this, sideEffect)
93+
}
6494
}

0 commit comments

Comments
 (0)