• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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)
297298}
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