Comments (5)
TL;DR: Assuming that we have the same understanding of the definition of Hiearchical State Machines
and Parallel State Machines
then yes, "composable" refers to supporting Hierarchical & Parallel state machines
(read: can be hierarchical or parallel or both, totally up to the developer).
1. My definition of Hierchical State Machine:
Let's say we want to show a list of Items but with Pagination support. Then you could have 1 "root" state machine with states loading first page state
, error loading first page state
, and show items state
, and then you could have a "sub state machine that manages while being in show items state
the loading of the next page with substates loading next page state
, error loading next page state
and show items with latest loaded page state
.
This would translate to something like that
class RooStateMachine<State, Action>(nextPageStateMachine : NextPageStateMachine) : FlowReduxStateMachine(initialState = LoadingState) {
inState<LoadingFirstPageState> {
onEnter {
try {
val items = loadFirstPageItems()
OverrideState( ShowItemsState(items) )
} catch (t : Throwable) {
OverrideState( ErrorFirstPageState(t) )
}
}
inState<ErrorFirstPageState> {
on<RetryLoadingAction> {
OverrideState( LoadingFirstPageState() )
}
}
// Now instead of having an inState<ShowItemsState> block we rather hierarchically compose it by splitting this functionality in a "subStateMachine"
subStateMachine(nextPageStateMachine) // register "subStateMachine". It will get all the "SubActions" and emits back "substate" to RooStateMachine
}
// data ShowItemState(val items : List<Item>, nextPageLoadingState : NextPageLoadingState) with enum class NextPageLoadingState { IDLE, LOADING, ERROR }
class NextPageStateMachine<ShowItemsState, SubActions> : FlowReduxStateMachine(initialState =ShowItemState( ){
inState <ShowItemsState>(condition = { state : ShowItemsState -> state.nextPageLoadingState == NextPageLoadingState.IDLE}) {
on<LoadNextPageAction> {
MutateState { copy( nextPageLoadingState =NextPageLoadingState. LOADING )}
}
}
inState <ShowItemsState>(condition = { state : ShowItemsState -> state.nextPageLoadingState == LOADING}) {
try {
val nextItems = loadNextPageItems()
MutateState { copy(items = items + nextItems, nextPageLoadingState = IDLE) }
catch (t : Throwable) {
MutateState { copy( nextPageLoadingState = IDLE) }
}
}
inState <ShowItemsState>(condition = { state : ShowItemsState -> state.nextPageLoadingState == NextPageLoadingState.ERROR}) {
delay(3000) // wait 3 seconds, this translates show an error message in UI for 3 seconds
MutateState { copy (nextPageLoadingState = IDLE) }
}
}
2. For parallel state Machines, imho this is just some special case of hierarchical state machines just with indipendent branches.
Let's say we would like to have some sort of screen that has 2 features in one screen: 1. upload Files and 2. download Files
Then you can split this 2 features into parallel state machines like that:
data class ScreenState (val uploadState : UploadState, val downloadState : DownloadState)
class RootStateMachine <ScreenState, Actions>(uploadStateMachine: UploadStateMachine, downloadStateMachine : DowloadStateMachine) : FlowReduxStateMachine {
spec {
subStateMachine(uploadStateMachine, stateMapper = { screenState, uploadState -> screenState.copy( uploadState = uploadState ) })
subStateMachine(downloadStateMachine, stateMapper = { screenState, downloadState -> screenState.copy( downloadState = downloadState ) })
// You could have some more inState{ ... } blocks here below if you want to ...
}
}
I was actually wondering what a better name for subStateMachine()
could be to make it more explicit. do you think we should stick to the academic terminology of Hierachical and Parallel State Machines?
from flowredux.
We are on the same page on the definition of Hierarchical & Parallel state machines. I'm referring to XState.
subStateMachine
- may not be appropirate for defining parallel state machines. Perhaps we could just use the same or generic name like stateMachine()
since the state machine is going to be composbale.
You can refer to W3C SCXML specification for state machines.
from flowredux.
Proposal Number 1: make composition of substatemachines part of inState {...}
Idea
the idea is that you "register" a "sub-statemachine" with:
class ParentStateMachine(childStateMachine : FlowReduxStateMachine) : FlowReduxStateMachine {
...
inState<FooState> {
stateMachine(
childStateMachineFactory: (FooState) -> FlowReduxStateMachine<ChildState, ChildAction>(), // Create Child StateMachine with Initial State
actionMapper : (ParentStateMachineAction) -> ChildStateMachineAction,
stateMapper: (FooState, ChildState) -> ChangeState<ParentState>
)
}
}
Terminology
Please note that the wording ParentStateMachine
and ChildStateMachine
is a bit misleading. ChildStateMachine
does not need to have Actions and State that are in the same inherence hierarchy (sealed class hierarchy) as the State and Action of the ParentStateMachine
. There are actionMapper
and stateMapper
to "bridge" or "convert" actions and states back and forth. So no common base class or something like that for
Need for this feature? Example use case
Composing StateMachines is a powerful concept in my opinion and mainly addresses 2 issues:
- A StateMachine (like any other class, think of it as a "GodStateMachine" in analogy to "GodActivity" 😄 ) could become too big and hard to test and maintain if it just contains too much code. Therefore, splitting functionality into smaller state machines and compose them together is a valid strategy.
- Reusability: With composition (favor composition over inheritance) we can build reusable smaller state machines that then can be used to reuse functionality.
Example
A concrete example is combining
- a traditional screen that loads items and either displays an error or the loaded items on screen (called LceStates in example bellow)
- with pagination
Of course you can do all in one state machine but making it reusable and composable by splitting it in 2 state machines it could look as following:
sealed interface LceState
object LoadingState : LceState
object ErrorState : LceState
data class ContentState(
val currentPage : Int,
val items : List<Item>,
val paginationState : PaginationState
) : LceState
enum class PaginationState {
LOADING_NEXT_PAGE,
ERROR_NEXT_PAGE,
IDLE
}
sealed interface Action
object RetryLoadingAction : Action
object LoadNextPageAction : Action
class LceStateMachine(
httpClient : HttpClient
) : FlowReduxStateMachine<LceState, Action>(LoadingState){
init {
spec {
inState<LoadingState> {
onEnter {
try {
val items = httpClient.loadFirstPage()
OverrideState( ContentState(1, items, PaginationState.IDLE) )
} catch (t : Throwable) {
OverrideState( ErrorState )
}
}
}
inState<ErrorState> {
on<RetryLoadingAction> { _ , _ -> OverrideState( LoadingState ) }
}
inState<ContentState> {
stateMachine(
childStateMachineFactory = { contentState -> PaginationStateMachine(initialState = contentState, httpClient = httpClient) },
actionMapper = { it },
stateMapper = { contentState, newPaginationState ->
OverrideState( newPaginationState )
} )
}
}
}
}
class PaginationStateMachine<ContentState, Action>(initialState : ContentState, httpClient : HttpClient) : FlowReduxStateMachine(initialState) {
init {
spec {
inStateWithCondition({ it.paginationState == LOADING_NEXT_PAGE) {
onEnter { stateSnapshot : ContentState ->
try {
val nextPage = stateSnapshot.currentPage + 1
val nextItems = httpClient.loadNextPage(nextPage)
MutateState { copy( currentPage = nextPage, items = this.items + nextItems, paginationState = IDLE) }
} catch (t : Throwable) {
MutateState { copy( paginationState = ERROR_NEXT_PAGE ) }
}
}
}
inStateWithCondition({ it.paginationState == ERROR_NEXT_PAGE) {
onEnter {
delay(3000) // display error message in UI for 3 seconds
MutateState { copy( paginationState = IDLE ) }
}
on<LoadNextPageAction> { _ , _ -> MutateState { copy( paginationState = LOADING_NEXT_PAGE ) }
}
inStateWithCondition({ it.paginationState == LOADING_NEXT_PAGE) {
on<LoadNextPageAction> { _ , _ -> MutateState { copy( paginationState = LOADING_NEXT_PAGE ) }
}
}
}
}
Design Cosiderations
Additionally, I thought about following the inState<State>
semantics that we already have. Thus stateMachine(...)
is part of the inState { ... }
block and act accordingly by only collecting the state of ChildStateMachine
if the surrounding inState
block condition is met. Same applies for dispatching actions to ChildStateMachine
(will only be done if condition of the surrounding inState block is met).
Some more thoughts and design problems that I have seen while working on this proof of concept:
-
stateMapper
will only be invoked if theChildStatemachine
emits a new state. That meansstateMapper
will not be invoked when the parent's StateMachine changes state. So this is not some sort ofcombineLatest()
operator. That to me seems alright because the overall idea is in my head is that one property of the ParentState is "computed" or "produced" by the state of the child state. Example:data class ParentState( val property1 : String val property2 : String val property3 : String ) data class ChildState(val i : Int)
then the mapper function could look something like this:
fun mapStateOfChild(parentState : ParentState, childState : ChildState) : ParentState { // simplification: in real world the return type is ChangeState<S> // this mapperFunction is only called when childState has changed, but not every time parentState changes return parentState.copy(property2 = "computed state from childstatemachine with value = ${childState.i}" }
-
Since
stateMachine(childStateMachineFactory , ...)
is surrounded by aninState {...}
block I believe we need a factory that would instantiate a fresh instance ofChildStateMachine
whenever we enter theinState
block and release the oldChildStateMachine
instance whenever we leave theinState
block. The issue I see is that the initial state of child state machine could come from the parent (not in all cases it is the case that childstatemachine's initial state depends on parent state machine, but see Pagination example).- But how would that work with dependency injection?
- What about state flow? I guess here is a good argument to make FlowReduxStateMachines "cold" again and remove StateFlow.
Any thoughts, feedback or comments are very much appreciated
from flowredux.
🎉 done.
from flowredux.
Does "composable" refer to defining Hierarchical & Parallel state machines?
from flowredux.
Related Issues (20)
- Using FlowRedux in Jetpack Compose with MVVM pattern HOT 5
- Make Compose artifact Compose Multiplatform HOT 1
- Cannot dispatch action error when testing with turbine HOT 2
- Cannot dispatch action because state Flow of this FlowReduxStateMachine is not collected yet HOT 2
- ChangedState reduce() type mismatch HOT 3
- [Question] Detail page implementation help HOT 6
- DSL: Add a way to work with"Identity" of states HOT 3
- 2.0 todo list
- Action Required: Fix Renovate Configuration
- FlowRedux and Flow from usecases HOT 1
- Support null values for identity? HOT 1
- Improve hierarchical state machines
- [Question] Search Implementation HOT 3
- [Idea] Generate a state transition graph in compile time HOT 2
- [Question] How to dispatch actions in one coroutine? HOT 1
- [Question] Dead lock in dispatching multiple actions to ORDERED OnAction statement HOT 4
- Cannot dispatch action because state Flow of this FlowReduxStateMachine is not collected yet HOT 7
- 1.2.0 seems to be broken regarding iOS HOT 2
- Unify state (snapshot) access and parameters in DSL
- [Question] State Collection On Multiple Screens HOT 1
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from flowredux.