Skip to content

Commit ce8335c

Browse files
FlowReduxLogger (#55)
* logger * add logger * add logger * dif * dif * fix tests * Update flowredux/src/commonTest/kotlin/com/hoc081098/flowredux/FlowReduxStoreTest.kt Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 7c494c1 commit ce8335c

File tree

12 files changed

+203
-44
lines changed

12 files changed

+203
-44
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.hoc081098.flowredux
2+
3+
public fun interface FlowReduxLogger<Action, State> {
4+
/**
5+
* Called when the reducer is called with [action] and [prevState] to produce [nextState].
6+
* @param action The action that was dispatched.
7+
* @param prevState The previous state.
8+
* @param nextState The new state produced by the reducer.
9+
*/
10+
public fun onReduced(action: Action, prevState: State, nextState: State)
11+
12+
public companion object {
13+
/**
14+
* Returns an empty logger that does nothing.
15+
* Use this logger to disable logging.
16+
*/
17+
@Suppress("UNCHECKED_CAST")
18+
public fun <Action, State> empty(): FlowReduxLogger<Action, State> = Empty as FlowReduxLogger<Action, State>
19+
}
20+
}
21+
22+
private object Empty : FlowReduxLogger<Any?, Any?> {
23+
override fun onReduced(action: Any?, prevState: Any?, nextState: Any?): Unit = Unit
24+
}
25+
26+
public fun <Action, State> Reducer<Action, State>.withLogger(
27+
logger: FlowReduxLogger<Action, State>,
28+
): Reducer<Action, State> = when (logger) {
29+
FlowReduxLogger.empty<Action, State>() -> this
30+
else -> Reducer { prevState, action ->
31+
val nextState = this(prevState, action)
32+
33+
logger.onReduced(action, prevState, nextState)
34+
35+
nextState
36+
}
37+
}

flowredux/src/commonMain/kotlin/com/hoc081098/flowredux/FlowReduxStore.kt

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,7 @@ package com.hoc081098.flowredux
33
import kotlin.coroutines.CoroutineContext
44
import kotlinx.coroutines.CoroutineScope
55
import kotlinx.coroutines.Job
6-
import kotlinx.coroutines.channels.Channel
7-
import kotlinx.coroutines.channels.ReceiveChannel
8-
import kotlinx.coroutines.flow.Flow
96
import kotlinx.coroutines.flow.StateFlow
10-
import kotlinx.coroutines.flow.emptyFlow
11-
import kotlinx.coroutines.flow.launchIn
12-
import kotlinx.coroutines.flow.mapNotNull
13-
import kotlinx.coroutines.flow.onCompletion
14-
import kotlinx.coroutines.flow.onEach
157
import kotlinx.coroutines.job
168

179
// TODO: Consider using AutoCloseable since Kotlin 1.8.20
@@ -70,35 +62,6 @@ public fun <Action, State> CoroutineScope.createFlowReduxStore(
7062
return store
7163
}
7264

73-
/**
74-
* Create a [SideEffect] that maps all actions to [Output]s and send them to a [Channel].
75-
* The result [Channel] will be closed when the [SideEffect] is cancelled (when calling [FlowReduxStore.close]).
76-
*
77-
* @param capacity The capacity of the [Channel].
78-
* @param transformActionToOutput A function that maps an [Action] to an [Output].
79-
* If the function returns `null`, the [Action] will be ignored.
80-
* Otherwise, the [Action] will be mapped to an [Output] and sent to the [Channel].
81-
* @return A [Pair] of the [SideEffect] and a [Flow] of [Output]s.
82-
*/
83-
public fun <Action, State, Output> allActionsToOutputChannelSideEffect(
84-
capacity: Int = Channel.UNLIMITED,
85-
transformActionToOutput: (Action) -> Output?,
86-
): Pair<SideEffect<Action, State>, ReceiveChannel<Output>> {
87-
val actionChannel = Channel<Output>(capacity)
88-
89-
val sideEffect = SideEffect<Action, State> { actionFlow, _, coroutineScope ->
90-
actionFlow
91-
.mapNotNull(transformActionToOutput)
92-
.onEach(actionChannel::send)
93-
.onCompletion { actionChannel.close() }
94-
.launchIn(coroutineScope)
95-
96-
emptyFlow()
97-
}
98-
99-
return sideEffect to actionChannel
100-
}
101-
10265
@Suppress("FunctionName") // Factory function
10366
public fun <Action, State> FlowReduxStore(
10467
coroutineContext: CoroutineContext,

flowredux/src/commonMain/kotlin/com/hoc081098/flowredux/SideEffect.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
package com.hoc081098.flowredux
22

33
import kotlinx.coroutines.CoroutineScope
4+
import kotlinx.coroutines.channels.Channel
5+
import kotlinx.coroutines.channels.ReceiveChannel
46
import kotlinx.coroutines.flow.Flow
57
import kotlinx.coroutines.flow.StateFlow
8+
import kotlinx.coroutines.flow.emptyFlow
9+
import kotlinx.coroutines.flow.launchIn
10+
import kotlinx.coroutines.flow.mapNotNull
11+
import kotlinx.coroutines.flow.onCompletion
12+
import kotlinx.coroutines.flow.onEach
613

714
/**
815
* It is a function which takes a stream of actions and returns a stream of actions. Actions in, actions out
@@ -23,3 +30,32 @@ public fun interface SideEffect<Action, State> {
2330
coroutineScope: CoroutineScope
2431
): Flow<Action>
2532
}
33+
34+
/**
35+
* Create a [SideEffect] that maps all actions to [Output]s and send them to a [Channel].
36+
* The result [Channel] will be closed when the [SideEffect] is cancelled (when calling [FlowReduxStore.close]).
37+
*
38+
* @param capacity The capacity of the [Channel].
39+
* @param transformActionToOutput A function that maps an [Action] to an [Output].
40+
* If the function returns `null`, the [Action] will be ignored.
41+
* Otherwise, the [Action] will be mapped to an [Output] and sent to the [Channel].
42+
* @return A [Pair] of the [SideEffect] and a [Flow] of [Output]s.
43+
*/
44+
public fun <Action, State, Output : Any> allActionsToOutputChannelSideEffect(
45+
capacity: Int = Channel.UNLIMITED,
46+
transformActionToOutput: (Action) -> Output?,
47+
): Pair<SideEffect<Action, State>, ReceiveChannel<Output>> {
48+
val actionChannel = Channel<Output>(capacity)
49+
50+
val sideEffect = SideEffect<Action, State> { actionFlow, _, coroutineScope ->
51+
actionFlow
52+
.mapNotNull(transformActionToOutput)
53+
.onEach(actionChannel::send)
54+
.onCompletion { actionChannel.close() }
55+
.launchIn(coroutineScope)
56+
57+
emptyFlow()
58+
}
59+
60+
return sideEffect to actionChannel
61+
}

flowredux/src/commonTest/kotlin/com/hoc081098/flowredux/FlowReduxStoreTest.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ private fun TestScope.createScope() = CoroutineScope(
4949
)
5050
)
5151

52-
fun <Action, State> CoroutineScope.createTestFlowReduxStore(
52+
fun <Action : Any, State> CoroutineScope.createTestFlowReduxStore(
5353
initialState: State,
5454
sideEffects: List<SideEffect<Action, State>>,
5555
reducer: Reducer<Action, State>,
@@ -511,6 +511,16 @@ class FlowReduxStoreTest {
511511

512512
cancelled.await()
513513
}
514+
515+
@Test
516+
fun `empty logger`() = runTest {
517+
val l1: FlowReduxLogger<Int, String> = FlowReduxLogger.empty()
518+
val l2: FlowReduxLogger<String, Int> = FlowReduxLogger.empty()
519+
val l3: FlowReduxLogger<Int, Int> = FlowReduxLogger.empty()
520+
val l4: FlowReduxLogger<String, String> = FlowReduxLogger.empty()
521+
522+
assertEquals(1, setOf(l1, l2, l3, l4).size)
523+
}
514524
}
515525

516526
@ExperimentalCoroutinesApi
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package com.hoc081098.github_search_kmm
2+
3+
actual fun isDebug(): Boolean = BuildConfig.DEBUG
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package com.hoc081098.github_search_kmm
2+
3+
expect fun isDebug(): Boolean
Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.hoc081098.github_search_kmm.presentation
22

3+
import com.hoc081098.flowredux.Reducer
34
import com.hoc081098.flowredux.allActionsToOutputChannelSideEffect
45
import com.hoc081098.flowredux.createFlowReduxStore
6+
import com.hoc081098.flowredux.withLogger
57
import com.hoc081098.github_search_kmm.domain.usecase.SearchRepoItemsUseCase
68
import com.hoc081098.github_search_kmm.utils.flip
79
import com.hoc081098.kmp.viewmodel.ViewModel
@@ -13,21 +15,24 @@ import kotlinx.coroutines.flow.receiveAsFlow
1315
open class GithubSearchViewModel(
1416
searchRepoItemsUseCase: SearchRepoItemsUseCase,
1517
) : ViewModel() {
16-
private val singleEventSideEffect =
18+
private val sendSingleEventSideEffect =
1719
allActionsToOutputChannelSideEffect<GithubSearchAction, GithubSearchState, GithubSearchSingleEvent> {
1820
it.toGithubSearchSingleEventOrNull()
1921
}
2022

23+
private val storeLogger = githubSearchFlowReduxLogger()
24+
2125
private val store = viewModelScope.createFlowReduxStore(
2226
initialState = GithubSearchState.initial(),
2327
sideEffects = GithubSearchSideEffects(searchRepoItemsUseCase).sideEffects +
24-
singleEventSideEffect.first,
25-
reducer = GithubSearchAction::reduce.flip(),
28+
sendSingleEventSideEffect.first,
29+
reducer = Reducer(flip(GithubSearchAction::reduce))
30+
.withLogger(storeLogger)
2631
)
2732

2833
fun dispatch(action: GithubSearchAction) = store.dispatch(action)
2934

3035
val stateFlow: NonNullStateFlowWrapper<GithubSearchState> = store.stateFlow.wrap()
3136

32-
val eventFlow: NonNullFlowWrapper<GithubSearchSingleEvent> = singleEventSideEffect.second.receiveAsFlow().wrap()
37+
val eventFlow: NonNullFlowWrapper<GithubSearchSingleEvent> = sendSingleEventSideEffect.second.receiveAsFlow().wrap()
3338
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.hoc081098.github_search_kmm.presentation
2+
3+
import com.hoc081098.flowredux.FlowReduxLogger
4+
import com.hoc081098.github_search_kmm.isDebug
5+
import com.hoc081098.github_search_kmm.utils.EitherLCE
6+
import com.hoc081098.github_search_kmm.utils.diff
7+
import io.github.aakira.napier.Napier
8+
import kotlin.LazyThreadSafetyMode.NONE
9+
10+
private val GithubSearchAction.debugString: String
11+
get() = when (this) {
12+
GithubSearchAction.LoadNextPage,
13+
GithubSearchAction.Retry,
14+
is GithubSearchAction.Search,
15+
is SideEffectAction.TextChanged -> toString()
16+
17+
is SideEffectAction.SearchLCE -> arrayOf(
18+
"term" to term,
19+
"nextPage" to nextPage,
20+
"lce" to when (lce) {
21+
EitherLCE.Loading -> "Loading"
22+
is EitherLCE.ContentOrError -> lce.either.fold(
23+
ifLeft = { "Left($it)" },
24+
ifRight = { "Right(${it.size})" },
25+
)
26+
},
27+
).joinToString(
28+
prefix = "SearchLCE { ",
29+
postfix = " }",
30+
separator = ", ",
31+
) { (k, v) -> "$k: $v" }
32+
}
33+
34+
private inline val GithubSearchState.debugMap: Map<String, Any?>
35+
get() = mapOf(
36+
"page" to page,
37+
"term" to term,
38+
"items.size" to items.size,
39+
"isLoading" to isLoading,
40+
"error" to error,
41+
"hasReachedMax" to hasReachedMax,
42+
)
43+
44+
private inline val GithubSearchState.debugString: String
45+
get() = debugMap.entries.joinToString(
46+
prefix = "GithubSearchState { ",
47+
postfix = " }",
48+
separator = ", ",
49+
) { (k, v) -> "$k: $v" }
50+
51+
internal fun githubSearchFlowReduxLogger(): FlowReduxLogger<GithubSearchAction, GithubSearchState> =
52+
if (isDebug()) {
53+
FlowReduxLogger { action, prevState, nextState ->
54+
val diffString by lazy(NONE) {
55+
prevState.debugMap
56+
.diff(nextState.debugMap)
57+
.joinToString(separator = ", ", prefix = "{ ", postfix = " }") { (k, v1, v2) -> "$k: ($v1 -> $v2)" }
58+
}
59+
60+
Napier.d(
61+
"""onReduced {
62+
| Action : ${action.debugString}
63+
| Prev state: ${prevState.debugString}
64+
| Next state: ${nextState.debugString}
65+
| Diff : ${if (prevState == nextState) "{ }" else diffString}
66+
|}
67+
""".trimMargin(),
68+
tag = "GithubSearchViewModel"
69+
)
70+
}
71+
} else {
72+
FlowReduxLogger.empty()
73+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.hoc081098.github_search_kmm.utils
2+
3+
inline fun <K, V> Map<K, V>.diff(other: Map<K, V>): List<Triple<K, V?, V?>> = entries
4+
.subtract(other.entries)
5+
.map { (k, v) -> Triple(k, this[k], other[k]) }

shared/src/commonMain/kotlin/com/hoc081098/github_search_kmm/utils/flip.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ package com.hoc081098.github_search_kmm.utils
44
* A function that flips arguments order of a binary function.
55
* @return A function with the same behavior as the input, but with arguments flipped.
66
*/
7-
inline fun <A, B, C> ((A, B) -> C).flip(): (B, A) -> C = { b, a -> this(a, b) }
7+
inline fun <A, B, C> flip(crossinline function: (A, B) -> C): (B, A) -> C = { b, a -> function(a, b) }

0 commit comments

Comments
 (0)