Slide 1

Slide 1 text

Kotlin Flows In-depth analysis of Kotlin Flows

Slide 2

Slide 2 text

Rostyslav Lesovyi

Slide 3

Slide 3 text

Kotlin Flows ➙ About ➙ Using ➙ Internals ➙ Extending ➙ Benchmarks ➙ Conclusion

Slide 4

Slide 4 text

// KLUG - Kotlin Flows About Flows Who is this new kid on the block?

Slide 5

Slide 5 text

// KLUG - Kotlin Flows Flows - Introduction val list = listOf(1, 2, 3) list.forEach { print(it) } val sequence = sequenceOf(1, 2, 3) sequence.forEach { print(it) } val flow = flowOf(1, 2, 3) flow.collect { print(it) } val observable = Observable.fromArray( 1, 2, 3) observable.subscribe { print(it) } -> 1, 2, 3

Slide 6

Slide 6 text

// KLUG - Kotlin Flows Flows - Introduction listOf(1, 2, 3) .filter { it % 2 != 0 } .map { it * 2 } .forEach { print(it) } 1 2 3 1 3 2 6 2 6 listOf filter map forEach List created

Slide 7

Slide 7 text

// KLUG - Kotlin Flows Flows - Introduction sequenceOf(1, 2, 3) .filter { it % 2 != 0 } .map { it * 2 } .forEach { print(it) } 1 2 3 1 6 6 sequenceOf filter map forEach 2 2 3 .. ..

Slide 8

Slide 8 text

// KLUG - Kotlin Flows IO Thread Caller Thread flowOf(1, 2, 3) .filter { it % 2 != 0 } .flowOn(Dispatchers.IO) .collect { print(it) } Flows - Introduction 1 2 3 1 3 flowOf filter forEach 1 3 .. ..

Slide 9

Slide 9 text

// KLUG - Kotlin Flows Flows - Introduction flowOf(1, 2, 3) .filter { it % 2 != 0 } .flowOn(Dispatchers.IO) .collect { print(it) } Observable.fromArray( 1, 2, 3) .filter { it % 2 != 0 } .subscribeOn(Schedulers.io()) .observeOn(mainScheduler) .subscribe { print(it) }

Slide 10

Slide 10 text

// KLUG - Kotlin Flows Using Flows Wow, they are so similar to Rx. I know them already!

Slide 11

Slide 11 text

// KLUG - Kotlin Flows Using Flows - Type of the Operators ● Builders ● Terminal operators ● Flow operators ● Context ● Delay ● Error ● Distinct ● Transform ● Limit ● Merge ● Emitters ● Zip

Slide 12

Slide 12 text

// KLUG - Kotlin Flows Using Flows - Terminology flowOf(1, 2, 3) .filter { it % 2 != 0 } .map { it * 2 } .map { "value = $it" } .catch { print(it.message) } .collect() < Builder < Flow operators < Terminal operator

Slide 13

Slide 13 text

// KLUG - Kotlin Flows Using Flows - Terminology flowOf(1, 2, 3) .filter { it % 2 != 0 } .map { it * 2 } .map { "value = $it" } .catch { print(it.message) } .collect() Upstream Downstream

Slide 14

Slide 14 text

// KLUG - Kotlin Flows Using Flows - Builders

Slide 15

Slide 15 text

// KLUG - Kotlin Flows Using Flows - Builders val flow1 = flowOf(1, 2, 3) // Observable.fromArray val flow2 = listOf(1, 2, 3).asFlow() // toObservable() val empty = emptyFlow() // Observable.empty()

Slide 16

Slide 16 text

// KLUG - Kotlin Flows Using Flows - Builders val list = flowOf(1, 2, 3).toList() // Observable.toList() val set = flowOf(1, 2, 3).toSet() val linkedList = flowOf(1, 2, 3).toCollection(LinkedList())

Slide 17

Slide 17 text

// KLUG - Kotlin Flows Using Flows - Builders val flow = flow { // Observable.create emit(1) scope.launch { emit(2) <-- prohibited } } val flow = channelFlow { // Observable.create send(1) launch { send(2) <-- allowed } awaitClose { /* free resources if needed */ } }

Slide 18

Slide 18 text

// KLUG - Kotlin Flows Using Flows - Terminal operators

Slide 19

Slide 19 text

// KLUG - Kotlin Flows Using Flows - Terminal operators val flow = flowOf(1, 2, 3) scope.launch { flow.collect { print(it) } // Observable.subscribe { print(it) } } -> 1, 2, 3

Slide 20

Slide 20 text

