-
Notifications
You must be signed in to change notification settings - Fork 27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add collectWhileInState functions with flow builders #182
Conversation
The new operator crashes when running the native tests, I will look at that. We can also see 2 different flakiness behaviors here:
|
@@ -76,5 +76,5 @@ class OnActionInStateSideEffectBuilder<InputState : S, S : Any, A : Any>( | |||
} | |||
|
|||
|
|||
typealias OnActionBlock<InputState, S, A> = suspend (action: A, state: InputState) -> ChangeState<S> | |||
typealias OnActionHandler<InputState, S, A> = suspend (action: A, state: InputState) -> ChangeState<S> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice 👍
@@ -55,5 +55,5 @@ class OnEnterInStateSideEffectBuilder<InputState : S, S : Any, A : Any>( | |||
} | |||
} | |||
|
|||
typealias InStateOnEnterBlock<InputState, S> = suspend (state: InputState) -> ChangeState<S> | |||
typealias InStateOnEnterHandler<InputState, S> = suspend (state: InputState) -> ChangeState<S> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
@@ -78,4 +164,4 @@ class InStateBuilderBlock<InputState : S, S : Any, A : Any>( | |||
} | |||
} | |||
|
|||
typealias InStateObserverBlock<T, InputState, S> = suspend (value: T, state: InputState) -> ChangeState<S> | |||
typealias InStateObserverHandler<T, InputState, S> = suspend (value: T, state: InputState) -> ChangeState<S> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 i like the name handler
much more!
@@ -0,0 +1,60 @@ | |||
package com.freeletics.flowredux.dsl.internal |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpicking: I find the class name confusing 😄
Not sure what a better name is and how to differentiate it from the other CollectInStateSideEffectBuilder
...
Maybe CollectAndTransformStateFlowInStateSideEffectBuilder
? (that's a mouthful, but something like that?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does CollectInStateBasedOnStateSideEffectBuilder
sound?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good to me 👍
override fun generateSideEffect(): SideEffect<S, Action<S, A>> { | ||
return { actions: Flow<Action<S, A>>, getState: GetState<S> -> | ||
actions.whileInState(isInState, getState) { inStateActions -> | ||
inStateActions.map { getState() as InputState } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, that lines of code is hard to read for me (mentally I mean) because it requires knowledge that FlowRedux
(inherited from RxRedux
) is using the concept of SideEffects
and the loopBack of each action to every SideEffect
in the core flowredux library and that the loopback is triggered after each action went through the reducer....
So while it works, I was wondering if instead of subscribing to the Actions
directly collect the StateFlow
from FlowReduxStateMachine
?
Then at least it would be nicer to read and maintain, otherwise as a Maintainer of the library I always need to have the implementation details of FlowRedux in mind.
Alternatively (and I think that would be more feasible) I would probably recommend to extract inStateActions.map { getState() as InputState }
to something like fun Flow<Action<S,A>>.getCurrentStateFlow(getState : GetState<S>) = this.map { getState() as InputState }.distinctUntilChanged()
so that "implementation details" of how to get to such a flow that emits current state is hidden from the reader of this piece of code.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So while it works, I was wondering if instead of subscribing to the Actions directly collect the StateFlow from FlowReduxStateMachine?
We can't do that ourselves because we don't have the StateFlow available here. A user could do it with the existing collectWhileInState
, but I think doing that is weird and you need to cast yourself.
Extracting parts of this into a function to make readability better makes sense
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
9abaac9 this should improve the readability and also adds some comments to the extracted function.
import kotlinx.coroutines.flow.flatMapConcat | ||
|
||
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class, ExperimentalTime::class) | ||
class CollectStateWhileInStateTest { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpicking: I'm wondering if we should merge this tests with the other CollectWhileInStateTest
to only have one "collectWhileInState" file with all tests of all variations...
OR
if we should have better naming conventions (i.e. class under test with "Test" suffix, i.e. then CollectInStateSideEffectBuilderTest
...
I lean more towards the first option (merge all inState<S> { collectWhileInState(...) }
into 1 file ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine with merging 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
) { | ||
on(FlatMapPolicy.LATEST, handler) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💪
} | ||
|
||
inState<ShowContentAndLoadingNextPageErrorPaginationState> { | ||
onEnter(block = ::moveToContentStateAfter3Seconds) | ||
onEnter(::moveToContentStateAfter3Seconds) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Much better to readability 🎉
handler: InStateObserverHandler<T, InputState, S> | ||
) { | ||
collectWhileInState(flow, FlatMapPolicy.CONCAT, handler) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💪
I could also think about a more specific use case and therefore a convenience method would be nice where the orifinal state that triggered Something of this method signature: fun collectWhileInState(
flowBuilder: (InputState) -> Flow<T>, // so not a Flow<InputState> but just `InputState` and only triggers once with the inputState that caused collectWhileInState to start
handler: InStateObserverHandler<T, InputState, S>
) i.e. if you want to observe a database for 1 particular Column / item by primary key, with SQLDelight or Room you don't want to cancel collection of query and start again because of flatMap transformation or so. inState<ShowItemState> {
collectWhileInState({ state : ShowItemState -> db.queryById(state.item.id) ) { item, state ->
MutateState { copy(item = item) // So this triggers multiple times whenever the Item in the database gets updated
}
} Another use case I could think of is connecting to a websocket when you would like to start only once the websocket connection. Under the hood it could be just a resuing what you are about to introduce with the PR: val stateFlow : Flow<InputState> = ...
stateFlow.take(1).flatMapLatest { state -> flowBuilder(state) } ... Do you think something like that could be useful? I'm happy to introduce something like this as a follow up PR ... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you so much @gabrielittner for working on this stuff 🙇♂️
The reason why I didn't make it an extension is that I thought having a I don't have a strong opinion here, so feel free to change it.
Updated to |
👍 🚢 |
Closes #161
This adds an
collectWhileInState
overload thatflowBuilder: (Flow<InputState>) -> Flow<T>
as parameter instead of a simpleFlow<T>
. The implementation is based on the same operator that onAction uses. This allows us to cancel it when the state changes without missing any actions while we are in the action. Any action emission is mapped into the current state anddistinctUntilChanged
is used to filter out emissions where the state did not change. The resultingFlow
is then passed toflowBuilder
and the return value of that is what we use like usually.While I was adding the overload to
InStateBuilderBlock
I've also updated the docs on all methods, added overloads withoutflatMapPolicy
(closes #179) and renamedblock
tohandler
.