-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
93543cf
commit 1b40d74
Showing
27 changed files
with
1,597 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,7 +23,7 @@ kotlin { | |
} | ||
|
||
linuxX64() | ||
// TODO linuxArm64() | ||
linuxArm64() | ||
|
||
iosArm64() | ||
iosX64() | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
# Extensions for Jetpack Compose | ||
|
||
This package provides some functions that may be useful if you use Jetpack Compose. | ||
|
||
|
||
### `rememberStateAndDispatch()` | ||
|
||
Let's say we have a very basic address book UI build with Jetpack Compose | ||
and with a FlowRedux powered state machine. | ||
|
||
Let's take a look at this over-simplified code sample: | ||
|
||
```kotlin | ||
val stateMachine = AddressBookStateMachine() | ||
|
||
@Compose | ||
fun AddressBookUi(){ | ||
// Extension function that is provided by this artifact | ||
val (state, dispatchAction) = stateMachine.rememberStateAndDispatch() | ||
Column { | ||
SearchBoxUi(state.searchQuery, dispatch) | ||
} | ||
|
||
LazyColumn { | ||
items(state.contacts) { contact : Contact -> | ||
ContactUi(contact, dispatchAction) | ||
} | ||
} | ||
} | ||
|
||
@Compose | ||
fun SearchBoxUi(searchQuery : String, dispatchAction: (AddressBookAction) -> Unit) { | ||
Column { | ||
TextField( | ||
value = searchQuery, | ||
// dispatches action async to state machine | ||
onValueChange = { text -> dispatchAction(SearchQueryChangedAction(text)) } | ||
) | ||
} | ||
} | ||
``` | ||
|
||
`rememberStateAndDispatch()`, as the name already suggests, is remembered across recompositions. | ||
|
||
### `rememberState()` | ||
|
||
If you only need state of from your stateMachine but not an async way to dispatch actions | ||
then `rememberState()` extension is what you are looking for. | ||
|
||
```kotlin | ||
import androidx.compose.runtime.State | ||
|
||
val stateMachine = AddressBookStateMachine() | ||
|
||
@Compose | ||
fun MyUi(){ | ||
val state : State<AddressBookState> = stateMachine.rememberState() // this returns Compose State | ||
LazyColumn { | ||
items(state.contacts) { contact : Contact -> | ||
ContactUi(contact) | ||
} | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# Contributing | ||
|
||
Contribution is more than welcome! | ||
If you would like to contribute code to FlowRedux you can do so through GitHub by forking the repository and sending a pull request. | ||
|
||
## Ktlint | ||
The project uses [Ktlint](https://pinterest.github.io/ktlint) to make the code base consistent. | ||
You can run the formatter by using these commands: | ||
|
||
```shell | ||
# This runs the lint formatter | ||
./kotlinw .kts/ktlint.main.kts | ||
|
||
# This runs the lint checker | ||
./kotlinw .kts/ktlint.main.kts --fail-on-changes | ||
``` | ||
The CI runs the check, too. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
@font-face { | ||
font-family: aktiv-grotesk; | ||
src: url("https://assets.freeletics.com/packages/web-package-particle/fonts-v1/aktivgrotesk/aktivgrotesk-rg.woff2") format("woff2"); | ||
font-weight: 400; | ||
font-style: normal | ||
} | ||
|
||
@font-face { | ||
font-family: aktiv-grotesk; | ||
src: url("https://assets.freeletics.com/packages/web-package-particle/fonts-v1/aktivgrotesk/aktivgrotesk-md.woff2") format("woff2"); | ||
font-weight: 500; | ||
font-style: normal | ||
} | ||
|
||
@font-face { | ||
font-family: aktiv-grotesk; | ||
src: url("https://assets.freeletics.com/packages/web-package-particle/fonts-v1/aktivgrotesk/aktivgrotesk-bd.woff2") format("woff2"); | ||
font-weight: 700; | ||
font-style: normal | ||
} | ||
|
||
body, input { | ||
font-family: aktiv-grotesk,"Helvetica Neue",helvetica,sans-serif; | ||
} | ||
|
||
.md-typeset h1, .md-typeset h2, .md-typeset h3, .md-typeset h4 { | ||
font-family: aktiv-grotesk,"Helvetica Neue",helvetica,sans-serif; | ||
line-height: normal; | ||
font-weight: bold; | ||
color: #283237; | ||
} | ||
|
||
.md-typeset a { | ||
color: #3375b8; | ||
} | ||
|
||
:root { | ||
--md-primary-fg-color: #283237; | ||
--md-primary-fg-color--light: #283237; | ||
--md-primary-fg-color--dark: #283237; | ||
|
||
--md-primary-accent-color: #3375b8; | ||
--md-primary-accent-color--transparent: #3375b8; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
--- | ||
hide: | ||
- navigation | ||
- toc | ||
--- | ||
|
||
# DSL Cheatsheet | ||
|
||
If you want to learn more about a particular part of the DSL, we recommend taking a look at the [user guide](/user-guide/). | ||
|
||
The following section describes the syntax and usage of the DSL blocks: | ||
|
||
```kotlin | ||
spec { | ||
inState<State1>{ // inState is always a "top level" element | ||
|
||
// Handle external "input", called Actions | ||
on<Action1>{ action, state -> ... } // Handle an action | ||
on<Action2>(ExecutionPolicy){ action, state -> ... } // You can have multiple on<Action> blocks. Optionally specify ExecutionPolicy | ||
|
||
// Do something when you enter the state. | ||
onEnter{ state -> ... } // Called exactly one time when the given state has been entered | ||
onEnter{ state -> ... } // You can have multiple onEnter blocks | ||
|
||
// Collect a Flow (from kotlinx.coroutines) as long as the state machine is in the state (see inState<State>) | ||
collectWhileInstate(flow1) { valueEmitedFromFlow, state -> ... } // stops flow collection when state is left | ||
collectWhileInstate(flow2) { valueEmitedFromFlow, state -> ... } // You can have multiple collectWhileInstate | ||
|
||
// Effects to do something without changing the state (i.e. logging, analytics, ...) | ||
onActionEffect<Action1>{ action, state -> ... } // You can have multiple onActionEffect | ||
onEnterEffect{ state -> ... } // You can have multiple onEnterEffect | ||
collectWhileInStateEffect(flow1){ valueEmitedFromFlow, state -> ... } // You can have multiple collectWhileInstate | ||
|
||
// Hierarchical state machines | ||
onEnterStartStateMachine( | ||
stateMachineFactory = { stateSnapshot : State1 -> OtherStateMachine() }, | ||
stateMapper = { state : State<State1>, otherStateMachineState : OtherState -> ... } | ||
) | ||
onActionStartStateMachine<Action1>( | ||
stateMachineFactory = { action, stateSnapshot : State1 -> OtherStateMachine() }, | ||
stateMapper = { state : State<State1>, otherStateMachineState :OtherState -> ... } | ||
) | ||
onEnterStartStateMachine(...) // You can have multiple onEnterStartStateMachine | ||
onActionStartStateMachine(...) // You can have multiple onActionStartStateMachine | ||
|
||
unitlIdentityChanged({ state.id }) { | ||
// Everyhting inside this block executes only as long as the "identity" (in this example state.id) | ||
// doesn't change. When it changes, then the previous executions will be canceled and | ||
// this block starts again but with the changed state | ||
|
||
// you can have multiple of the dsl blocks, i.e. multiple on<Action> blocks and so on. | ||
on<Action3>{ action, state -> ... } // you can have multiple on<Action> | ||
onEnter{ state -> ... } | ||
collectWhileInState(flow){ valueEmitedFromFlow, state -> ... } | ||
onActionEffect{ action, state -> ...} | ||
onEnterEffect{ state -> ... } | ||
collectWhileInStateEffect(flow){ valueEmitedFromFlow, state -> ... } | ||
onEnterStartStateMachine(...) | ||
onActionStartStateMachine(...) | ||
} | ||
|
||
// Custom conditions | ||
condition({ state.someString == "Hello" }){ | ||
// Everything inside this block only executes if the surounding condition is met | ||
// and the state machine is in the state as specified by the top level inState<State1>. | ||
|
||
// You can have each DSL block multiple times, i.e. multiple on<Action> blocks and so on. | ||
on<Action3>{ action, state -> ... } | ||
onEnter{ state -> ... } | ||
collectWhileInState(flow){ valueEmitedFromFlow, state -> ... } | ||
onActionEffect{ action, state -> ...} | ||
onEnterEffect{ state -> ... } | ||
collectWhileInStateEffect(flow){ valueEmitedFromFlow, state -> ... } | ||
onEnterStartStateMachine(...) | ||
onActionStartStateMachine(...) | ||
|
||
untilIdentityChanged(...) { // version of untilIdentityChanged that is only ran if the condition block is active | ||
on<Action3>{ action, state -> ... } | ||
onEnter{ state -> ... } | ||
collectWhileInState(flow){ valueEmitedFromFlow, state -> ... } | ||
onActionEffect{ action, state -> ...} | ||
onEnterEffect{ state -> ... } | ||
collectWhileInStateEffect(flow){ valueEmitedFromFlow, state -> ... } | ||
onEnterStartStateMachine(...) | ||
onActionStartStateMachine(...) | ||
|
||
// Please note that you cannot have a condition block inside an untilIdentityChanged block | ||
} | ||
|
||
// Please note that you cannot have nested conditions inside a condition block | ||
} | ||
} | ||
|
||
inState<State2> { ... } // You can have multiple "top level" inState blocks | ||
} | ||
``` |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
# Acting across multiple states | ||
|
||
Let's assume we have our state modeled like this: | ||
|
||
```kotlin | ||
sealed interface ListState | ||
object Loading : ListState | ||
data class ShowContent : ListState | ||
data class Error (val message : String) : ListState | ||
``` | ||
|
||
If for whatever reason you want to trigger a state change for all states you can achieve that by | ||
using `inState<>` on a base class. | ||
|
||
```kotlin | ||
// DSL specs | ||
spec { | ||
inState<ListState> { | ||
// on, onEnter, collectWhileInState for all states because | ||
// ListState is the base class, thus these never get cancelled | ||
} | ||
|
||
inState<Loading> { | ||
// on, onEnter, collectWhileInState specific to Loading state only | ||
} | ||
|
||
inState<ShowContent> { | ||
// on, onEnter, collectWhileInState specific to ShowContent state only | ||
} | ||
|
||
... | ||
} | ||
``` | ||
|
||
In case you want to trigger state changes from a subset of states you could introduce another | ||
level to your state class hierarchy. For example the following would allow you to have a | ||
`inState<PostLoading>` block to share actions between `ShowContent` and `Error`: | ||
|
||
```kotlin | ||
sealed interface ListState { | ||
|
||
// Shows a loading indicator on screen | ||
object Loading : ListState | ||
|
||
sealed interface PostLoading : ListState | ||
|
||
// List of items loaded successfully, show it on screen | ||
data class ShowContent(val items: List<Item>) : PostLoading | ||
|
||
// Error while loading happened | ||
data class Error(val message: String) : PostLoading | ||
} | ||
``` | ||
|
||
``` | ||
// DSL specs | ||
spec { | ||
inState<PostLoading> { | ||
// on, onEnter, collectWhileInState for all PostLoading states. | ||
// It means as long as we are in ShowContent or ErrorState this DSL block | ||
// is active | ||
} | ||
inState<ListState> { | ||
// on, onEnter, collectWhileInState for all ListState states, so for all states of this state machine. | ||
} | ||
inState<Loading> { | ||
// on, onEnter, collectWhileInState specific to Loading state only | ||
} | ||
inState<ShowContent> { | ||
// on, onEnter, collectWhileInState specific to ShowContent state only | ||
} | ||
... | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
# ExecutionPolicy | ||
|
||
Have you ever wondered what would happen if you would execute `Action` very fast 1 after another? | ||
For example: | ||
|
||
```kotlin | ||
spec { | ||
inState<FooState> { | ||
on<BarAction> { action, state : State<FooState> -> | ||
delay(5000) // wait for 5 seconds | ||
state.override { OtherState() } | ||
} | ||
} | ||
} | ||
``` | ||
|
||
The example above shows a problem with async. state machines like FlowRedux: | ||
If our state machine is in `FooState` and a `BarAction` got triggered, we wait for 5 seconds and then set the state to another state. | ||
What if while waiting 5 seconds (i.e. let's say after 3 seconds of waiting) another `BarAction` gets | ||
triggered. | ||
That is possible, right? | ||
With `ExecutionPolicy` you can specify what should happen in that case. | ||
There are three options to choose from: | ||
|
||
- `CANCEL_PREVIOUS`: **This is the default one automatically applied unless specified otherwise.** It would cancel any previous execution and just run the latest one. In the example mentioned it means the previous still running `BarAction` handler gets canceled and a new one with the laster `BarAction` starts. | ||
- `UNORDERED`: Choosing this causes all the blocks to continue running but there are no guarantees in which order. For example: | ||
|
||
```kotlin | ||
spec { | ||
inState<FooState> { | ||
on<BarAction>(executionPolicy = FlapMapPolicy.UNORDERED) { _, state : State<FooState> -> | ||
delay(randomInt()) // wait for some random time | ||
state.override { OtherState } | ||
} | ||
} | ||
} | ||
``` | ||
|
||
Let's assume that we trigger `BarAction` two times. | ||
We use random amount of seconds for waiting. | ||
Since we use `UNORDERED` as policy `on<BarAction>` the handler block gets executed 2 times without canceling the previous one (that is the difference to `CANCEL_PREVIOUS`). | ||
Moreover, `UNORDERED` doesn't make any promise on order of execution of the block (see `ORDERED` if you need promises on order). | ||
If `on<BarAction>` gets executed two times it will run in parallel and the the second execution | ||
could complete before the first execution (because using a random time of waiting). | ||
|
||
- `ORDERED`: In contrast to `UNORDERED` and `CANCEL_PREVIOUS`, `ORDERED` will not run `on<BarAction>` in parallel and will not cancel any previous execution. Instead, `ORDERED` will preserve the order. | ||
|
||
`on<Action>` and `collectWhileInstate()` can specify an `ExecutionPolicy`: | ||
|
||
- `on<Action>(executionPolicy = ExecutionPolicy.CANCEL_PREVIOUS) { ... }` | ||
- `collectWhileInState(executionPolicy = ExecutionPolicy.CANCEL_PREVIOUS) { ... }` | ||
|
||
Please note that `onEnter` doesn't have the option to specify `ExecutionPolicy`. |
Oops, something went wrong.