// KLUG - Kotlin Flows Using Flows - Terminal operators val flow = flowOf(1, 2, 3) flow.launchIn(scope) scope.launch { flow.collect() }

Slide 21

Slide 21 text

// KLUG - Kotlin Flows Using Flows - Terminal operators val flow = flow { emit(1) delay(50) // suspend block, we can delay here emit(2) } flow.collectLatest { delay(100) // emulate work println(it) } -> 2

Slide 22

Slide 22 text

// KLUG - Kotlin Flows Using Flows - Terminal operators val flow = flowOf(1, 2, 3) flow.count() // Observable.count() -> 3

Slide 23

Slide 23 text

// KLUG - Kotlin Flows Using Flows - Terminal operators val flow = flowOf(1) flow.single() // Observable.singleOrError() flow.singleOrNull() // Observable.singleElement() flow.first() // Observable.firstElement() flow.firstOrNull() // Observable.firstOrError() -> 1

Slide 24

Slide 24 text

// KLUG - Kotlin Flows Using Flows - Terminal operators val flow = flowOf(1, 2, 3) flow.reduce { accumulator, value -> // Observable.reduce() accumulator + value } -> 6

Slide 25

Slide 25 text

// KLUG - Kotlin Flows Using Flows - Terminal operators val flow = flowOf(1, 2, 3) flow.fold(20) { accumulator, value -> // Observable.reduce() accumulator + value } -> 26

Slide 26

Slide 26 text

// KLUG - Kotlin Flows Using Flows - Flow operators

Slide 27

Slide 27 text

// KLUG - Kotlin Flows Using Flows - Flow operators val flow = flowOf(1, 2, 3) flow.map { it * 2 } // Observable.map { ... } -> 2, 4, 6

Slide 28

Slide 28 text

// KLUG - Kotlin Flows Using Flows - Flow operators val flow = flowOf(1, 2, 3) flow.filter { it % 2 != 0 } // Observable.filter { ... } -> 1, 3

Slide 29

Slide 29 text

// KLUG - Kotlin Flows Using Flows - Flow operators val flow = flowOf(1, 2, 3) flow.flatMapMerge { // Observable.flatMap { ... } flowOf(it * 2, it * 3) } -> [1, 2, 3] -> [[2, 3], [4, 6], [6, 9]] -> [2, 3, 4, 6, 6, 9]

Slide 30

Slide 30 text

// KLUG - Kotlin Flows Using Flows - Flow operators val flow = flowOf(1, 2, 3) flow.transform { // Observable.flatMap { ... } emit(it * 2) emit(it * 3) } -> [1, 2, 3] -> [[2, 3], [4, 6], [6, 9]] -> [2, 3, 4, 6, 6, 9]

Slide 31

Slide 31 text

// KLUG - Kotlin Flows Using Flows - Flow operators val flow = flowOf(1, 2, 3) // Observable.onErrorResumeNext { } flow.map { throw Exception("oops") } .catch { print(it.message) } .collect() -> oops

Slide 32

Slide 32 text

// KLUG - Kotlin Flows Using Flows - Flow operators val flow = flowOf(1, 2, 3) flow.onStart { coroutineContext[CoroutineName]?. name } .flowOn(CoroutineName( "name2")) .onStart { coroutineContext[CoroutineName]?. name } .flowOn(CoroutineName( "name1")) .collect() -> name1, name2

Slide 33

Slide 33 text

// KLUG - Kotlin Flows Using Flows - Cancellation

Slide 34

Slide 34 text

// KLUG - Kotlin Flows Using Flows - Cancellation #1 val flow = flowOf(1, 2, 3) val job = scope.launch { flow.collect { } } job.cancel() // Disposable.dispose()

Slide 35

Slide 35 text

// KLUG - Kotlin Flows Using Flows - Cancellation #2 val flow = flowOf(1, 2, 3) try { flow.collect { if (it == 2) throw Exception("cancel") print(it) } } catch (e: Exception) { print(e.message) } -> 1, cancel

Slide 36

Slide 36 text

// KLUG - Kotlin Flows Using Flows - Cancellation #3 val flow = flowOf(1, 2, 3) withTimeoutOrNull(400) { flow.collect { delay(150) print(it) } } -> 1, 2

Slide 37

Slide 37 text

// KLUG - Kotlin Flows Flow Internals The truth is more complicated... !

Slide 38

Slide 38 text

// KLUG - Kotlin Flows Flow Internals ● Properties ○ Context preservation ○ Exception transparency ● Tax Flow collectors ● Operator fusing ● Backpressure ● Cold vs Hot

Slide 39

