freeletics / flowredux Goto Github PK
View Code? Open in Web Editor NEWKotlin Multiplatform Statemachine library with nice DSL based on Flow from Kotlin Coroutine's.
Home Page: https://freeletics.github.io/FlowRedux/
License: Apache License 2.0
Kotlin Multiplatform Statemachine library with nice DSL based on Flow from Kotlin Coroutine's.
Home Page: https://freeletics.github.io/FlowRedux/
License: Apache License 2.0
The Problem:
To unit test a function that returns an instance of ChangeState<S>
we need to "unwrap" it to get to the next state value by calling changeState.reduce(currentState)
. Not a big deal, but some repetitive boilerplate if you have to do that all the time for all of your tests. Also ChangeState
is not meant to be compared directly with via equal with a real State
like Assert.assertEquals(expectedState, changeState)
(expectedState is of type State and changeState of type ChangeState).
Maybe we should provide an additional artifact to provide a more convenient way to unit test functions that involve ChangeState
. Could be just a simple extension function like ChangeState.test(currentState) { newState -> ... }
where you can do some assertions inside the lambda block on the newState
object.
Or we could go wilder and define some more convenient API like:
@Test
fun `in loading state then load items and transition to content state`() = runBlocking {
val items: List<Item> = ...
givenState<LoadingState, State>(LoadingState)
.onEnter(::loadFirstPage) // loadFirstPage() is the function that we want to unit test
.then(ShowContentState(notifications = items, currentPage = 1)) // Expected state
}
@gabrielittner @dmo60 do you thing that would be useful?
Hi @sockeqwe,
first of all: great work!
I love FlowRedux and already played a little bit with this library.
But I am facing and problem:
I am dispatching a loading action to the statemachine and this loading actions contains the data for loading feeds (e.g. id for a specific article). i looked at your best practices but i don't know how to get the data out of my action.
my current implementation:
init {
spec {
inState<Loading> {
onEnter(block = ::loadScreen)
}
inState<Content<Any>> {
on<ActivityReloadAction<Ref>> { _, _, setState ->
setState { Loading(refresh = true) }
}
}
inState<Error> {
on<ActivityReloadAction<Ref>> { _, _, setState ->
setState { Loading(refresh = false) }
}
}
}
}
suspend fun loadScreen(getState: GetState<ActivityViewState>, setState: SetState<ActivityViewState>) {
// here i miss the action which contains the data to do the http request
}
Maybe you can tell me how i can get the action which triggered the state or how i can pass in paramters to load the data?
Thx
Bodo
When you build a state machine we ideally want something like this
class MyStateMachine(...) : FlowReduxStateMachine(...) {
init {
spec {
inState<MyState> {
on(::onAction1TransisitionToA)
on(::onAction2DoSomethingElse)
}
}
}
}
suspend fun onAction1TransisitionToA(action: Action1, stateSnapshot: MyState): ChangeState<MyState> {
// do stuff
}
suspend fun onAction2DoSomethingElse(action: Action2, stateSnapshot: MyState): ChangeState<MyState> {
// do stuff
}
Having the handlers as standalone methods lets you easily test them without having to build the full state machine. Method references in the DSL help a lot with keeping the definition itself readable when the state machine gets bigger.
The issue arises when the handler needs some dependency. You either need to
The latter would look like this
class MyStateMachine(
action1TransisitionToA: Action1TransisitionToA,
action2DoSomethingElse: Action2DoSomethingElse,
...
) : FlowReduxStateMachine(...) {
init {
spec {
inState<MyState> {
on(action1TransisitionToA::handle)
on(action2DoSomethingElse::handle)
}
}
}
}
class Action1TransisitionToA(...) {
suspend fun handle(action: Action1, stateSnapshot: MyState): ChangeState<MyState> {
// do stuff
}
}
class Action2DoSomethingElse(...) {
suspend fun handle(action: Action2, stateSnapshot: MyState): ChangeState<MyState> {
// do stuff
}
}
This works with everything that we have however we could improve the experience by turning
typealias OnActionHandler<InputState, S, A> = suspend (action: A, state: InputState) -> ChangeState<S>
into
fun interface OnActionHandler<InputState, S, A> {
suspend fun handle(action: A, state: InputState): ChangeState<S>
}
That way those classes can implement the interface which makes implementing it easier and also prevents any unused_parameter warning on the function. Any existing usage and usecase would still work like before because fun interface
support SAM conversion.
Currently we support collectWhileInState
with flowBuilder
as shown bellow:
fun <T> collectWhileInState(
flowBuilder: (Flow<InputState>) -> Flow<T>,
handler: InStateObserverHandler<T, InputState, S>
)
I would like to add an overload with
fun <T> collectWhileInState(
flowBuilder: (InputState) -> Flow<T>, // just InputState , not Flow<InputState> as above
handler: InStateObserverHandler<T, InputState, S>
)
because I personally have more need for this variation of flowBuilder ( (InputState) -> Flow<T>
) compare to the current one (Flow<InputState>) -> Flow<T>
.
Obviously, that causes a clash on the JVM because both methods have same signature.
Thus we would need to rename one.
I would propose the following:
(InputState) -> Flow<T>
. Thus I would keep that one under the collectWhileInState
name. fun <T> collectWhileInState(
flowBuilder: (InputState) -> Flow<T>, // just InputState , not Flow<InputState> as above
handler: InStateObserverHandler<T, InputState, S>
)
Flow<InputState>) -> Flow<T>
to collectWhileInStateWithGenericBuilder()
:fun <T> collectWhileInStateWithGenericBuilder(
flowBuilder: (Flow<InputState>) -> Flow<T>,
handler: InStateObserverHandler<T, InputState, S>
)
Any thoughts?
Hi, is it possible to call suspend function in on ?
ihave this state machine
inStateWithCondition(isInState = { state -> state.loading }) {
onEnterEffect {
val questionsResult = complianceQuestionsUseCase("co")
Log.e("onEnterEffect", "$questionsResult")
if (questionsResult is UIState.Success) {
OverrideState(
ComplianceState(
showContent = true,
loading = false,
)
)
}
OverrideState(
ComplianceState(
showContent = false,
loading = false,
error = true
)
)
}
onEnter {
val questionsResult = complianceQuestionsUseCase("co")
Log.e("onEnter", "$questionsResult")
if (questionsResult is UIState.Success) {
OverrideState(
ComplianceState(
showContent = true,
loading = false,
)
)
}
OverrideState(
ComplianceState(
showContent = false,
loading = false,
error = true
)
)
}
on<GetQuestionsAction> { action, _ ->
val questionsResult = complianceQuestionsUseCase(action.country)
Log.e("GetQuestionsAction", "$questionsResult")
if (questionsResult is UIState.Success) {
OverrideState(
ComplianceState(
showContent = true,
loading = false,
)
)
}
OverrideState(
ComplianceState(
showContent = false,
loading = false,
error = true
)
)
}
}
in the onEnter and onEnterEffect methods the coroutine is canceled a first time then it enters the methods again and the normal call is made but in the on method the coroutines are always canceled
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@b8540dc
on<GetQuestionsAction> { action, _ ->
val questionsResult = complianceQuestionsUseCase(action.country)
Log.e("GetQuestionsAction", "$questionsResult")
if (questionsResult is UIState.Success) {
OverrideState(
ComplianceState(
showContent = true,
loading = false,
)
)
}
NoStateChange
}
is cancelable but add GlobalScope.Launch is coroutine sucess
GlobalScope.launch {
val questionsResult = complianceQuestionsUseCase(action.country)
Log.e("GetQuestionsAction", "$questionsResult")
if (questionsResult is UIState.Success) {
OverrideState(
ComplianceState(
showContent = true,
loading = false,
)
)
}
}
D/TaskRequestDetailsViewModel$dispatch: FlowRedux -> action : ApproveTask(taskId=TaskReferenceId(id=45019))
D/FlowRedux: Upstream: action ApproveTask(taskId=TaskReferenceId(id=45019)) received
D/FlowRedux: Upstream: reducing Success(taskDetails=TaskDetailsUi(id=TaskReferenceId(id=45019), title=TaskTitle(value=test forward 10), progress=TaskProgress(progress=0.0),
D/FlowRedux: SideEffect3: action SelfReducableAction Caused by on<ApproveTask(taskId=TaskReferenceId(id=45019))> received
D/FlowRedux: SideEffect3: reducing Success(taskDetails=TaskDetailsUi(id=TaskReferenceId(id=45019), title=TaskTitle(value=test forward 10), progress=TaskProgress(progress=0.0
D/TaskRequestDetailsViewModel: FlowRedux -> state : Loading(type=ACTION)
D/FlowRedux: SideEffect3: action SelfReducableAction Caused by on<ApproveTask(taskId=TaskReferenceId(id=45019))> received
D/FlowRedux: SideEffect3: reducing Loading(type=ACTION) with SelfReducableAction Caused by on<ApproveTask(taskId=TaskReferenceId(id=45019))> -> Loading(type=ACTION)
To help to lint and format our codebase we would like to add ktlint to this project.
Hi, do you have any example of flow redux with compose ?
The problem is that parameter with default args doesnt play nice with function references as it requires named arguments.
i.e.
inState<S1> {
onAction<A1> (block = ::handlerMethod) // this explicitly needs to named argument
}
Suggested changes:
inState<S1> {
onAction<A1> (::handlerMethod) // no explicit named argument required
}
Renaming observeWhileInState()
to collectWhileInState
to be more aligned with flow.collect { ... }
Another interesting question would be though if we should do the same with
storewide observe(flow)
. Should it be renamed to collect(flow)
which is could be misleading since flow.collect {...}
is the way to call it. An alternative option is to rename observe(flow)
to collectWhileStateMachineRuns(flow)
or something like that.
We could provide a convinience artefact for jetpack compose that
@Composable
fun MyComponent() {
val (state : State<MyState>, dispatch : (MyAction) -> Unit) by stateMachine(MyFlowReduxStateMachine) // Will do remember { mutableStateOf( ... ) } under the hood and start obsering the StateMachine.state property on the right coroutine scope
}
@Composable
fun MyComponent() {
val (state : State<MyState>, dispatch : (MyAction) -> Unit) by stateMachine {
// statemachine spec DSL
inState<S1> {
onEnter { ... }
onAction { ... }
}
...
}
}
To be more aligned with the DSL naming choice SetState
it makes sense to rename StateAccessor
to GetState
The iOS sample is broken and somehow doesn't sync with the shared multiplatform code anymore.
Some issue with coccoa pods setup I guess.
Maybe someone with more kotlin multi platform experience can help? Maybe @joreilly?
Right now mutlithreaded coroutine dont work on native targets.
Just an idea that I have mixed feelings about, so dropping my idea here to hear opinions.
enum class MyEnum { A, B, C }
data class MyState(val e : MyEnum)
inState<MyState>(additionalCondition = { it.e == MyEnum.B }) { // means state is MyState AND state.e == B
...
}
I am wondering if this could be expressed somehow with DSL constructs.
For example something like adding a new condition
DSL block like this:
inState<MyState> {
condition( {it.e == MyEnum.B} ) {
onEnter {...}
on<Action> {...}
collectWhileInstate { ... }
condition (...){
// even subcondition could be supported
}
}
onEnter {...}
on<Action> {...}
collectWhileInstate { ... }
...
}
That reads for me a bit nicer compared to the current inState<S>(additionalCondition={...})
but obviously has some disadvantages like:
when
functionality alike (could probably be solved with Lint checks or so)Any thoughts?
As a user of FlowRedux I need to specify explicitly the Type argiments of MutateState
. Example:
sealed interface State
data class S1 (val i : Int) : State
object S2 : State
inState<S1> {
onEnter { stateSnapshot ->
...
MutateState<S1, State> { this.copy(i = this.i++) } // MutateState needs S1 as type parameter; compiler cannot infer it.
}
}
This is annoying and counterproductive to our goal of increase developer experience but I'm not sure how to solve this problem best. Some random ideas:
inState
block already know of which type the State we want to mutate is, we could add a extension function to the onEnterBlock
, onAction
and collectWhileInstate
that would allow us to write something like this:inState<S1> {
onEnter { stateSnapshot ->
...
mutateStateExtensionFunction { this.copy(i = this.i++) } // this == S1 can be inferred by the compiler via extensionFuntion
}
}
(Please note that mutateStateExtensionFunction
is just a random name to illustrate the point I try to make; not the final name in case we go for this solution)
While this approach works for blocks directly defined in the DSL it would not work for functional references like this:
inState<S1> {
onEnter(::doSomethingOnEnterS1)
}
fun doSomethingOnEnterS1(stateSnapshot: S1) : ChangeState<State> {
...
return stateSnapshot ->
...
mutateStateExtensionFunction { this.copy(i = this.i++) } // wont work because no such extension function defined for an aribtarry function
}
}
So I was looking if it is possible to define extension functions on "Functions" but didn't figure it out yet how to do it (nor am I sure if the kotlin IDE plugin would work properly). The following did not work (but I didn't dive deeper):
fun <R> kotlin.Function<R>.mutateStateExtensionFunction() {...}
fun <InputState, S> ((stateSnapshot : InputState ) -> S).mutateStateExtensionFunction() {...}
Maybe #189 could introduce a new supertype we could define a mutateStateExtensionFunction
extension function on, but it doesn't look to solve the problem of function reference.
MutateState
with receiver type. Receiver type is handy especially with data classes used for State definition because then you can just call copy()
to do a mutation as this
is introduced by the compiler via reciever type
. However, this introduces the problem that we need to tell in MutateState
what the type of the receiver is causing developers to Explicitly specify the Generic Type Arguments.inState<S1> {
onEnter { stateSnapshot : S1 ->
...
MutateState { currentState : S1 -> currentState .copy(i = currentState.i++) } // Explicity have defined input parameter currentState : S1 is enough for compiler to be satisfied
}
}
this however doesn't fully solve the problem of developer's productivity but could be some compromise.
Do you see any other options? Do I miss something?
I switched from RxRedux to CoRedux and I think another switch seems unnecessary when you can integrate Flow
into CoRedux.
I wrote these SideEffects a while ago supporting Flows, and I haven't found any issue yet, that's why I'm wondering whether yet another project is really necessary.
Thoughts? Sorry if I missed something...
It appears that com.freeletics.flow.test:test:0.0.2-SNAPSHOT
(and a few other test libraries) are included in com.freeletics.flowredux:dsl:0.2.1
as a runtime dependency.
Are there any reasons the following dependencies are not defined in the jvmTest
configuration?
Lines 48 to 52 in fde227e
Since onEnter(FlatMapPolicy)
is exactly triggered once (and doesnt emit later on again) it doesnt need a FlatMapPolicy as a parameter
I've been wondering whether "FlowRedux" should just be the DSL artifact and the reduxStore
is just an internal implementation detail. That would also allow us significantly change the implementation later on (like #209) without having to maintain the reduxStore
method.
Publishing GithubPages and writing docs. In progress.
https://freeletics.github.io/FlowRedux/dsl/
We already have inState<S>(additionalCondition = { ... })
and maybe it would be useful to add additionalCondition
also to on<Action>
.
Use case:
Where the on<Action>
block should actually only be triggered when action.id == someId
Just leaving it here for a potential future addition / improvement.
While my state machine is in a specific state I want to collect a query in my database. However the query has a filter that can change based on actions. I'd like to make that filter part of my state so that actions just change the state and then the query can update automatically. However for that I would need a Flow
of the current state.
I'm currently working around this by having this in my state machine:
private val openingTime = MutableStateFlow(Instant.now())
init {
spec {
inState<MyState> {
collectWhileInState(loadData(), block = ::updateStateWithData)
on(block = ::onRefresh)
}
}
}
private fun loadData(): Flow<Feeds> {
// here is where I'd like to have a `Flow<MyState>` instead
return openingTime.flatMapLatest { repository.load(it) }
}
private fun updateStateWithData(...): ChangeState<MyState> { ... }
private suspend fun onRefresh(
action: Refresh,
state: MyState
): ChangeState<MyState> {
// this should mutate MyState instead of updating an external Flow
openingTime.emit(Instant.now())
return NoStateChange
}
All of that feels very wrong with how the DSL based state machines work. Additionally if I need that Instant
in my state for other purposes, like showing it in the UI, there would be 2 sources of truth for it and a chance of inconsistencies. For example if there'd be a second action that updates it I could forget to call openingTime.emit
and then the UI would something different than what is being queried.
Maybe collectWhileInState
should take a lambda that returns the Flow
instead of directly receiving that Flow
and we pass a Flow
of the state to that lambda. So something like
fun <T> collectWhileInState(
flow: (Flow<InputState>) -> Flow<T>,
flatMapPolicy: FlatMapPolicy = FlatMapPolicy.CONCAT,
block: InStateObserverBlock<T, InputState, S>
)
The lambda would also fit the style of using method references inside spec
and maybe discourage a bit to build the Flow
directly when calling collectWhileInState
instead of in an extra function.
Note: With the change from #150 it would technically be possible to observe the main state
of the FlowReduxStateMachine
since it is visible to us and always collectable, but that feels also wrong. Especially in an inState
block because state
has all states and not just the filtered one.
Setup CI so that mkdocs build github page on ever push on main branch
For FlowReduxStateMachine
we should use StateFlow
instead of Flow so that the StateMachine can work independent from it's state subscribers (like UI can subscribe and unsubscribe according to UI's lifecycle.
Furthemore, the statemachine should keep alive independent from subscribers lifecycle. Therefore a CoroutineScope should be passed as constructor parameter. That CoroutineScope will then be used to keep the whole statemachine alive until that scope gets canceled. On canceling this statemachine firther calls of dispatch()
or state.collect()
must throw an IllegalStateException
.
Kotlin 1.5.30 added native support for Apple Silicon.
To enable this in a KMP project (app) all transitive dependencies need to add those new targets.
Upcoming kotlinx.coroutines 1.5.2 will be adding these:
addTarget(presets.iosSimulatorArm64)
addTarget(presets.watchosSimulatorArm64)
addTarget(presets.tvosSimulatorArm64)
addTarget(presets.macosArm64)
It'd be great to add these targets to FlowRedux
too.
Happy to send a PR once Coroutines 1.5.2 is out :)
With Version 0.8.0 there's an InvalidMutabilityException
when initializing a FlowReduxStateMachine
on iOS:
Uncaught Kotlin exception: kotlin.native.concurrent.InvalidMutabilityException: mutation attempt of frozen MyStateMachine@106e9c8
Looks like it's introduced in this change:
Currently when using method references you that the Kotlin compiler will flag your parameters as unused when you just add them to be to use method references. The most common case is probably an unused stateSnapshot
, but for the action handlers it could even be the action itself if it's just an object
. If you don't use method references this issue still exists. While you can just use _
to get rid of the warning you still of the verbosity of having to write out the lambda parameters (on<SomeAction> { action, _ ->
).
Side note that influences potential solutions: I'm getting more and more convinced that we should not use method references anymore and rely on lambdas. The general aspiration of having the spec
block as small as possible is still valid, however I think method references are not the right solution. There are multiple issues and while some of them are not our fault and could be fixed by Kotlin they still matter:
::...
on(::buttonClicked)
, my assumption is that if you have on<ButtonClicked> { ... }
you are less inclined to call the method the sameon(::transitionToSomeState)
you loose the information of when it happens while on<ButtonClicked> { transitionToSomeState(it) }
would make getting an overview easier. Of course you can combine both in your function name but that leads to long/weird names. For example onButtonClickedTransitionToSomeState
is long and would then result in on(::on...)
while leaving out the on
from the method name reads weirdly when looking at just the functionGoing back to the original problem depending on whether we want to encourage method references or not all handlers would need overloads. If we are saying method references are the way to go then we'd need additional interfaces + overloads with 0 and 1 parameters for everything that has 1+ parameters. If we decide for lambdas then it would be interfaces + overloads for everything with 2+ parameters because a single parameter is just an implicit it
either way.
SelfReducableAction
should be renamed to SetStateAction
. SetStateAction
should not be exposed publicly though, at the moment it is exposed via FlowReduxLogger
as
reducing
SelfReducableAction
This should be cleaned up as well a be only visible as setState { ... }
in the logs.
Relates to #87
Hi, I made the changes as you advised me, but I have a problem when I make an http call, I'm getting a courutine is canceled error, I share my code with you
state machine
spec {
inState<LoadingState> {
on<GetQuestionsAction> { action, _ ->
Log.e(
"ComplianceMachineState",
"LoadingState GetQuestionsAction: ${action.country}"
)
OverrideState(GetQuestionsAnswerState(action.country))
}
}
inState<GetQuestionsAnswerState> {
onEnter { state ->
Log.e(
"ComplianceMachineState",
"GetQuestionsAnswerState"
)
try {
val result = complianceQuestionsUseCase(state.country) //suspend function call return error kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@b74dc27
if (result is UIState.UnExpectedError) {
OverrideState(GetQuestionsAnswerErrorState(state.country))
} else {
OverrideState(AnswerShowContentState())
}
} catch (e: Exception) {
OverrideState(GetQuestionsAnswerErrorState(state.country))
}
}
}
inState<AnswerShowContentState> {
onEnter {
Log.e("ComplianceMachineState", "AnswerShowContentState")
NoStateChange
}
}
inState<GetQuestionsAnswerErrorState> {
onEnter { state ->
Log.e(
"ComplianceMachineState",
"GetQuestionsAnswerErrorState onEnter: ${state.country}"
)
OverrideState(GetQuestionsAnswerState(state.country))
}
}
}
prints logs, no enter in GetQuestionsAnswerErrorState
2022-05-02 13:36:09.593 3080 E LoadingState GetQuestionsAction: co
2022-05-02 13:36:13.471 3080 E GetQuestionsAnswerState
my composable
@Composable
fun TestFlowReduxStateMachine(
stateMachine: ComplianceMachineState,
) {
val (state, dispatchAction) = stateMachine.rememberStateAndDispatch()
LaunchedEffect(Unit) {
dispatchAction(GetQuestionsAction("CO"))
}
}
onEnter coroutine call complianceQuestionsUseCase is cancelled
Originally posted by @Jparrgam in #294 (comment)
I'm trying to use 0.0.3 of flowredux in a multiplatform project, and getting the following error while trying to target iOS:
The abi versions don't match. Expected '[22]', found '17'. The library produced by 1.3.61-release-13550 compiler
I assume this is an issue with the version of xcode under which things were compiled.
Not really sure how to proceed
At the beginning FlowRedux the FlowReduxStatemachine
was a "cold flow" (meaning on every new state.collect()
the statemachine started with the initial state again).
Starting in FlowRedux 0.6.0 we then have made FlowReduxStatemachine
a "hot flow" and have started using StateFlow
(hot means the same state machine state will be collected / shared by all collectors; so it doesn't start with the initial state for every new state.collect {}
; plus statemachine can run without any active Collector
as long as the CoroutineScope where the state machine launched in is active).
We thought this could give us some advantages with Freeletic's DI setup and Jetpack Compose, however it turns out the drawback's are bigger than expected. Just a few pain points:
state.collect {...}
. That leads to some state transitions being missed in unit test because the collector used in the test was not subscribed yet.Therefore we are going to revert this. We will move away from StateFlow
and make FlowReduxStateMachine "cold" again .
Since Flow and underlying flatMaps are mainly hidden from the consumer of this library it makes most likely sense to rename FlatMapPolicy
to something more specific the consumer can understand better.
My Proposal would be to rename it to ExecutionPolicy
:
FlatMapPolicy.LATEST
--> ExecutionPolicy.CANCEL_PREVIOUS_EXECUTION
FlatMapPolicy.MERGE
--> ExecutionPolicy.UNORDERED
FlatMapPolicy.CONCAT
--> ExceutionPolicy.ORDERED
What do you think?
As an example let's say we have this state
sealed class MyState
class StateA : MyState()
class StateB : MyState()
Now in a state machine for MyState
you can do
init {
spec {
collectWhileInAnyState(...) { ... }
inState<MyState> {
collectWhileInState(...) { ... }
on<...>() { ... }
}
}
}
The 2 collectWhile...
calls have the same lifetime because we are always in MyState
. The inconsistency in my opinion is that there are effectively 2 ways to do collectWhileInAnyState
but it's only possible to define an on
that should happen in any state within inState
. Initially I was searching for an onInAnyState
because I knew collectWhileInAnyState
exists.
Instead of having collectWhileInAnyState
could we just remove it and recommend using collectWhileInState
with the base class? Then it's consistent with on
and there is just one way to do things.
There are in some places still println()
that has been introduced while developing FlowRedux and have been forgotten to remove
api could looks something like this:
class Myflowreduxstatemachine<MyState MyAction>) {
...
spec {
subStateMachine(subStateMachine: FlowReduxStateMachine sm1, actionsMapper : (MyAction) -> SubStateMachineAction, stateMapper : (MySubState, RootState) -> RootState
}
If #130 is implemented, should wr check that all sub statemachines run on the same CoroutineScope as the Parent StateMachine? @gabrielittner do you have any thoughts on that?
I'm seeing a crash when doing the following, but couldn't wrap my head around what's causing it:
collectWhileInAnyState(UserStore.stream()) { userState, getState, setState ->
setState {
when (userState) {
UserState.LoggedOut -> LoadModel.LoggedOut
is UserState.LoggedIn -> LoadModel.LoggedIn(userState.user.name)
}
}
}
Crash:
java.lang.IllegalStateException: ReceiveChannel.consumeAsFlow can be collected just once
at kotlinx.coroutines.flow.ConsumeAsFlow.markConsumed(Channels.kt:100)
at kotlinx.coroutines.flow.ConsumeAsFlow.collect(Channels.kt:123)
at com.freeletics.flowredux.dsl.DslKt$reduxStore$$inlined$map$1.collect(SafeCollector.kt:127)
at kotlinx.coroutines.flow.FlowKt__EmittersKt$onStart$$inlined$unsafeFlow$1.collect(SafeCollector.kt:127)
at kotlinx.coroutines.flow.internal.ChannelFlowOperatorImpl.flowCollect(ChannelFlow.kt:135)
at kotlinx.coroutines.flow.internal.ChannelFlowOperator.collectTo$suspendImpl(ChannelFlow.kt:102)
at kotlinx.coroutines.flow.internal.ChannelFlowOperator.collectTo(Unknown Source:0)
at kotlinx.coroutines.flow.internal.ChannelFlow$collectToFun$1.invokeSuspend(ChannelFlow.kt:62)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
at kotlinx.coroutines.EventLoop.processUnconfinedEvent(EventLoop.common.kt:68)
at kotlinx.coroutines.DispatchedContinuation.resumeWith(DispatchedContinuation.kt:298)
at kotlin.coroutines.ContinuationKt.startCoroutine(Continuation.kt:128)
at kotlinx.coroutines.channels.AbstractChannel$ReceiveSelect.completeResumeReceive(AbstractChannel.kt:942)
at kotlinx.coroutines.channels.ArrayChannel.offerInternal(ArrayChannel.kt:79)
at kotlinx.coroutines.channels.AbstractSendChannel.send(AbstractChannel.kt:147)
at kotlinx.coroutines.channels.ChannelCoroutine.send$suspendImpl(Unknown Source:2)
at kotlinx.coroutines.channels.ChannelCoroutine.send(Unknown Source:0)
at kotlinx.coroutines.flow.internal.SendingCollector.emit(SendingCollector.kt:19)
at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:33)
at com.freeletics.flowredux.dsl.StoreWideCollectBuilderBlock$setStateFlow$2$setState$2.invokeSuspend(StoreWideCollectBuilderBlock.kt:43)
at com.freeletics.flowredux.dsl.StoreWideCollectBuilderBlock$setStateFlow$2$setState$2.invoke(Unknown Source:14)
at com.freeletics.flowredux.dsl.SetStateImpl.invoke(SetState.kt:51)
at com.freeletics.flowredux.dsl.SetState.invoke$default(SetState.kt:34)
refer to this issue that discussed the introduction of consumeAsFlow
At the moment a FlowReduxLogger
is passed in as a constructor parameter and it logs things.
The Api are awful, nor is it clear what the logger is good for and what it o is suppose to log (i.e. internal ro debug state machine? For FlowRedux maintainers to create bug reports? For consumer of FlowRedux library)?
At the moment we don't cancel any ongoing handler of a on<Action>
if the surrounding inState<State>
condition doesn't hold anymore (so statemachine transitioned to another state).
This was actually the expected behavior before FlowRedux 0.5.0, but now should change.
At the moment you cannot do a piece of work without the need of returning a ChangeState
object.
This proposal is about adding "effects". A typical use case for an Effect is triggering navigation without changing the state of the statemachine itself or doing some analytics tracking calls.
Proposed Syntax
inState<MyState> {
onEnterEffect { stateSnapshot : MyState ->
// Do something.
Unit // return unit, no state change
}
onActionEffect<MyAction> action : MyAction, stateSnapshot : MyState ->
// Do something.
Unit // return unit, no state change
}
collectWhileInStateEffect(flowOf(1,2)) { value : Int,
stateSnapshot : MyState ->
// Do something.
Unit // return unit, no state change
}
}
Specs:
onEnterEffect
is suspend (State) -> Unit
withContext(NonCancelable)
Disclaimer: This is not about changing anything in the DSL per se, this is about rewriting some internals of FlowRedux.
At the moment FlowRedux internally uses Flow
s as some sort of broadcast channel to publish state changes to the outside but also to the inside (like DSL specific blocks that then trigger) and dispatch actions.
Especially blocks of the DSL (i.e. inState<State> {...}
) where cancellation of ongoing background work is required and need to be managed is complex with pure Flow
s.
@gabrielittner came up with un interesting idea to basically use Jetpack Compose (compiler + runtime) because it is build around State and changes over time on State and has concepts of starting and canceling background work when state changes. That seems to be what we need in FlowRedux and with Compose and @Composable
we could probably achieve the same functionality as we have today with our Flow
based implementation but with lower complexity (mental overhead) for maintainers of the FlowRedux library itself.
In my head, this is something that we can tackle after FlowRedux 1.0 has been released as the goal is to refacor internals (so private APIs) and not the public API of the DSL etc.
The following use case is not really nicely handable by FlowRedux at the moment:
Let's say we have a list of Items that we want to display on screen. So the overall screen state looks something like this:
sealed interface ScreenState
data class ShowListScreenState(val items : List<Item>) : ScreenState
object LoadingState : ScreenState
What if in the UI each Item
that is displayed in the list of Items also has a button. An by pressing that button a http request is done to update something to the backend for this particular item (i.e. mark a item as "Liked" on a facebook alike newsfeed post)?
In other words, each Item
has their own state.
data class Item (val id : Int, val state : ItemState)
enum class ItemState {
IDLE,
LOADING
ERROR,
SUCCESSFUL
}
data class ItemButtonClickedAction (val itemId : Int) : Action
What I would like to propose and Brainstorm here is to take usage of the substate machines concept we already have.
spec {
inState<LoadingState> {
onEnter {
val items = loadItemsFromServer()
OverrideState(ShowListScreenState (items))
}
inState<ShowListScreenState> {
// NEW DSL primitive
onActionStartSubstateMachine<ItemButtonClickedAction>(
stateMachineFactory = { action: ItemButtonClickedAction, state : ShowListScreenState ->
ItemButtonStateMachine( action.itemId)
},
actionMapper = { it }, // Forward future incoming actions to the substate machine (not needed in this example)
additionalStopCondition= { state : ShowListScreenState, stateFromItemButtonStateMachine : ItemState ->
// Invoked after stateMapper ran
stateFromItemButtonStateMachine != ItemState.LOADING
},
stateMapper = { state : ShowListScreenState , stateFromItemButtonStateMachine : ItemState,
MutateState<ShowListScreenState, ScreenState> {
copy(items = items.replaceItem(Item(originatedAction.id, stateFromItemButtonStateMachine)))
}
}
}
class ItemButtonStateMachine (val itemId : Int) : FlowReduxStateMachine<Action, ItemState> {
init {
spec {
inState<LOADING>
onEnter {
val succesful : Boolean = makeHttpRequest(itemId)
OverrideState (
if (successful) SUCCESSFUL
else ERROR)
}
}
}
}
}
So basically you can start a "substatemachine" from an action. We have already known concepts (from already existing statemachine()
DSL construct) like stateMachineFactory
but alsostateMapper()
and actionMapper()
The remaining question for me is when does a sub statemachine stops. As always it should stop when the surounding inState<...>
condition (of parent statemachine) doesn't hold anymore but we may additionally need some more fine grained control. This is why I would propose to introduce additionalStopConidtion: (ParentStateMachineState, SubStateMachineState) -> Boolean
. additionalStopConidtion
is invoked after a state change of either parentStateMachine or subStateMachine (so it is called after reducer or stateMapper
did run).
If additionalStopCondition
returns true the child statemachine will be stopped (collection of state flow of child statemachine is cancelled; it is not possible to resume child state machine).
as usually ExecutionPolicy
can be applied (thus ExecutionPolicy.CANCEL_PREVIOUS
can cancel substatemachine as well).
the same concept of starting substatemachine can also be applied to collectWhileInState
and onEnter
. in fact current stateMachine()
call is actually already onEnterStartSubStatemachine()
.
instead of onActionStartSubstateMachine()
and the like we could also go a different route of introducing additional primitives for inState
to make define state changes even mor fine granular detectable and triggerable.
maybe something like (pseudocode / non functional DSL)
inState<ShowListScreenState> { state –>
// collectWhileInState () + on<Action> are available of course
for (item in state.items){
inState(condition = { item –> item.state == ItemState.LOADING }) {
onEnter { item –>
val succesful : Boolean = makeHttpRequest(item.id)
OverrideState (
if (successful) SUCCESSFUL
else ERROR
}
}
}
but that is an entirely shift of DSL as we use it right now. right now DSL is basically a one-time builder that executes one time at init but what I am proposing above is more like dynamic execution of the DSL over and over again on state changes like what Jetpack Compose does, so relates to #209.
I think for a FlowRedux 1.0 release going with first option (onActionStartSubstateMachine()
) should be sufficient but I totally can see how jetpack compose (greetings to square molecule but also would be great to have kmp support, native jetbrains compose is at least to me cumbersome to work with at the moment) would allow us more flexibility and a sharp powerful tool to build even better state machines. I'm just not sure right now how much of this flexibility and dynamic-ism we need to guide users of FlowRedux in the dirction we want, powerful enough, but also specific and expressive enough to not have options to do things in multiple ways and shoot yourself in the foot. Also current stateamachine()
(to start substatemachine) seems to work good for us, so if we only need one mor way to start sub statemachines on Actions, maybe that is actually all we need at least for now / FlowRedux 1.0.
What do you think?
This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.
This repository currently has no open or pending branches.
.github/workflows/build.yml
actions/checkout v4
actions/setup-java v4
gradle/actions v4
.github/workflows/ios-build.yml
actions/checkout v4
actions/setup-java v4
maxim-lobanov/setup-xcode v1.6.0
actions/cache v4
actions/cache v4
sersoft-gmbh/xcodebuild-action v3.2.0
macos 14
.github/workflows/publish-docs.yml
actions/checkout v4
actions/setup-java v4
actions/setup-python v5
actions/upload-pages-artifact v3
actions/deploy-pages v4
.github/workflows/publish-release.yml
actions/checkout v4
actions/setup-java v4
gradle/actions v4
softprops/action-gh-release v2
.github/workflows/publish-snapshot.yml
actions/checkout v4
actions/setup-java v4
gradle/actions v4
gradle.properties
settings.gradle.kts
com.freeletics.gradle.settings 0.15.0
build.gradle.kts
compose/gradle.properties
compose/compose.gradle.kts
flowredux/gradle.properties
flowredux/flowredux.gradle.kts
gradle/libs.versions.toml
org.jetbrains.kotlin:kotlin-test 2.0.10
org.jetbrains.kotlin:kotlin-test-annotations-common 2.0.10
org.jetbrains.kotlin:kotlin-test-junit 2.0.10
org.jetbrains.kotlinx:kotlinx-coroutines-core 1.8.1
org.jetbrains.kotlinx:kotlinx-coroutines-test 1.8.1
androidx.compose.runtime:runtime 1.6.8
androidx.compose.ui:ui 1.6.8
androidx.compose.foundation:foundation 1.6.8
androidx.compose.material:material 1.6.8
androidx.compose.material3:material3 1.2.1
org.jetbrains.compose.runtime:runtime 1.6.11
androidx.activity:activity 1.9.1
androidx.annotation:annotation 1.8.2
androidx.activity:activity-compose 1.9.1
androidx.appcompat:appcompat 1.7.0
androidx.constraintlayout:constraintlayout 2.1.4
androidx.core:core-ktx 1.13.1
androidx.lifecycle:lifecycle-common 2.8.4
androidx.lifecycle:lifecycle-livedata-core 2.8.4
androidx.lifecycle:lifecycle-viewmodel 2.8.4
androidx.recyclerview:recyclerview 1.3.2
com.google.android.material:material 1.12.0
com.hannesdorfmann:adapterdelegates4 4.3.2
com.hannesdorfmann:adapterdelegates4-kotlin-dsl 4.3.2
com.jakewharton.timber:timber 5.0.1
com.freeletics.mad:state-machine 0.24.0
app.cash.turbine:turbine 1.1.0
org.jetbrains.kotlinx:kotlinx-collections-immutable 0.3.7
com.freeletics.gradle.android 0.15.0
com.freeletics.gradle.android.app 0.15.0
com.freeletics.gradle.jvm 0.15.0
com.freeletics.gradle.multiplatform 0.15.0
com.freeletics.gradle.publish.oss 0.15.0
com.freeletics.gradle.root 0.15.0
com.freeletics.gradle.settings 0.15.0
com.android.application 8.5.2
com.android.library 8.5.2
org.jetbrains.kotlin.multiplatform 2.0.10
org.jetbrains.kotlin.android 2.0.10
org.jetbrains.kotlin.plugin.compose 2.0.10
com.vanniktech.maven.publish 0.29.0
org.jetbrains.dokka 1.9.20
org.jetbrains.kotlinx.binary-compatibility-validator 0.16.3
com.autonomousapps.dependency-analysis 1.33.0
sample/android/sample-android.gradle.kts
sample/shared_code/sample-shared_code.gradle.kts
gradle/wrapper/gradle-wrapper.properties
gradle 8.10
.kts/ktlint.main.kts
com.freeletics.gradle:scripts-formatting 0.15.0
Accessing the stateSnapshot parameter inside a MutateState block should be forbidden by a lint Rule.
fun onActionXDoFoo(action: ActionX, stateSnapshot: State) {
....
return MutateState {
stateSnapshot.copy( ... ) // Accessing must be forbidden because lambda parameter state must be used instead
}
}
We should restructure our docs to emphasize even more the DSL so that user of this library have a better guide to learn about it. I think a step by step guide (similar to Redux docs) could be useful. For that I would propose to break the big dsl.md
file into multiple smaller .md files so that mkdocs also generates "section" entries in the main menu...
The sample is old and has a lot of unused dependencies like ktor or screenshot testing. We don't need that. Let's remove it as it just causes noise and confusion.
To ensure that iOS example doesn't break (again) we should build iOS sample app on CI for each PR etc. Too
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.