Skip to content
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

Merged
merged 10 commits into from
Aug 4, 2021

Conversation

gabrielittner
Copy link
Member

Closes #161

This adds an collectWhileInState overload that flowBuilder: (Flow<InputState>) -> Flow<T> as parameter instead of a simple Flow<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 and distinctUntilChanged is used to filter out emissions where the state did not change. The resulting Flow is then passed to flowBuilder 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 without flatMapPolicy (closes #179) and renamed block to handler.

@gabrielittner
Copy link
Member Author

The new operator crashes when running the native tests, I will look at that.

We can also see 2 different flakiness behaviors here:

  • on Action triggers only while in custom state fails because one of the counters is 2 instead of 1, this looks like it might be an actual bug in the implementation or how the test is implemented
  • 2 different tests failed once because they were already in the second state when we were expecting the initial state, I think this might be a test specific race condition. I'll check if my is assumption is right tomorrow.

@@ -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>
Copy link
Collaborator

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>
Copy link
Collaborator

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>
Copy link
Collaborator

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
Copy link
Collaborator

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?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does CollectInStateBasedOnStateSideEffectBuilder sound?

Copy link
Collaborator

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 }
Copy link
Collaborator

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?

Copy link
Member Author

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

Copy link
Member Author

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 {
Copy link
Collaborator

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 ...

Copy link
Member Author

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 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

) {
on(FlatMapPolicy.LATEST, handler)
}
Copy link
Collaborator

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)
}
}
}
Copy link
Collaborator

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)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💪

@sockeqwe
Copy link
Collaborator

sockeqwe commented Aug 4, 2021

I could also think about a more specific use case and therefore a convenience method would be nice where the orifinal state that triggered collectWhileInstate is passed as a parameter but it should not invoke again again with newer state updates.

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 ...

Copy link
Collaborator

@sockeqwe sockeqwe left a 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 🙇‍♂️

@gabrielittner
Copy link
Member Author

Have you considered making it an extension function on Flow<Action<S, A>>? For me, personally, having one parameter less (actions) improves readability a bit (not a game changer, thus up to you).

The reason why I didn't make it an extension is that I thought having a currentStateFlow method that gets some parameters makes more sense logically then transforming the actions Flow into a state Flow with an operator. If you just look at the point where it's called. From the implementation perspective it's still the same obviously.

I don't have a strong opinion here, so feel free to change it.

currentStateFlow(): In my head I still think about kotlin's StateFlow. No need to change, but maybe flowForCurrentState() or statemachinesStateAsFlow() ... Again, no need to change, can be merged as it is.

Updated to flowOfCurrentState 👍

@sockeqwe
Copy link
Collaborator

sockeqwe commented Aug 4, 2021

👍 🚢

@gabrielittner gabrielittner merged commit cb391ce into main Aug 4, 2021
@gabrielittner gabrielittner deleted the collect-state-while-in-state branch August 4, 2021 17:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

Add overloads for even nicer DSL with function reference collectWhileInState based on the current state
2 participants