Slide 39 text

// KLUG - Kotlin Flows Flow Internals - Context preservation

Slide 40

Slide 40 text

// KLUG - Kotlin Flows Flow Internals - Context preservation 1. scope.launch { 2. flow { 3. emit(1) 4. 5. launch { 6. emit(2) 7. } 8. } 9. } < OK < prohibited < coroutine context #1 < coroutine context #2 Rule #1: emit must always be called on the coroutine context of its flow

Slide 41

Slide 41 text

// KLUG - Kotlin Flows Flow Internals - Context preservation 1. scope.launch { 2. channelFlow { 3. send(1) 4. 5. launch { 6. send(2) 7. } 8. } 9. } Rule #2: send/offer doesn’t care about coroutine context

Slide 42

Slide 42 text

// KLUG - Kotlin Flows Flow Internals - Context preservation 1. flow { 2. val channel = Channel(Channel. BUFFERED) 3. 4. launch { 5. channel.send(1) 6. channel.send( 2) 7. channel.send( 3) 8. } 9. 10. for (value in channel) { 11. emit(value) 12. } 13. }

Slide 43

Slide 43 text

// KLUG - Kotlin Flows Flow Internals - Exception transparency

Slide 44

Slide 44 text

// KLUG - Kotlin Flows Flow Internals - Exception transparency 1. @Throws(Exception::class) 2. fun doSomethingOrCrash(): Int 3. 4. flow { 5. try { 6. emit(doSomethingOrCrash()) 7. } catch (e: Exception) { 8. // handle or ignore 9. } 10. } < prohibited Rule #3: emit (downstream) exception must never be handled

Slide 45

Slide 45 text

// KLUG - Kotlin Flows Flow Internals - Exception transparency 1. val flow = flow { 2. for (value in 0..1000) { 3. try { 4. emit(value) 5. } catch (e: Exception) { 6. print(e.message) 7. } 8. } 9. } 10. flow.collect { 11. if (it == 2) throw Exception("cancel") 12. print(it) 13. } -> 0, 1, cancel, 3, 4, 5, ...

Slide 46

Slide 46 text

// KLUG - Kotlin Flows Flow Internals - Exception transparency 1. withTimeoutOrNull(25) { 2. val flow = flow { 3. for (value in 0..1000) { 4. try { 5. emit(value) 6. delay(50) 7. } catch (e: Exception) { 8. print(e.message) 9. } 10. } 11. } 12. flow.collect { print(it) } 13. } -> 0, Timed out waiting for 25 ms, 1, Timed out waiting for 25 ms, 2, ...

Slide 47

Slide 47 text

// KLUG - Kotlin Flows Flow Internals - Flow collectors

Slide 48

Slide 48 text

// KLUG - Kotlin Flows Flow Internals - Flow collectors 1. public interface Flow { 2. public suspend fun collect(collector: FlowCollector< T>) 3. } 1. public interface FlowCollector< in T> { 2. public suspend fun emit(value: T) 3. }

Slide 49

Slide 49 text

// KLUG - Kotlin Flows Flow Internals - Flow collectors 1. val flow = flow { 2. emit(1) 3. emit(2) 4. } 1. val flow = object : Flow { 2. override suspend fun collect(collector: FlowCollector) { 3. collector.emit( 1) 4. collector.emit( 2) 5. } 6. } Almost the same except flow {...} also enforces properties checks

Slide 50

Slide 50 text

// KLUG - Kotlin Flows Flow Internals - Operator fusing

Slide 51

Slide 51 text

// KLUG - Kotlin Flows Flow Internals - Operator fusing ### Operator fusion Adjacent applications of [channelFlow], [flowOn], [buffer], [produceIn], and [broadcastIn] are always fused so that only one properly configured channel is used for execution. 1. flowOf(1, 2, 3) 2. .flowOn(Dispatchers.IO) 3. .buffer(20) 4. .flowOn(Dispatchers.Main) 5. .count { } < channel-based < channel-based < channel-based all reusing single channel

Slide 52

Slide 52 text

// KLUG - Kotlin Flows Flow Internals - Backpressure

Slide 53

Slide 53 text

// KLUG - Kotlin Flows Flow Internals - Backpressure 1. val flow = flow { 2. for (value in 0..5) { 3. print("out $value") 4. emit(value) 5. } 6. } 7. flow.collect { 8. print("in $it") 9. delay(100) 10. } -> out 0, in 0, out 1, in 1, out 2, in 2, ...

Slide 54

Slide 54 text

