Skip to content

Commit b3dcfa8

Browse files
committed
Documentation
Refs: #7
1 parent 3f38464 commit b3dcfa8

3 files changed

Lines changed: 209 additions & 0 deletions

File tree

README.md

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ Please checkout the Medium article on pattern/library usage.
4040
+ [Gestures and view-states](#gestures-and-view-states)
4141
+ [View implementation](#view-implementation)
4242
+ [Adopting foreign state-flow](#adopting-foreign-state-flow)
43+
- [Running state-machines in parallel (composition)](#running-state-machines-in-parallel-composition)
44+
* [MultiMachineState](#multimachinestate)
45+
* [ProxyMachineContainer](#proxymachinecontainer)
46+
* [Mapping UI states](#mapping-ui-states)
47+
* [Dispatching gestures](#dispatching-gestures)
48+
* [MachineLifecle bonus](#machinelifecle-bonus)
4349
- [Conclusion](#conclusion)
4450
- [Note on multiplatform](#note-on-multiplatform)
4551

@@ -125,6 +131,9 @@ val commonMain by getting {
125131

126132
- [LCE](examples/lce) - basic example of Load-Content-Error application
127133
- [Welcome](examples/welcome/welcome) - multi-module example of user on-boarding flow
134+
- [Parallel](examples/multi/parallel) - two machines running in parallel in one proxy state
135+
- [Navbar](examples/multi/navbar) - several machines running in proxy state, one of them active at a time
136+
- [Lifecycle](examples/lifecycle) - track your Android app lifecycle to pause pending operations when the app is suspended
128137

129138
## The basic task - Load-Content-Error
130139

@@ -1163,6 +1172,206 @@ or to advance to `Complete` state as described in [Common Api](#common-api). The
11631172
this interface by switching host machine to email or complete states in corresponding
11641173
`backToEmailEntry` and `complete` functions.
11651174

1175+
## Running state-machines in parallel (composition)
1176+
1177+
In case you want several state-machines to run in parallel producing a single combined UI state or you
1178+
want to persist several machines on a single screen (like a page with a bottom navigation) there is an
1179+
option to do it with the [MultiMachineState](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/multi/MultiMachineState.kt)
1180+
and [ProxyMachineContainer](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/multi/ProxyMachineContainer.kt)
1181+
1182+
### MultiMachineState
1183+
1184+
This state is a proxy that holds several machines at once. It is in charge for combining the UI state
1185+
whenever the running machine updates and for dispatching gestures from a single parent gesture to
1186+
proxied machines inside the composition. To distinguish machines and to ensure type-safety each machine in
1187+
composition is identified with the [MachineKey](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/multi/MachineKey.kt)
1188+
The state has three things to override:
1189+
1190+
- [container](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/multi/MultiMachineState.kt#L28):
1191+
manages machines lifecycle. More on this follows.
1192+
- [mapUiState](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/multi/MultiMachineState.kt#L97):
1193+
called each time your proxied machine updates UI state or explicitly when calling [updateUi](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/multi/MultiMachineState.kt#L69).
1194+
Here you take a decision on changes and build a common resulting UI state. See the dedicated section below.
1195+
- [mapGesture](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/multi/MultiMachineState.kt#L91):
1196+
called when state gesture is processed. Here you can map the gesture and update your proxied machine.
1197+
1198+
Now let's see how the things work a bit closer.
1199+
1200+
### ProxyMachineContainer
1201+
1202+
Container is in charge for creating and managing the lifecycle of the state machines. So far the
1203+
interface has two companion functions:
1204+
1205+
- [allTogether](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/multi/ProxyMachineContainer.kt#L44):
1206+
runs all machines in parallel with common lifecycle - startup and cleanup. [Example](examples/multi/parallel) - running two
1207+
timers simultaneously:
1208+
1209+
![parallel](doc/screenshots/parallel.png)
1210+
1211+
- [some](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/multi/ProxyMachineContainer.kt#L55):
1212+
runs machines with additional [MachineLifecycle](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/lifecycle/MachineLifecycle.kt)
1213+
management. You could make some machines active and dormant with [ActiveMachineContainer](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/multi/ProxyMachineContainer.kt#L65)
1214+
methods.
1215+
1216+
![navbar](doc/screenshots/navbar.png)
1217+
1218+
Container is initialized with a collection of [MachineInit](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/multi/MachineInit.kt)
1219+
structures:
1220+
1221+
```kotlin
1222+
/**
1223+
* Proxy machine initialization record
1224+
*/
1225+
interface MachineInit<G: Any, U: Any> {
1226+
/**
1227+
* Machine key to find a machine among the others
1228+
*/
1229+
val key: MachineKey<G, U>
1230+
1231+
/**
1232+
* Initial UI state for the machine
1233+
*/
1234+
val initialUiState: U
1235+
1236+
/**
1237+
* Creates initial child state
1238+
* [MachineLifecycle] passed to the factory determines the activity of
1239+
* the machine within the machine group. For example, for a paging screen
1240+
* you may want to stop some pending operations when active machine is not
1241+
* active anymore
1242+
*/
1243+
val init: (MachineLifecycle) -> CommonMachineState<G, U>
1244+
}
1245+
```
1246+
1247+
The `init` function is called each time the container needs to create a new machine. The [MachineLifecycle](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/lifecycle/MachineLifecycle.kt)
1248+
interface passed to initialization may be used by your states to determine if the machine is suspended
1249+
or active. If you use coroutines you could use [asFlow](coroutines/src/commonMain/kotlin/com/motorro/commonstatemachine/coroutines/lifecycle/lifecycleStateFlow.kt)
1250+
function to convert it to `Flow`. See [example](examples/timer/src/commonMain/kotlin/com/motorro/statemachine/timer/state/TimerState.kt) on how to start/stop
1251+
your pending operations that are not needed when your machine is inactive: gps tracking, server messaging, etc.
1252+
For example:
1253+
1254+
```kotlin
1255+
1256+
private sealed class MultiGesture {
1257+
data class IntGesture(val data: Int) : MultiGesture()
1258+
data class StringGesture(val data: String) : MultiGesture()
1259+
}
1260+
1261+
private open class TestState : MultiMachineState<MultiGesture, String>() {
1262+
1263+
private data object IntKey : MachineKey<Int, Int>(null) // Int for gesture and state
1264+
private data object StringKey : MachineKey<String, String>(null) // String for gesture and state
1265+
1266+
override val container: ProxyMachineContainer = AllTogetherMachineContainer(
1267+
listOf(
1268+
object : MachineInit<Int, Int> {
1269+
override val key: MachineKey<Int, Int> = IntKey
1270+
override val initialUiState: Int = 0
1271+
override val init: (MachineLifecycle) -> CommonMachineState<Int, Int> = {
1272+
TestChildState(0)
1273+
}
1274+
},
1275+
object : MachineInit<String, String> {
1276+
override val key: MachineKey<String, String> = StringKey
1277+
override val initialUiState: String = "X"
1278+
override val init: (MachineLifecycle) -> CommonMachineState<String, String> = {
1279+
TestChildState("X")
1280+
}
1281+
}
1282+
)
1283+
)
1284+
}
1285+
```
1286+
1287+
Check example states for each case:
1288+
1289+
- [Parallel](examples/multi/parallel/src/main/java/com/motorro/statemachine/parallel/model/state/ParallelState.kt) - two machines running in parallel in one proxy state
1290+
- [Navbar](examples/multi/navbar/src/main/java/com/motorro/statemachine/navbar/model/state/NavbarState.kt) - several machines running in proxy state, one of them active at a time
1291+
1292+
### Mapping UI states
1293+
1294+
The gesture/ui type systems for each machine in composition are different, so we need some kind of
1295+
type casting to be on a safe side. Binding machines with keys in `MachineInit` makes sure the machine type
1296+
corresponds to the key and is used to map key to correct UI-state in [mapUiState](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/multi/MultiMachineState.kt#L97)
1297+
method of `MultiMachineState`. To be able to do it, take the [UiStateProvider](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/multi/MachineAccess.kt#L21)
1298+
provided to the method to get the correct ui-state type:
1299+
1300+
```kotlin
1301+
1302+
private sealed class MultiGesture {
1303+
data class IntGesture(val data: Int) : MultiGesture()
1304+
data class StringGesture(val data: String) : MultiGesture()
1305+
}
1306+
1307+
private open class TestState : MultiMachineState<MultiGesture, String>() {
1308+
1309+
private data object IntKey : MachineKey<Int, Int>(null) // Int for gesture and state
1310+
private data object StringKey : MachineKey<String, String>(null) // String for gesture and state
1311+
1312+
// ... machine init omitted
1313+
1314+
override fun mapUiState(provider: UiStateProvider, changedKey: MachineKey<*, *>?): String {
1315+
val i: Int = provider.getValue(IntKey) // Cast to Int
1316+
val s: String = provider.getValue(StringKey) // Cast to String
1317+
return "$i - $s" // Combined state of any kind you like
1318+
}
1319+
}
1320+
```
1321+
1322+
Check example states for use cases:
1323+
1324+
- [Parallel](examples/multi/parallel/src/main/java/com/motorro/statemachine/parallel/model/state/ParallelState.kt) - two machines running in parallel in one proxy state
1325+
- [Navbar](examples/multi/navbar/src/main/java/com/motorro/statemachine/navbar/model/state/NavbarState.kt) - several machines running in proxy state, one of them active at a time
1326+
1327+
### Dispatching gestures
1328+
1329+
As with UI-state mapping, binding machines with keys in `MachineInit` makes sure the machine type
1330+
corresponds to the key and is used to map key to correct gesture processor. Whenever the proxy receives
1331+
a gesture it calls [mapGesture](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/multi/MultiMachineState.kt#L91).
1332+
Using the provided [GestureProcessor](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/multi/MachineAccess.kt#L48)
1333+
and a key you can get access to the proxied machine instance to map and process your gesture:
1334+
1335+
```kotlin
1336+
1337+
private sealed class MultiGesture {
1338+
data class IntGesture(val data: Int) : MultiGesture()
1339+
data class StringGesture(val data: String) : MultiGesture()
1340+
}
1341+
1342+
private open class TestState : MultiMachineState<MultiGesture, String>() {
1343+
1344+
private data object IntKey : MachineKey<Int, Int>(null) // Int for gesture and state
1345+
private data object StringKey : MachineKey<String, String>(null) // String for gesture and state
1346+
1347+
// ... machine init omitted
1348+
1349+
// Our parent gesture is
1350+
override fun mapGesture(parent: MultiGesture, processor: GestureProcessor) = when(parent) {
1351+
is MultiGesture.IntGesture -> {
1352+
processor.process(IntKey, parent.data) // Int expected
1353+
}
1354+
is MultiGesture.StringGesture -> {
1355+
processor.process(StringKey, parent.data) // String expected
1356+
}
1357+
}
1358+
}
1359+
```
1360+
1361+
Check example states and test class for use cases:
1362+
1363+
- [Parallel](examples/multi/parallel/src/main/java/com/motorro/statemachine/parallel/model/state/ParallelState.kt) - two machines running in parallel in one proxy state
1364+
- [Navbar](examples/multi/navbar/src/main/java/com/motorro/statemachine/navbar/model/state/NavbarState.kt) - several machines running in proxy state, one of them active at a time
1365+
- [MultiMachineStateTest](commonstatemachine/src/commonTest/kotlin/com/motorro/commonstatemachine/multi/MultiMachineStateTest.kt) - unit test
1366+
1367+
### MachineLifecyle bonus
1368+
1369+
The interface used to pass the machine activity to proxied state machine could also be used as an
1370+
view lifecycle monitor for your app. Pass [UiMachineLifecycle](commonstatemachine/src/androidMain/kotlin/com/motorro/commonstatemachine/lifecycle/UiMachineLifecycle.kt)
1371+
to your model initialization to by able to suspend your machines when app is not in use.
1372+
Similar to [state collection methods](https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda) optimized with lificycle.
1373+
Check the [example](examples/lifecycle) to get the details.
1374+
11661375
## Conclusion
11671376

11681377
I hope someone finds the article (and the library if you like to take it as-is) helpful in building

doc/screenshots/navbar.png

31.1 KB
Loading

doc/screenshots/parallel.png

34.4 KB
Loading

0 commit comments

Comments
 (0)