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