xref: /aosp_15_r20/frameworks/base/packages/SystemUI/utils/kairos/docs/flow-to-kairos-cheatsheet.md (revision d57664e9bc4670b3ecf6748a746a57c557b6bc9e)
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)
298299}
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