// KLUG - Kotlin Flows Flow Internals - Backpressure 1. val flow = flow { 2. for (value in 0..5) { 3. print("out $value") 4. emit(value) 5. } 6. } 7. flow.buffer(1).collect { 8. delay(100) 9. print("in $it") 10. } -> out 0, out 1, out 2, in 0, out 3, in 1, out 4, in 2, out 5, ...

Slide 55

Slide 55 text

// KLUG - Kotlin Flows Flow Internals - Backpressure 1. val flow = flow { 2. for (value in 0..5) { 3. print("out $value") 4. emit(value) 5. } 6. } 7. flow.buffer(Channel.CONFLATED).collect { 8. delay(100) 9. print("in $it") 10. } -> out 0, out 1, out 2, out 3, out 4, out 5, in 0, in 5

Slide 56

Slide 56 text

// KLUG - Kotlin Flows Flow Internals - Cold vs Hot

Slide 57

Slide 57 text

// KLUG - Kotlin Flows Flow Internals - Cold vs Hot 1. val flow = flow { 2. print("emitting") 3. emit(1) 4. emit(2) 5. } 6. 7. delay(100) 8. 9. print("collecting") 10. flow.collect { 11. print(it) 12. } -> collecting, emitting, 1, 2 Cold

Slide 58

Slide 58 text

// KLUG - Kotlin Flows Flow Internals - Cold vs Hot 1. val channel = flow { 2. print("emitting") 3. emit(1) 4. emit(2) 5. }.produceIn(scope) 6. 7. delay(100) 8. 9. print("collecting") 10. for (value in channel) { 11. print(value) 12. } -> emitting, collecting, 1, 2 HOT Convert to channel

Slide 59

Slide 59 text

// KLUG - Kotlin Flows Extending Flows When it is just not enough.

Slide 60

Slide 60 text

// KLUG - Kotlin Flows Extending Flows - take() example 1. public fun Flow.take(count: Int) = flow { 2. var consumed = 0 3. try { 4. collect { value -> 5. emit(value) 6. if (++consumed == count) { 7. throw AbortFlowException() 8. } 9. } 10. } catch (e: AbortFlowException) { 11. // Nothing, bail out 12. } 13. }

Slide 61

Slide 61 text

// KLUG - Kotlin Flows Extending Flows - AndroidX Room @Dao interface UsersDao { @Query("SELECT * FROM users WHERE id == :id") suspend fun getById(id: String): UserRecord @Query("SELECT * FROM users WHERE id == :id") fun rxTrackById(id: String): Flowable @Query("SELECT * FROM users WHERE id == :id") fun flowTrackById(id: String): Flow }

Slide 62

Slide 62 text

// KLUG - Kotlin Flows Extending Flows - AndroidX Room 1. flow { 2. val observerChannel = Channel(Channel.CONFLATED) 3. val observer = object : InvalidationTracker.Observer(tableNames) { 4. override fun onInvalidated(tables: MutableSet) { 5. observerChannel.offer(Unit) 6. } 7. } 8. observerChannel.offer(Unit) 9. 10. val flowContext = coroutineContext 11. withContext(coroutineContext) { 12. db.invalidationTracker.addObserver(observer) 13. try { 14. for (signal in observerChannel) { 15. val result = performQuery() 16. withContext(flowContext) { emit(result) } 17. } 18. } finally { 19. db.invalidationTracker.removeObserver(observer) 20. } 21. } 22. } < create channel < notify on change < signal initial query < add listener < query table < ensure context & emit < release listener

Slide 63

Slide 63 text

// KLUG - Kotlin Flows Extending Flows - Interval flow 1. val flow = flow { 2. while (true) { 3. emit(Unit) 4. delay(delay) 5. } 6. } 1. flow.collect { 2. print("trigger") 3. } > trigger, [delay], trigger, [delay], trigger, [delay], ...

Slide 64

Slide 64 text

// KLUG - Kotlin Flows Extending Flows - Observable to Flow 1. fun Observable.toFlow() = channelFlow { 2. val disposable = subscribeWith(object : DisposableObserver() { 3. override fun onNext(t: T) { offer(t) } 4. override fun onComplete() { close() } 5. override fun onError(e: Throwable) { close(e) } 6. }) 7. awaitClose { disposable.dispose() } 8. } 1. Observable.fromArray(1, 2, 3).toFlow() 2. .collect { 3. print(it) 4. } -> 1, 2, 3 < send next value < close channel < crash channel < release Observable

Slide 65

Slide 65 text

