Skip to content

Commit 6ebd54d

Browse files
author
Peter Bryant
committed
💥 Add Emitter receiver to mapEventToState method
Previously, Bloc implementations could emit from anywhere, making it hard to enforce correct usage. This commit removes the emit method from BlocBase and moves it to a new Emitter interface, which is used as the receiver for mapEventToState. This allows us to enforce the pattern whereby Blocs can only emit within the mapEventToState method. Additionally, Cubits now implement the Emitter interface, maintaining their ability to emit from anywhere.
1 parent 8dea671 commit 6ebd54d

File tree

12 files changed

+70
-29
lines changed

12 files changed

+70
-29
lines changed

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
@@ -1,11 +1,12 @@
11
package com.ptrbrynt.kotlin_bloc.compose.blocs
22

33
import com.ptrbrynt.kotlin_bloc.core.Bloc
4+
import com.ptrbrynt.kotlin_bloc.core.Emitter
45

56
enum class CounterEvent { Increment, Decrement }
67

78
class CounterBloc : Bloc<CounterEvent, Int>(0) {
8-
override suspend fun mapEventToState(event: CounterEvent) {
9+
override suspend fun Emitter<Int>.mapEventToState(event: CounterEvent) {
910
when (event) {
1011
CounterEvent.Increment -> emit(state + 1)
1112
CounterEvent.Decrement -> emit(state - 1)

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,22 @@ abstract class Bloc<Event, State>(initial: State) : BlocBase<State>(initial) {
2121
private val transitionFlow = eventFlow
2222
.onEach { onEvent(it) }
2323
.transformEvents()
24-
.onEach { mapEventToState(it) }
24+
.onEach { emitter.mapEventToState(it) }
2525
.zip(mutableChangeFlow) { event, change ->
2626
Transition(change.state, event, change.newState)
2727
}
2828
.onEach { onTransition(it) }
2929

30+
private val emitter = object : Emitter<State> {
31+
override suspend fun emit(state: State) {
32+
mutableChangeFlow.emit(Change(this@Bloc.state, state))
33+
}
34+
35+
override suspend fun emitEach(states: Flow<State>) {
36+
states.onEach { emit(it) }.launchIn(blocScope)
37+
}
38+
}
39+
3040
init {
3141
transitionFlow.launchIn(blocScope)
3242
}
@@ -39,8 +49,11 @@ abstract class Bloc<Event, State>(initial: State) : BlocBase<State>(initial) {
3949
* set of [State]s.
4050
*
4151
* [mapEventToState] can emit zero, one, or multiple [State]s for each [event].
52+
*
53+
* [mapEventToState] receives this [Bloc]s [Emitter], which allows [mapEventToState] to call
54+
* the [Emitter.emit] method to trigger a state change.
4255
*/
43-
abstract suspend fun mapEventToState(event: Event)
56+
abstract suspend fun Emitter<State>.mapEventToState(event: Event)
4457

4558
/**
4659
* Notifies the [Bloc] of a new [event], which triggers [mapEventToState].

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

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,6 @@ abstract class BlocBase<State>(initial: State) {
3434
*/
3535
val stateFlow = mutableChangeFlow.map { it.newState }
3636

37-
/**
38-
* Causes this to emit a new [state].
39-
*/
40-
protected suspend fun emit(state: State) {
41-
mutableChangeFlow.emit(Change(this@BlocBase.state, state))
42-
}
43-
4437
/**
4538
* The current [State]
4639
*/

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package com.ptrbrynt.kotlin_bloc.core
22

3+
import kotlinx.coroutines.flow.Flow
4+
import kotlinx.coroutines.flow.launchIn
5+
import kotlinx.coroutines.flow.onEach
6+
37
/**
48
* A [Cubit] is similar to a [Bloc] but has no notion of events,
59
* instead relying on methods to [emit] [State]s.
@@ -18,4 +22,12 @@ package com.ptrbrynt.kotlin_bloc.core
1822
* @see Bloc
1923
*/
2024

21-
abstract class Cubit<State>(initial: State) : BlocBase<State>(initial)
25+
abstract class Cubit<State>(initial: State) : BlocBase<State>(initial), Emitter<State> {
26+
override suspend fun emit(state: State) {
27+
mutableChangeFlow.emit(Change(this.state, state))
28+
}
29+
30+
override suspend fun emitEach(states: Flow<State>) {
31+
states.onEach { emit(it) }.launchIn(blocScope)
32+
}
33+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.ptrbrynt.kotlin_bloc.core
2+
3+
import kotlinx.coroutines.flow.Flow
4+
5+
/**
6+
* Interface which can be implemented on any object which can [emit] a [State].
7+
*/
8+
interface Emitter<State> {
9+
/**
10+
* Emit a new [State]
11+
*/
12+
suspend fun emit(state: State)
13+
14+
/**
15+
* [emit] each [State] which is emitted by the [states] [Flow].
16+
*/
17+
suspend fun emitEach(states: Flow<State>)
18+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.ptrbrynt.kotlin_bloc.core.blocs
22

33
import com.ptrbrynt.kotlin_bloc.core.Bloc
4+
import com.ptrbrynt.kotlin_bloc.core.Emitter
45
import com.ptrbrynt.kotlin_bloc.core.Transition
56
import kotlinx.coroutines.flow.Flow
67
import kotlinx.coroutines.flow.filter
@@ -11,7 +12,7 @@ open class CounterBloc(
1112
private val onTransitionCallback: ((Transition<CounterEvent, Int>) -> Unit)? = null,
1213
private val onEventCallback: ((CounterEvent) -> Unit)? = null,
1314
) : Bloc<CounterEvent, Int>(0) {
14-
override suspend fun mapEventToState(event: CounterEvent) {
15+
override suspend fun Emitter<Int>.mapEventToState(event: CounterEvent) {
1516
when (event) {
1617
CounterEvent.Increment -> emit(state + 1)
1718
CounterEvent.Decrement -> emit(state - 1)

core/src/test/java/com/ptrbrynt/kotlin_bloc/core/blocs/MultiFlowBloc.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
package com.ptrbrynt.kotlin_bloc.core.blocs
22

33
import com.ptrbrynt.kotlin_bloc.core.Bloc
4+
import com.ptrbrynt.kotlin_bloc.core.Emitter
45
import kotlinx.coroutines.ExperimentalCoroutinesApi
56
import kotlinx.coroutines.flow.MutableStateFlow
6-
import kotlinx.coroutines.flow.launchIn
7-
import kotlinx.coroutines.flow.onEach
87

98
sealed class MultiFlowEvent
109

@@ -17,9 +16,9 @@ data class MultiFlowNumberAdded(val number: Int) : MultiFlowEvent()
1716
class MultiFlowBloc : Bloc<MultiFlowEvent, List<Int>>(emptyList()) {
1817
private val numbers = MutableStateFlow(emptyList<Int>())
1918

20-
override suspend fun mapEventToState(event: MultiFlowEvent) {
19+
override suspend fun Emitter<List<Int>>.mapEventToState(event: MultiFlowEvent) {
2120
when (event) {
22-
is MultiFlowInitialized -> numbers.onEach { emit(it) }.launchIn(blocScope)
21+
is MultiFlowInitialized -> emitEach(numbers)
2322
is MultiFlowNumberAdded -> {
2423
numbers.tryEmit(numbers.value + event.number)
2524
}

core/src/test/java/com/ptrbrynt/kotlin_bloc/core/blocs/SeededBloc.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package com.ptrbrynt.kotlin_bloc.core.blocs
22

33
import com.ptrbrynt.kotlin_bloc.core.Bloc
4+
import com.ptrbrynt.kotlin_bloc.core.Emitter
45

56
class SeededBloc(private val seed: List<Int>, initial: Int) : Bloc<String, Int>(initial) {
6-
override suspend fun mapEventToState(event: String) {
7+
override suspend fun Emitter<Int>.mapEventToState(event: String) {
78
for (value in seed) {
89
emit(value)
910
}

docs/bloc-compose.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ Let's take a look at how to use `BlocComposer` to hook up a `Counter` widget to
6969
enum class CounterEvent { Incremented }
7070

7171
class CounterBloc: Bloc<CounterEvent, Int>(0) {
72-
override suspend fun mapEventToState(event: CounterEvent) {
72+
override suspend fun Emitter<Int>.mapEventToState(event: CounterEvent) {
7373
when (event) {
7474
is CounterEvent.Incremented -> emit(state + 1)
7575
}

docs/core-concepts.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,19 +140,21 @@ Using `Bloc` requires us to override the `mapEventToState` method. This will be
140140
enum class CounterEvent { Incremented }
141141

142142
class CounterBloc: Bloc<CounterEvent, Int>(0) {
143-
override suspend fun mapEventToState(event: CounterEvent) {
143+
override suspend fun Emitter<Int>.mapEventToState(event: CounterEvent) {
144144

145145
}
146146
}
147147
```
148148

149+
?> The `mapEventToState` method receives an `Emitter`, which provides the `emit` and `emitEach` methods we'll use below. This means that, while a `Cubit` can `emit` from anywhere, a `Bloc` can only `emit` from within the `mapEventToState` method.
150+
149151
We can then update `mapEventToState` to handle the `CounterEvent.Incremented` event:
150152

151153
```kotlin
152154
enum class CounterEvent { Incremented }
153155

154156
class CounterBloc: Bloc<CounterEvent, Int>(0) {
155-
override suspend fun mapEventToState(event: CounterEvent) {
157+
override suspend fun Emitter<Int>.mapEventToState(event: CounterEvent) {
156158
when (event) {
157159
CounterEvent.Incremented -> emit(state + 1)
158160
}
@@ -273,7 +275,7 @@ class CounterCubit : Cubit<Int>(0) {
273275
enum class CounterEvent { Increment }
274276

275277
class CounterBloc : Bloc<CounterEvent, Int>(0) {
276-
override suspend fun mapEventToState(event: CounterEvent) {
278+
override suspend fun Emitter<Int>.mapEventToState(event: CounterEvent) {
277279
when (event) {
278280
is CounterEvent.Increment -> emit(state + 1)
279281
}

docs/tutorials/todo-list.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -148,12 +148,12 @@ Now we have our events and states, we can create our `TodosBloc` class:
148148

149149
```kotlin
150150
class TodosBloc(private val todoDao: TodoDao) : Bloc<TodosEvent, TodosState>(TodosLoading) {
151-
override suspend fun mapEventToState(event: TodosEvent) {
151+
override suspend fun Emitter<TodosState>.mapEventToState(event: TodosEvent) {
152152
when (event) {
153153
is TodosInitialized -> {
154-
todoDao.getAllTodos()
155-
.onEach { emit(TodosLoadSuccess(it)) }
156-
.launchIn(blocScope)
154+
emitEach(
155+
todoDao.getAllTodos().map { TodosLoadSuccess(it) },
156+
)
157157
}
158158
is TodoAdded -> {
159159
todoDao.addTodo(Todo(name = event.name))
@@ -177,9 +177,9 @@ class TodosBloc(private val todoDao: TodoDao) : Bloc<TodosEvent, TodosState>(Tod
177177

178178
Let's briefly talk through how this works.
179179

180-
1. When the `TodosInitialized` event is added, the `TodosBloc` will get a `Flow` containing the current `List<Todo>` from our DAO using the `getAllTodos()` method. Room has built-in support for [observable queries](https://developer.android.com/training/data-storage/room/async-queries#flow-coroutines) using Flow.
181-
2. We are then adding an `onEach` call to this Flow, which `emit`s the current value provided by `getAllTodos()`. We then launch this flow using the `launchIn` method, and pass in the `blocScope` which is provided by the Bloc library.
182-
3. For all other events, we are simply calling the relevant DAO method. Thanks to Room's observable query, we don't need to manually retrieve and emit the new list of Todos, as this will be handled by the observable query we set up in steps 1 and 2.
180+
1. When the `TodosInitialized` event is added, the `TodosBloc` gets a `Flow` of the current list of `Todo`s using the `getAllTodos()` method on the DAO. It then `map`s this flow into `TodosLoadSuccess` states containing the current value.
181+
2. This flow of states is then passed into the `emitEach` method, which provided by the `Emitter` receiver. This will listen to the provided flow and emit each value.
182+
3. For all other events, we are simply calling the relevant DAO method. Thanks to Room's observable query, we don't need to manually retrieve and emit the new list of Todos, as this will be handled by the `emitEach` call we set up in steps 1 and 2.
183183

184184
## Koin Module
185185

sample/src/main/java/com/ptrbrynt/kotlin_bloc/sample/ui/blocs/CounterBloc.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
package com.ptrbrynt.kotlin_bloc.sample.ui.blocs
22

33
import com.ptrbrynt.kotlin_bloc.core.Bloc
4+
import com.ptrbrynt.kotlin_bloc.core.Emitter
45

56
enum class CounterEvent { Increment, Decrement }
67

78
class CounterBloc : Bloc<CounterEvent, Int>(0) {
8-
override suspend fun mapEventToState(event: CounterEvent) {
9+
override suspend fun Emitter<Int>.mapEventToState(event: CounterEvent) {
910
when (event) {
1011
CounterEvent.Increment -> emit(state + 1)
1112
CounterEvent.Decrement -> emit(state - 1)

0 commit comments

Comments
 (0)