Slide 1

Slide 1 text

Akshay Chordiya Go with the Flow Hey everyone, it’s super exciting to see people from India some familiar faces and even some people joining from other parts of the world. I hope everyone is coming up with creative ideas while at home and staying safe. Welcome and thank you for joining. I’m Akshay Chordiya, I’m based in Berlin, Germany; originally from Pune, India. I work as Android Developer at Clue which is a female health company. I’m also Google Developer Expert for Android. Today we are going to talk about something which many people are excited about i.e Kotlin Flow [moving to next slide]

Slide 2

Slide 2 text

Akshay Chordiya Go with the Flow The goal of this talk is to tell you why Kotlin Flow was designed, some basics, show what makes Flow special, how can you migrate to Flow, how to architect Flow on Android and most important testing

Slide 3

Slide 3 text

Coroutines Let’s rewind ⏪ a little bit Let’s rewind a little bit and look at what Kotlin gave us - coroutines. They simplify asynchronous programming in general and especially on Android

Slide 4

Slide 4 text

Synchronous code fun showUser() { val user = db.loadUser() // Blocks the thread render(user) } Here is an example of synchronous function called `showUser` which internally loads the user from the database.

Slide 5

Slide 5 text

Synchronous code fun showUser() { val user = db.loadUser() // Blocks the thread render(user) } loadUser When this function calls `loadUser` it blocks the current thread and waits till user is loaded from the database, this blocking is bad because at this moment the whole UI is frozen -> making the user unhappy

Slide 6

Slide 6 text

Synchronous code fun showUser() { val user = db.loadUser() // Blocks the thread render(user) } loadUser render And then once the data is loaded from database the `render` function is called which renders the user data on the UI. There are multiple ways to tackle this. Let’s look at how we can convert this function to use coroutine

Slide 7

Slide 7 text

With Coroutines fun showUser() { val user = db.loadUser() render(user) } Let’s start by taking the original synchronous function

Slide 8

Slide 8 text

With Coroutines suspend fun showUser() { val user = db.loadUser() // Background thread render(user) } First we need to mark the `showUser` function with suspend keyword, of course we will need to mark the `loadUser` as suspend too since we also want want it to run on background thread. You can see that just by marking it as suspend the code will run in background thread

Slide 9

Slide 9 text

With Coroutines suspend fun showUser() { val user = db.loadUser() // Background thread render(user) } But there is a problem the `render` function needs to be called from UI thread since its touching the view hierarchy.

Slide 10

Slide 10 text

With Coroutines suspend fun showUser() { val user = db.loadUser() // Background thread withContext(Dispatchers.Main) { render(user) // UI thread } } We can use the `withContext` function which allows to switch the thread inside the suspending function and change the thread to main and call `render` function, and that’s it!

Slide 11

Slide 11 text

With Coroutines suspend fun showUser() { val user = db.loadUser() // Background thread withContext(Dispatchers.Main) { render(user) // UI thread } } showUser This is how it will work, we start by calling the `showUser` function, since it’s marked as suspend it will switch to a background thread or the thread specified while launching the coroutine

Slide 12

Slide 12 text

With Coroutines suspend fun showUser() { val user = db.loadUser() // Background thread withContext(Dispatchers.Main) { render(user) // UI thread } } showUser loadUser and then the user will be loaded from the database on a different thread while the main thread continues to go ahead without waiting for anything i.e not freezing or blocking the user experience

Slide 13

Slide 13 text

With Coroutines suspend fun showUser() { val user = db.loadUser() // Background thread withContext(Dispatchers.Main) { render(user) // UI thread } } showUser loadUser render And then it will switch back to main thread because of `withContext` and then call the `render` function on the Main / UI thread

Slide 14

Slide 14 text

A suspend function can only be called from another suspend function One key thing to remember is a suspend function can only be called from another suspend function

Slide 15

Slide 15 text

A suspend function can only be called from another suspend function or a coroutine scope Or from a coroutine scope,

Slide 16

Slide 16 text

Coroutine scope A coroutine scope represents the scope of the coroutines, each coroutine needs a scope to run in and when the scope is destroyed all the running jobs inside the scope are also stopped or cancelled

Slide 17

Slide 17 text

Coroutine scope ~ Lifecycle scope This scope is generally associated to the lifecycle scope for simplicity. For instance, on Android each activity has a lifecycle. Let’s look at example to help understand this better

Slide 18

Slide 18 text

fun main() { lifecycleScope.launch { showUser() } } * available in lifecycle-ktx Jetpack library For example, in order to call our `showUser` suspend function from a normal function we would need a coroutine scope. On Android these scopes are already implemented for us like `lifecycleScope` which is part of `lifecycle-ktx` library from Jetpack components. it is provided to use coroutine function in Activity or Fragment. It’s a coroutine scope which is tied to the lifecycle of activity or fragment

Slide 19

Slide 19 text

fun main() { lifecycleScope.launch { showUser() } } CREATED DESTROYED This provides the magic of automatically clearing or cancelling all the jobs running in the specified scope when the activity or fragment is destroyed

Slide 20

Slide 20 text

fun main() { lifecycleScope.launch { showUser() } } The launch function is used to start the coroutine, it will immediately start the coroutine That means coroutines are hot in nature, this also one of the reason why coroutines are not replacement for reactive streams

Slide 21

Slide 21 text

“A coroutine scope is kinda a subscription management system associated to lifecycle ♻”

Slide 22

Slide 22 text

Structured Concurrency This concept is called as Structured Concurrency and this is what makes Kotlin coroutines so powerful and also helps developers prevent leaking the job / subscription

Slide 23

Slide 23 text

Why Kotlin Flow? Let’s come to the important question, why Kotlin Flow?

Slide 24

Slide 24 text

Why Kotlin Flow? Kotlin Coroutine With growing usage of Kotlin and the love for Kotlin coroutines, people expressed a lot of interest in having a pure Kotlin implementation of RxJava to leverage all the amazing features which Kotlin provides

Slide 25

Slide 25 text

RxKotlin or from scratch? But the confusion was creating some sort of magic on top of RxJava to leverage Kotlin or to build a whole new library from scratch. In case if you don’t know RxKotlin does exist but it cannot really leverage all the Kotlin features.

Slide 26

Slide 26 text

Me “At the end it’s important that any new library raises the bar compared to existing ones and is not just a replica with slight improvements” To me it’s important that any new library raises the bar compared to any existing libraries out there and is not just a replica with slight improvements. And Kotlin Flow does raise the bar

Slide 27

Slide 27 text

So What’s special So let’s look at what’s special in Kotlin Flow to raise the bar

Slide 28

Slide 28 text

What’s special ✨ Null safety in streams ✨ Interoperability between other reactive streams and coroutines ✨ Supports Kotlin multiplatform ✨ No special handling for back pressure ✨ Fewer and simple operators ✨ Perks of structured concurrency • Flow provides null safety in the stream that means you cannot send null values in the stream, this is not something new; RxJava 2 has it too • This is the key thing, Flow provides easy APIs to convert Flow to any other reactive stream like RxJava Flowable and the reverse. Also you can combine Flow and coroutines, we will see this later • You can actually use Kotlin coroutines and Flow on any of the supported Kotlin platforms like JVM, JS and native + use it the common code or shared code for Kotlin multiplatform projects • There is no special handling for back pressure, there are literally no operators for handling back pressure cause this is magically handled by the suspending nature of coroutines • There are fewer operators which directly correlates to ease of understanding, this is because of 2 reasons • Kotlin’s extension function magic • Suspending nature of coroutines allow having same operator for synchronous / asynchronous code which cuts down the number of operators •

Slide 29

Slide 29 text

What is Flow?

Slide 30

Slide 30 text

What is Flow? Based upon reactive stream specification Similar to a list / collection It has operators like map, switchMap, etc Built on top of coroutines It’s ❄ in nature

Slide 31

Slide 31 text

Basics Let’s look at some of the basics of Flow

Slide 32

Slide 32 text

fun stream(): Flow = flow { emit("") // Emits the value upstream ☝ emit("⚽") emit("") } Essentially there are 2 components i.e emitter and collector where emitter emits new values and collector receives the value. Here is an example of an emitter.

Slide 33

Slide 33 text

fun stream(): Flow = flow { emit("") // Emits the value upstream ☝ emit("⚽") emit("") } Which returns Flow of string very similar to how you would return a list of string

Slide 34

Slide 34 text

fun stream(): Flow = flow { emit("") // Emits the value upstream ☝ emit("⚽") emit("") } There are various ways of creating a flow using something called as flow builders (they are functions provided by Kotlin coroutines library). It internally creates the flow for us, we will come to it later.

Slide 35

Slide 35 text

fun stream(): Flow = flow { emit("") // Emits the value upstream ☝ emit("⚽") emit("") } ❄ ❄ ❄ ❄ ❄ Since Flow is cold, nothing inside the flow builder block will be called until there is a subscriber or a collector observing the flow

Slide 36

Slide 36 text

fun stream(): Flow = flow { emit("") // Emits the value upstream ☝ emit("⚽") emit("") } Emitter Collector fun main() = runBlocking { stream().collect { value -> println(value) } } Let’s look at the collector, here we have a main function which gets the flow inside the `runBlocking` function. For people who haven’t used coroutines; `runBlocking` function is a blocking coroutine scope i.e it blocks the current thread until everything is completed. We need the coroutine scope since the `collect` function is a suspend function, so is `emit`. We will see ahead why they are suspend functions

Slide 37

Slide 37 text

fun stream(): Flow = flow { emit("") // Emits the value upstream ☝ emit("⚽") emit("") } Emitter Collector fun main() = runBlocking { stream().collect { value -> println(value) } } main() stream() Let’s understand how things work

Slide 38

Slide 38 text

fun stream(): Flow = flow { emit("") // Emits the value upstream ☝ emit("⚽") emit("") } Emitter Collector fun main() = runBlocking { stream().collect { value -> println(value) } } main() stream() Initially the main function calls the stream function

Slide 39

Slide 39 text

fun stream(): Flow = flow { emit("") // Emits the value upstream ☝ emit("⚽") emit("") } Emitter Collector fun main() = runBlocking { stream().collect { value -> println(value) } } main() stream() Which immediately returns the flow

Slide 40

Slide 40 text

fun stream(): Flow = flow { emit("") // Emits the value upstream ☝ emit("⚽") emit("") } Emitter Collector fun main() = runBlocking { stream().collect { value -> println(value) } } main() stream() Which immediately returns the flow

Slide 41

Slide 41 text

fun stream(): Flow = flow { emit("") // Emits the value upstream ☝ emit("⚽") emit("") } Emitter Collector fun main() = runBlocking { stream().collect { value -> println(value) } } main() stream() collect Then we call the collect function on the flow, which essentially means we are subscribed to the emitter

Slide 42

Slide 42 text

fun stream(): Flow = flow { emit("") // Emits the value upstream ☝ emit("⚽") emit("") } Emitter Collector fun main() = runBlocking { stream().collect { value -> println(value) } } main() stream() collect emit That the emitter starts emitting the value sequentially

Slide 43

Slide 43 text

fun stream(): Flow = flow { emit("") // Emits the value upstream ☝ emit("⚽") emit("") } Emitter Collector fun main() = runBlocking { stream().collect { value -> println(value) } } main() stream() collect emit And the collector receives them and prints in this case

Slide 44

Slide 44 text

fun stream(): Flow = flow { emit("") // Emits the value upstream ☝ emit("⚽") emit("") } Emitter Collector fun main() = runBlocking { stream().collect { value -> println(value) } } main() stream() collect emit collect emit ⚽ And then the same loop happens emitting the next value

Slide 45

Slide 45 text

fun stream(): Flow = flow { emit("") // Emits the value upstream ☝ emit("⚽") emit("") } Emitter Collector fun main() = runBlocking { stream().collect { value -> println(value) } } main() stream() collect emit collect emit ⚽ ⚽

Slide 46

Slide 46 text

fun stream(): Flow = flow { emit("") // Emits the value upstream ☝ emit("⚽") emit("") } Emitter Collector fun main() = runBlocking { stream().collect { value -> println(value) } } main() stream() collect emit collect emit ⚽ ⚽ .. This goes on

Slide 47

Slide 47 text

fun stream(): Flow = flow { emit("") // Emits the value upstream ☝ emit("⚽") emit("") } Emitter Collector fun main() = runBlocking { stream().collect { value -> println(value) } } main() stream() collect emit collect emit ⚽ ⚽ collect .. Till the emitter has no values that’s when the execution moves out of collect and is complete

Slide 48

Slide 48 text

Let’s dive deeper Let’s dive a little deeper and see how this is working internally

Slide 49

Slide 49 text

public interface Flow { public suspend fun collect(collector: FlowCollector) } Collector As I mentioned before Flow is based upon 2 components a collector and emitter, essentially they are interfaces. The following represents the interface for collector which exposes the `collect` function

Slide 50

Slide 50 text

public interface Flow { public suspend fun collect(collector: FlowCollector) } Collector public interface FlowCollector { public suspend fun emit(value: T) } Emitter And the emitter interface which allows emitting values

Slide 51

Slide 51 text

public interface Flow { public suspend fun collect(collector: FlowCollector) } Collector public interface FlowCollector { public suspend fun emit(value: T) } Emitter The key thing to note over here is both the functions are suspend which allows the magic of interoperability with coroutines and also allows using the structured concurrency provided by coroutines. The funny thing is the whole internal mechanism of flow is essentially `emit` and `collect` function calling each other.

Slide 52

Slide 52 text

Flow ❤ Coroutines Flow loves coroutine also because it’s built on top of it

Slide 53

Slide 53 text

fun loadFromDatabase(): String { // Heavy operation of fetching from DB return "Very heavy " } Let’s say you have a function which loads some data from database, since this operation takes time to perform you’d ideally want to perform it in the background thread by marking it as suspend

Slide 54

Slide 54 text

suspend fun loadFromDatabase(): String { // Heavy operation of fetching from DB return "Very heavy " } And the funny thing is

Slide 55

Slide 55 text

suspend fun loadFromDatabase(): String { // Heavy operation of fetching from DB return "Very heavy " } fun stream() = flow { val value = loadFromDatabase() emit(value) } you can just directly call any coroutine function inside the `flow` block

Slide 56

Slide 56 text

suspend fun loadFromDatabase(): String { // Heavy operation of fetching from DB return "Very heavy " } fun stream() = flow { val value = loadFromDatabase() emit(value) } public fun flow(@BuilderInference block: suspend FlowCollector.() -> Unit): Flow That’s because the `flow` block accepts a suspending lambda and also because the both the collector and emitter are suspend functions which is just great!

Slide 57

Slide 57 text

Principles of Flow Let’s take a look at core ideology of Flow and how they make our life easier

Slide 58

Slide 58 text

fun main() = runBlocking { flow { emit("Context") } .flowOn(Dispatchers.IO) } With Flow you can define or switch the thread by using the operator called as `flowOn` which takes in a dispatcher which in this case is IO.

Slide 59

Slide 59 text

fun main() = runBlocking { flow { emit("Context") // IO thread } .flowOn(Dispatchers.IO) } ‐ This operator sets the specified this thread only upstream i.e anything above the `flowOn` operator will run on IO thread.

Slide 60

Slide 60 text

fun main() = runBlocking { flow { emit("Context") // IO thread } .flowOn(Dispatchers.IO) .map { "$it Preservation" } } The map operator below the `flowOn` will run on some different thread and not IO thread

Slide 61

Slide 61 text

Context Preservation This concept of preventing context from leaking is called as “Context Preservation” where the context is encapsulated and it’s never allowed to go downstream ⬇ . There is only one way to change the context of a flow

Slide 62

Slide 62 text

Observable.just("") .map{ /* IO thread */ } .subscribeOn(Schedulers.io()) .map{ /* IO thread */ } .subscribeOn(Schedulers.computation()) .map{ /* IO thread */ } RxJava example If you compare this to RxJava where it’s super confusing especially for beginners to understand which thread their code is going to run on. In the example above you can see that only the first `subscribeOn` is applied and other one is ignored

Slide 63

Slide 63 text

Observable.just("") .map{ /* IO thread */ } .subscribeOn(Schedulers.io()) .map{ /* IO thread */ } .subscribeOn(Schedulers.computation()) .map{ /* IO thread */ } .observeOn(Schedulers.computation()) .map { /* */ } RxJava example Combined with another operator to change thread i.e `observeOn` with different behaviour, it’s a facepalm moment

Slide 64

Slide 64 text

Exception Transparency This is the 2nd principle or ideology of Flow. Flow implementations never catch or handle exceptions that occur in downstream ⬇ flows, they catch only for upstream ⬆ ; this is again due to context preservation. There is only 1 operator to catch error which is the `catch` operators. Let’s look at an example

Slide 65

Slide 65 text

flow { val value = loadFromDatabase() emit(value) } Let’s take the previous example of a flow which tries to get the value from the database and emit. As you can imagine anything can happen while loading from database which might throw some sort of exception

Slide 66

Slide 66 text

flow { val value = loadFromDatabase() emit(value) } .catch { emit("Fallback value") } Very similar to try/catch block, flow has a `catch` operator which will catch the any exceptions thrown above

Slide 67

Slide 67 text

flow { try { val value = loadFromDatabase() emit(value) } catch (e: Exception) { emit("Fallback value") } } You can even use the classic try/catch block, it will behave exactly similar to the catch operator

Slide 68

Slide 68 text

flow { try { val value = loadFromDatabase() emit(value) } catch (e: Exception) { when (e) { is CancellationException -> throw e else -> emit("Fallback value") } } } I’d recommend to use the catch operator but there are can be use-cases. During those time just make sure to ignore the `CancellationException` because you don’t want to emit an error when your flow or coroutine was cancelled when the activity was closed or something. This is pretty how the catch operator works internally

Slide 69

Slide 69 text

flow { try { val value = loadFromDatabase() emit(value) } catch (e: IOException) { emit("Fallback value") } } Or you can specifically handle the exception you expect to see. For example, in case of database operation you’d specifically want to exception related to IO operation.

Slide 70

Slide 70 text

flow { val value = loadFromDatabase() emit(value) } .catch { emit("Fallback value") } .collect { } The value emitted from the catch is received in the collect, that means there is only a single place to handle all types of cases either success or failure which makes it super transparent to handle all the cases

Slide 71

Slide 71 text

sealed class State { object Success: State() object Failure: State() } fun main() = runBlocking { flow { emit(State.Success) } .catch { emit(State.Failure) } .collect { state -> when (state) { State.Success -> println("Success") State.Failure -> println("Failure") } } } Ex Example I’d recommend to toss in sealed classes so you always cover all your cases

Slide 72

Slide 72 text

“- Use suspend for one-shot operations like insert operation or a network operation - Use Flow to get stream of data like getting updates on data changes in the database.” It might be confusing to understand when to use suspend and when to use `Flow`.

Slide 73

Slide 73 text

“- Use suspend for one-shot operations like insert operation or a network operation - Use Flow to get stream of data like getting updates on data changes in the database.” The general rule of thumb is use `suspend` for one-shot operations like insert into database operation or a network operation.

Slide 74

Slide 74 text

“- Use suspend for one-shot operations like insert operation or a network operation - Use Flow to get stream of data like getting updates on data changes in the database.” And use Flow when there is stream of data involved, for example get data from the database and continuously listening for updates.

Slide 75

Slide 75 text

Legacy ➡ Flow This is one of the coolest thing and I love it a lot, Flow provides an amazing API to covert any legacy callback based API to Flow

Slide 76

Slide 76 text

fun updateOnDocumentChange(document: Document) { val listener = Document.ChangeListener { updateUI(it) } document.addChangeListener(listener) } Let’s say you have a function which has a listener which is triggered each time the document is updated.

Slide 77

Slide 77 text

fun updateOnDocumentChange(document: Document) { val listener = Document.ChangeListener { updateUI(it) } document.addChangeListener(listener) } removeChangeListener(listener) You need to make sure to remove this listener when the component is destroyed for example when activity is destroyed

Slide 78

Slide 78 text

fun Document.awaitChange(): Flow = callbackFlow { } Flow provides an API called as `callbackFlow`

Slide 79

Slide 79 text

fun Document.awaitChange(): Flow = callbackFlow { val listener = Document.ChangeListener { } addChangeListener(listener) } Where you can write the legacy listener aka callback

Slide 80

Slide 80 text

fun Document.awaitChange(): Flow = callbackFlow { val listener = Document.ChangeListener { offer(it) } addChangeListener(listener) } And offer which basically means emit this value each time the document is changed. But you also need to remove the listener at some point right?

Slide 81

Slide 81 text

fun Document.awaitChange(): Flow = callbackFlow { val listener = Document.ChangeListener { offer(it) } addChangeListener(listener) awaitClose { removeChangeListener(listener) } } You can use the function `awaitClose` which is called when the flow or coroutine is cancelled, where we can safely remove the listener

Slide 82

Slide 82 text

/** * Listen for changes in the [Document] and notify the * receivers about changes. */ fun Document.awaitChange(): Flow = callbackFlow { val listener = Document.ChangeListener { offer(it) } addChangeListener(listener) awaitClose { removeChangeListener(listener) } } Finally with code comments it looks something like this which a lot of people forget to write

Slide 83

Slide 83 text

Rx ➡ Flow Let’s look at how you can migrate from Rx to Flow, the goal here is to not tell you to go next moment and migrate your codebase to Flow [I mean that would be great for your own projects].

Slide 84

Slide 84 text

Reactive Stream Specification Flow Rx Flow and Rx follow the same reactive stream specification so in theory it’s very easy to convert data streams from Rx to Flow and the reverse

Slide 85

Slide 85 text

https://github.com/Kotlin/kotlinx.coroutines/tree/master/reactive/kotlinx-coroutines-rx2 fun flow(): Flow { return Observable.just("a") .asFlow() } There are two ways to convert, 1st is like refactor your Rx implementation to use flow but that’s time confusing and sometimes you just want to switch types so you can gradually move to flow. In that cases you can use the `asFlow` extension function to quick switch the type from `Flowable` / `Observable` to `Flow`

Slide 86

Slide 86 text

fun observable(): Observable { return flow { emit("a") }.asObservable() } https://github.com/Kotlin/kotlinx.coroutines/tree/master/reactive/kotlinx-coroutines-rx2 fun flow(): Flow { return Observable.just("a") .asFlow() } Same can be done to convert flow to observable or flowable when you need it

Slide 87

Slide 87 text

Rx Coroutines Single / Completable / Maybe suspend function Observable Flow Subject Channel Schedulers Dispatchers Disposables Scopes subscribe collect Terminology between Rx and Coroutine world Here’s a quick comparison of terminology between Rx and coroutine which I stole from Clue’s documentation. In my defence I created this table at Clue Use this chart as a reference when you are new to coroutines world or working on migrating from Rx to coroutines or reverse. Now that see, we pretty much covered all the things mentioned except channel.

Slide 88

Slide 88 text

Fun part Let’s look at something really fun

Slide 89

Slide 89 text

public fun ObservableSource.asFlow(): Flow = callbackFlow { val observer = object : Observer { override fun onComplete() { close() } override fun onSubscribe(d: Disposable) { .. } override fun onNext(t: T) { sendBlocking(t) } override fun onError(e: Throwable) { close(e) } } subscribe(observer) awaitClose { /* Dispose */ } } The `asFlow` function actually uses `callbackFlow` to convert observable to flow which is soooo cool

Slide 90

Slide 90 text

Testing Now to everyone’s favourite topic - testing Testing flow or coroutine is pretty easy

Slide 91

Slide 91 text

@RunWith(JUnit4::class) class TestingWithFlow { fun getFlow() = flow { emit("Bob the builder ") } } Let’s take a simple flow of string

Slide 92

Slide 92 text

@RunWith(JUnit4::class) class TestingWithFlow { fun getFlow() = flow { emit("Bob the builder ") } @Test fun testFlow() = runBlocking { } } And write the test function and use the `runBlocking` coroutine scope because we want our test to wait till the coroutine is completed

Slide 93

Slide 93 text

@RunWith(JUnit4::class) class TestingWithFlow { fun getFlow() = flow { emit("Bob the builder ") } @Test fun testFlow() = runBlocking { val expected = listOf("Bob the builder ") val actualList = getFlow().take(1).toList() // Assert the list assertEquals(expected, actualList) } } Like I said Flow is similar to a list or a collection

Slide 94

Slide 94 text

@RunWith(JUnit4::class) class TestingWithFlow { fun getFlow() = flow { emit("Bob the builder ") } @Test fun testFlow() = runBlocking { val expected = listOf("Bob the builder ") val actualList = getFlow().take(1).toList() // Assert the list assertEquals(expected, actualList) } } So you can use `take(1)` operator to pick the 1st value from the stream

Slide 95

Slide 95 text

@RunWith(JUnit4::class) class TestingWithFlow { fun getFlow() = flow { emit("Bob the builder ") } @Test fun testFlow() = runBlocking { val expected = listOf("Bob the builder ") val actualList = getFlow().take(1).toList() // Assert the list assertEquals(expected, actualList) } } and then convert the flow to list so you can easily assert it

Slide 96

Slide 96 text

/** * Asserts only the [expected] items by just taking that many from the stream */ suspend fun Flow.assertItems(vararg expected: T) { assertEquals(expected.toList(), this.take(expected.size).toList()) } Or use this extension function which I again stole from our Clue code

Slide 97

Slide 97 text

@RunWith(JUnit4::class) class TestingWithFlow { fun getFlow() = flow { emit("Bob the builder ") } @Test fun testFlow() = runBlocking { val expected = "Bob the builder " // Assert the list getFlow().assertItems(expected) } } With the extension function it would be something like this, much more cleaner

Slide 98

Slide 98 text

Conclusion

Slide 99

Slide 99 text

Conclusion • Revisited coroutines • Why Flow? And what makes it special? • Basics of Flow • Principles of Flow • Rx or Legacy code -> Flow • Testing • Pro-tips along the way Here’s a quick summary what you learned today - We revisited coroutines - We saw why Kotlin Flow came into the picture and what makes it special - We saw basics of Flow i.e the emitter and collector and how they work internally - Principles of Flow i.e context preservation and exception transparency and how they make our lives easy - then how to convert Rx or Legacy code to Flow - How you can test flows - And some pro-tips along the way

Slide 100

Slide 100 text

Me “Kotlin Flow raises the bar and introduces easy reactive streaming, that said it’s work in progress and lots of exciting things are coming sooooooon ” Final thoughts To conclude Kotlin Flow raises the bar and makes reactive programming easy, gives us great Kotlin features. That said it’s still work in progress and lots of exciting things are coming soooon.

Slide 101

Slide 101 text

Thank You