1# From Flows to Kairos 2 3## Key differences 4 5* Kairos evaluates all events (`TFlow` emissions + observers) in a transaction. 6 7* Kairos splits `Flow` APIs into two distinct types: `TFlow` and `TState` 8 9 * `TFlow` is roughly equivalent to `SharedFlow` w/ a replay cache that 10 exists for the duration of the current Kairos transaction and shared with 11 `SharingStarted.WhileSubscribed()` 12 13 * `TState` is roughly equivalent to `StateFlow` shared with 14 `SharingStarted.Eagerly`, but the current value can only be queried within 15 a Kairos transaction, and the value is only updated at the end of the 16 transaction 17 18* Kairos further divides `Flow` APIs based on how they internally use state: 19 20 * **FrpTransactionScope:** APIs that internally query some state need to be 21 performed within an Kairos transaction 22 23 * this scope is available from the other scopes, and from most lambdas 24 passed to other Kairos APIs 25 26 * **FrpStateScope:** APIs that internally accumulate state in reaction to 27 events need to be performed within an FRP State scope (akin to a 28 `CoroutineScope`) 29 30 * this scope is a side-effect-free subset of FrpBuildScope, and so can be 31 used wherever you have an FrpBuildScope 32 33 * **FrpBuildScope:** APIs that perform external side-effects (`Flow.collect`) 34 need to be performed within an FRP Build scope (akin to a `CoroutineScope`) 35 36 * this scope is available from `FrpNetwork.activateSpec { … }` 37 38 * All other APIs can be used anywhere 39 40## emptyFlow() 41 42Use `emptyTFlow` 43 44``` kotlin 45// this TFlow emits nothing 46val noEvents: TFlow<Int> = emptyTFlow 47``` 48 49## map { … } 50 51Use `TFlow.map` / `TState.map` 52 53``` kotlin 54val anInt: TState<Int> = … 55val squared: TState<Int> = anInt.map { it * it } 56val messages: TFlow<String> = … 57val messageLengths: TFlow<Int> = messages.map { it.size } 58``` 59 60## filter { … } / mapNotNull { … } 61 62### I have a TFlow 63 64Use `TFlow.filter` / `TFlow.mapNotNull` 65 66``` kotlin 67val messages: TFlow<String> = … 68val nonEmpty: TFlow<String> = messages.filter { it.isNotEmpty() } 69``` 70 71### I have a TState 72 73Convert the `TState` to `TFlow` using `TState.stateChanges`, then use 74`TFlow.filter` / `TFlow.mapNotNull` 75 76If you need to convert back to `TState`, use `TFlow.hold(initialValue)` on the 77result. 78 79``` kotlin 80tState.stateChanges.filter { … }.hold(initialValue) 81``` 82 83Note that `TFlow.hold` is only available within an `FrpStateScope` in order to 84track the lifetime of the state accumulation. 85 86## combine(...) { … } 87 88### I have TStates 89 90Use `combine(TStates)` 91 92``` kotlin 93val someInt: TState<Int> = … 94val someString: TState<String> = … 95val model: TState<MyModel> = combine(someInt, someString) { i, s -> MyModel(i, s) } 96``` 97 98### I have TFlows 99 100Convert the TFlows to TStates using `TFlow.hold(initialValue)`, then use 101`combine(TStates)` 102 103If you want the behavior of Flow.combine where nothing is emitted until each 104TFlow has emitted at least once, you can use filter: 105 106``` kotlin 107// null used as an example, can use a different sentinel if needed 108combine(tFlowA.hold(null), tFlowB.hold(null)) { a, b -> 109 a?.let { b?.let { … } } 110 } 111 .filterNotNull() 112``` 113 114Note that `TFlow.hold` is only available within an `FrpStateScope` in order to 115track the lifetime of the state accumulation. 116 117#### Explanation 118 119`Flow.combine` always tracks the last-emitted value of each `Flow` it's 120combining. This is a form of state-accumulation; internally, it collects from 121each `Flow`, tracks the latest-emitted value, and when anything changes, it 122re-runs the lambda to combine the latest values. 123 124An effect of this is that `Flow.combine` doesn't emit until each combined `Flow` 125has emitted at least once. This often bites developers. As a workaround, 126developers generally append `.onStart { emit(initialValue) }` to the `Flows` 127that don't immediately emit. 128 129Kairos avoids this gotcha by forcing usage of `TState` for `combine`, thus 130ensuring that there is always a current value to be combined for each input. 131 132## collect { … } 133 134Use `observe { … }` 135 136``` kotlin 137val job: Job = tFlow.observe { println("observed: $it") } 138``` 139 140Note that `observe` is only available within an `FrpBuildScope` in order to 141track the lifetime of the observer. `FrpBuildScope` can only come from a 142top-level `FrpNetwork.transaction { … }`, or a sub-scope created by using a 143`-Latest` operator. 144 145## sample(flow) { … } 146 147### I want to sample a TState 148 149Use `TState.sample()` to get the current value of a `TState`. This can be 150invoked anywhere you have access to an `FrpTransactionScope`. 151 152``` kotlin 153// the lambda passed to map receives an FrpTransactionScope, so it can invoke 154// sample 155tFlow.map { tState.sample() } 156``` 157 158#### Explanation 159 160To keep all state-reads consistent, the current value of a TState can only be 161queried within a Kairos transaction, modeled with `FrpTransactionScope`. Note 162that both `FrpStateScope` and `FrpBuildScope` extend `FrpTransactionScope`. 163 164### I want to sample a TFlow 165 166Convert to a `TState` by using `TFlow.hold(initialValue)`, then use `sample`. 167 168Note that `hold` is only available within an `FrpStateScope` in order to track 169the lifetime of the state accumulation. 170 171## stateIn(scope, sharingStarted, initialValue) 172 173Use `TFlow.hold(initialValue)`. There is no need to supply a sharingStarted 174argument; all states are accumulated eagerly. 175 176``` kotlin 177val ints: TFlow<Int> = … 178val lastSeenInt: TState<Int> = ints.hold(initialValue = 0) 179``` 180 181Note that `hold` is only available within an `FrpStateScope` in order to track 182the lifetime of the state accumulation (akin to the scope parameter of 183`Flow.stateIn`). `FrpStateScope` can only come from a top-level 184`FrpNetwork.transaction { … }`, or a sub-scope created by using a `-Latest` 185operator. Also note that `FrpBuildScope` extends `FrpStateScope`. 186 187## distinctUntilChanged() 188 189Use `distinctUntilChanged` like normal. This is only available for `TFlow`; 190`TStates` are already `distinctUntilChanged`. 191 192## merge(...) 193 194### I have TFlows 195 196Use `merge(TFlows) { … }`. The lambda argument is used to disambiguate multiple 197simultaneous emissions within the same transaction. 198 199#### Explanation 200 201Under Kairos's rules, a `TFlow` may only emit up to once per transaction. This 202means that if we are merging two or more `TFlows` that are emitting at the same 203time (within the same transaction), the resulting merged `TFlow` must emit a 204single value. The lambda argument allows the developer to decide what to do in 205this case. 206 207### I have TStates 208 209If `combine` doesn't satisfy your needs, you can use `TState.stateChanges` to 210convert to a `TFlow`, and then `merge`. 211 212## conflatedCallbackFlow { … } 213 214Use `tFlow { … }`. 215 216As a shortcut, if you already have a `conflatedCallbackFlow { … }`, you can 217convert it to a TFlow via `Flow.toTFlow()`. 218 219Note that `tFlow` is only available within an `FrpBuildScope` in order to track 220the lifetime of the input registration. 221 222## first() 223 224### I have a TState 225 226Use `TState.sample`. 227 228### I have a TFlow 229 230Use `TFlow.nextOnly`, which works exactly like `Flow.first` but instead of 231suspending it returns a `TFlow` that emits once. 232 233The naming is intentionally different because `first` implies that it is the 234first-ever value emitted from the `Flow` (which makes sense for cold `Flows`), 235whereas `nextOnly` indicates that only the next value relative to the current 236transaction (the one `nextOnly` is being invoked in) will be emitted. 237 238Note that `nextOnly` is only available within an `FrpStateScope` in order to 239track the lifetime of the state accumulation. 240 241## flatMapLatest { … } 242 243If you want to use -Latest to cancel old side-effects, similar to what the Flow 244-Latest operators offer for coroutines, see `mapLatest`. 245 246### I have a TState… 247 248#### …and want to switch TStates 249 250Use `TState.flatMap` 251 252``` kotlin 253val flattened = tState.flatMap { a -> getTState(a) } 254``` 255 256#### …and want to switch TFlows 257 258Use `TState<TFlow<T>>.switch()` 259 260``` kotlin 261val tFlow = tState.map { a -> getTFlow(a) }.switch() 262``` 263 264### I have a TFlow… 265 266#### …and want to switch TFlows 267 268Use `hold` to convert to a `TState<TFlow<T>>`, then use `switch` to switch to 269the latest `TFlow`. 270 271``` kotlin 272val tFlow = tFlowOfFlows.hold(emptyTFlow).switch() 273``` 274 275#### …and want to switch TStates 276 277Use `hold` to convert to a `TState<TState<T>>`, then use `flatMap` to switch to 278the latest `TState`. 279 280``` kotlin 281val tState = tFlowOfStates.hold(tStateOf(initialValue)).flatMap { it } 282``` 283 284## mapLatest { … } / collectLatest { … } 285 286`FrpStateScope` and `FrpBuildScope` both provide `-Latest` operators that 287automatically cancel old work when new values are emitted. 288 289``` kotlin 290val currentModel: TState<SomeModel> = … 291val mapped: TState<...> = currentModel.mapLatestBuild { model -> 292 effect { "new model in the house: $model" } 293 model.someState.observe { "someState: $it" } 294 val someData: TState<SomeInfo> = 295 getBroadcasts(model.uri) 296 .map { extractInfo(it) } 297 .hold(initialInfo) 298 … 299} 300``` 301 302## flowOf(...) 303 304### I want a TState 305 306Use `tStateOf(initialValue)`. 307 308### I want a TFlow 309 310Use `now.map { initialValue }` 311 312Note that `now` is only available within an `FrpTransactionScope`. 313 314#### Explanation 315 316`TFlows` are not cold, and so there isn't a notion of "emit this value once 317there is a collector" like there is for `Flow`. The closest analog would be 318`TState`, since the initial value is retained indefinitely until there is an 319observer. However, it is often useful to immediately emit a value within the 320current transaction, usually when using a `flatMap` or `switch`. In these cases, 321using `now` explicitly models that the emission will occur within the current 322transaction. 323 324``` kotlin 325fun <T> FrpTransactionScope.tFlowOf(value: T): TFlow<T> = now.map { value } 326``` 327 328## MutableStateFlow / MutableSharedFlow 329 330Use `MutableTState(frpNetwork, initialValue)` and `MutableTFlow(frpNetwork)`. 331