// KLUG - Kotlin Flows Extending Flows - Log Flow Lifecycle 1. fun Flow.logLifecycle() = flow { 2. try { 3. print("flow start") 4. collect { 5. print("flow emit") 6. emit(it) 7. } 8. print("flow finish") 9. } catch (e: Exception) { 10. print("flow error") 11. throw e 12. } 13. } 1. flowOf(1, 2, 3) 2. .logLifecycle() 3. .collect { 4. print(it) 5. } -> flow start, flow emit, 1, flow emit, 2, flow emit, 3, flow finish

Slide 66

Slide 66 text

// KLUG - Kotlin Flows Benchmarks All is great but can it run Cr… I mean Flows?

Slide 67

Slide 67 text

// KLUG - Kotlin Flows Benchmarks - Flow vs Rx - Emit 1. flow { 2. for (value in 0..count) { 3. emit("") 4. } 5. }.collect() 1. Observable.create { 2. for (value in 0..count) { 3. it.onNext("") 4. } 5. it.onComplete() 6. }.blockingSubscribe()

Slide 68

Slide 68 text

// KLUG - Kotlin Flows Benchmarks - Flow vs Rx - Emit ~4x slower ~5x slower ~1.3x slower ~1.7x slower

Slide 69

Slide 69 text

// KLUG - Kotlin Flows Benchmarks - Flow vs Rx - Cancel flowOf("", "").collect { throw Exception() } flowOf("", "").collect { throw exception } flowOf("", "").collect { coroutineContext[Job]?.cancel() } 1. Observable.fromArray( "", "") 2. .subscribe( object : DisposableObserver() { 3. override fun onComplete() {} 4. override fun onNext(t: String) = dispose() 5. override fun onError(e: Throwable) {} 6. }) “Correct” - throw exception (still need to catch exception) Bad - throw cached exception (exception has wrong stack trace) Bad - cancel job (but it also cancels parent job)

Slide 70

Slide 70 text

// KLUG - Kotlin Flows Benchmarks - Flow vs Rx - Cancel ~49x slower ~9x slower ~14x slower

Slide 71

Slide 71 text

// KLUG - Kotlin Flows Benchmarks - Why Job.cancel is faster? internal class JobCancellationException constructor(...) : CancellationException(...), CopyableThrowable { // ... override fun fillInStackTrace(): Throwable { if (DEBUG) { return super.fillInStackTrace() } /* * In non-debug mode we don't want to have a stacktrace * on every cancellation, parent job reference is enough. * Stacktrace of JCE is not needed most of the time and hurts performance. */ return this } // ... }

Slide 72

Slide 72 text

// KLUG - Kotlin Flows Benchmarks - Flow vs Rx Benchmark Mode Cnt Score Error Units Benchmarks.flow_emit_1 thrpt 5 3652247.139 ± 16668.981 ops/s Benchmarks.flow_emit_10 thrpt 5 3281303.599 ± 42834.687 ops/s Benchmarks.flow_emit_1000 thrpt 5 366228.664 ± 8101.192 ops/s Benchmarks.flow_emit_1_000_000 thrpt 5 368.609 ± 2.751 ops/s Benchmarks.rx_emit_1 thrpt 5 19697204.475 ± 415523.489 ops/s Benchmarks.rx_emit_10 thrpt 5 19168316.124 ± 446412.865 ops/s Benchmarks.rx_emit_1000 thrpt 5 460720.252 ± 3492.123 ops/s Benchmarks.rx_emit_1_000_000 thrpt 5 627.488 ± 2.768 ops/s Benchmarks.flow_cancel thrpt 5 656798.927 ± 173212.926 ops/s Benchmarks.flow_cancel_cacheException thrpt 5 3658436.373 ± 696998.489 ops/s Benchmarks.flow_cancel_cancelJob thrpt 5 2363809.629 ± 408097.912 ops/s Benchmarks.rx_cancel thrpt 5 32290853.094 ± 8736309.342 ops/s

Slide 73

Slide 73 text

// KLUG - Kotlin Flows Conclusion Just some thoughts about all of this greatness

Slide 74

Slide 74 text

// KLUG - Kotlin Flows Conclusion Pros: ● Built-in suspend functions support ● No need to handle cancellation ● Easy to write simple extensions Cons: ● Much bigger overhead -> much slower (especially for cancellation) ● Requires scope to call terminal function ● More edge-cases (for ex. context preservation, exception transparency) ● Most interesting functions are marked as FlowPreview/ExperimentalCoroutinesApi

Slide 75

Slide 75 text

More info: 1. Always read sources, a lot of interesting stuff there :) 2. https://kotlinlang.org/docs/reference/coroutines/flow.html

Slide 76

Slide 76 text

// Q&A