Slide 1

Slide 1 text

Channels & Flows 
 in Practice Mohit Sarveiya /heyitsmohit twitter.com

Slide 2

Slide 2 text

Channels & Flows in Practice • Channels • Inside Channels • Flows • Implementing & Testing Polling

Slide 3

Slide 3 text

Coroutine A Problem Coroutine B

Slide 4

Slide 4 text

Coroutine A Problem Coroutine B 1 2 3

Slide 5

Slide 5 text

Passing Data • Deferred • Channels • Flows

Slide 6

Slide 6 text

Channels

Slide 7

Slide 7 text

What is a Channel? Item Channel
 
 Buffer (optional) Item

Slide 8

Slide 8 text

What is a Channel? interface Channel : SendChannel, ReceiveChannel interface ReceiveChannel { suspend fun receive(): E } interface SendChannel { suspend fun send(element: E) }

Slide 9

Slide 9 text

Channel Types • Rendezvous • Buffered • Unlimited • Conflated • Broadcast Channel

Slide 10

Slide 10 text

Creating Channel fun Channel(capacity: Int = RENDEZVOUS) = when (capacity) { RENDEZVOUS "-> RendezvousChannel() UNLIMITED "-> LinkedListChannel() CONFLATED "-> Con"flatedChannel() BUFFERED "-> ArrayChannel(CHANNEL_DEFAULT_CAPACITY) else "-> ArrayChannel(capacity) }

Slide 11

Slide 11 text

Using Channels runBlocking {
 val channel = Channel() launch { channel.send(1) } val num = channel.receive() } Consumer Producer

Slide 12

Slide 12 text

Rendezvous Channel • No Buffer • Waits for receiver to send data

Slide 13

Slide 13 text

Rendezvous Channel runBlocking { val channel = Channel() launch { channel.send(1) channel.send(2) } }

Slide 14

Slide 14 text

Rendezvous Channel runBlocking { val channel = Channel() launch { channel.send(1) channel.send(2) } } SUSPEND

Slide 15

Slide 15 text

Rendezvous Channel runBlocking { val channel = Channel() launch { channel.send(1) channel.send(2) }
 val num = channel.receive() } Receiver

Slide 16

Slide 16 text

runBlocking { val channel = Channel() launch { channel.send(1) channel.send(2) }
 val num = channel.receive() } Rendezvous Channel RESUME Receive value 1

Slide 17

Slide 17 text

runBlocking { val channel = Channel() launch { channel.send(1) channel.send(2) }
 val num = channel.receive() } Rendezvous Channel SUSPEND Received value 1

Slide 18

Slide 18 text

Buffered Channel • Buffer with specific capacity. • Caches sent items in Array

Slide 19

Slide 19 text

runBlocking { val channel = Channel(capacity = 3) launch { channel.send(1) channel.send(2) channel.send(3) } } Buffered Channel 1 2 3 Buffer

Slide 20

Slide 20 text

runBlocking { val channel = Channel(capacity = 3) launch { channel.send(1) channel.send(2) channel.send(3)
 channel.send(4) } } Buffered Channel SUSPEND 1 2 3 Buffer

Slide 21

Slide 21 text

Unlimited Channel runBlocking { val channel = Channel(Channel.UNLIMITED) launch { channel.send(1) channel.send(2) channel.send(3) } }

Slide 22

Slide 22 text

Broadcast Channel Coroutine A Coroutine B Coroutine C Coroutine D

Slide 23

Slide 23 text

Broadcast Channel • Buffered • Unlimited • Conflated

Slide 24

Slide 24 text

Broadcast Channel runBlocking { val channel = broadcast(capacity = 10, block = { send(1) }) val consumer1 = channel.openSubscription() val consumer2 = channel.openSubscription() launch { println(consumer1.receive()) "// 1 println(consumer2.receive()) "// 1 }

Slide 25

Slide 25 text

Creating Producers runBlocking {
 val channel = Channel() launch { for (i in 1"..5) { channel.send(i) } channel.close() } } Producer

Slide 26

Slide 26 text

Producer Builder • Creates Coroutine with Channel • DSL for creating channels

Slide 27

Slide 27 text

Producer Builder fun CoroutineScope.produce( context: CoroutineContext, capacity: Int = 0, block: suspend ProducerScope.() "-> Unit ): ReceiveChannel

Slide 28

Slide 28 text

Creating Producers suspend fun CoroutineScope.producer(): ReceiveChannel = produce { for (i in 1"..5) { send(i) } close() }

Slide 29

Slide 29 text

Creating Producers runBlocking {
 val channel: ReceiveChannel = producer() }
 suspend fun CoroutineScope.producer(): ReceiveChannel = produce { for (i in 1"..5) { send(i) } close() }

Slide 30

Slide 30 text

Consuming Producers runBlocking {
 val channel: ReceiveChannel = producer() for (num in channel) { println(num) } }
 suspend fun CoroutineScope.producer(): ReceiveChannel = produce { for (i in 1"..5) { send(i) } close() }

Slide 31

Slide 31 text

Inside Channels

Slide 32

Slide 32 text

Inside Channels

Slide 33

Slide 33 text

Inside Channels 
 val channel = Channel() What happens under the hood?

Slide 34

Slide 34 text

Inside Channels • Queue (Doubly Linked List) • Buffer (Array)

Slide 35

Slide 35 text

Channel States • Empty Queue • Send Queued • Send Buffered • Closed

Slide 36

Slide 36 text

Channel States EmptyQueue LockFreeLinkedListNode SendBuffered SendQueued Closed

Slide 37

Slide 37 text

Query Channel State • Get most recent state • Size of buffer

Slide 38

Slide 38 text

class AbstractSendChannel { fun toString() = {$queueDebugStateString}$bufferDebugString” } Query Channel State Current Channel State Buffer Size

Slide 39

Slide 39 text

Rendezvous Channel val channel = Channel() EmptyQueue Queue

Slide 40

Slide 40 text

Rendezvous Channel val channel = Channel() channel.send(1)

Slide 41

Slide 41 text

Rendezvous Channel class SendElement( val data: Any?, val cont: CancellableContinuation ) : LockFreeLinkedListNode()

Slide 42

Slide 42 text

Rendezvous Channel val channel = Channel() channel.send(1) SendElement(1) Queue

Slide 43

Slide 43 text

Rendezvous Channel val channel = Channel() channel.send(1)
 println(channel)
 
 Output: 
 RendezvousChannel{SendQueued}

Slide 44

Slide 44 text

Rendezvous Channel val channel = Channel() channel.send(1)
 channel.receive() EmptyQueue Queue

Slide 45

Slide 45 text

Unlimited Channel runBlocking { val channel = Channel(Channel.UNLIMITED) launch { channel.send(1) channel.send(2) channel.send(3) } } SendBuffered(1) SendBuffered(2) SendBuffered(3)

Slide 46

Slide 46 text

Consuming Values runBlocking {
 val channel: ReceiveChannel = producer() for (num in channel) { println(num) } }

Slide 47

Slide 47 text

Coroutine A Consuming Values Coroutine B 1 2 3 x Closed token

Slide 48

Slide 48 text

Channel Iterator interface ChannelIterator { suspend fun hasNextSuspend(): E fun next(): E }

Slide 49

Slide 49 text

Channel Iterator fun hasNextSuspend() = suspendCancellable { cont "-> while (true) { val result = channel.pollInternal() this.result = result if (result is Closed""<*>) {
 cont.resume(false) } else { cont.resume(true) } …
 } }

Slide 50

Slide 50 text

Coroutine A Channel Iterator Coroutine B EmptyQueue Channel Queue

Slide 51

Slide 51 text

Coroutine A Channel Iterator Coroutine B 1 SendElement(1) Channel Queue hasNext()

Slide 52

Slide 52 text

Coroutine A Channel Iterator Coroutine B 1 SendElement(1) Channel Queue next()

Slide 53

Slide 53 text

Coroutine A Channel Iterator Coroutine B 1 SendElement(1) Channel Queue next()

Slide 54

Slide 54 text

Coroutine A Channel Iterator Coroutine B 1 SendElement(3) Channel Queue next() 2 3

Slide 55

Slide 55 text

Coroutine A Channel Iterator Coroutine B 1 Closed Channel Queue 2 3 x Closed token

Slide 56

Slide 56 text

Coroutine A Channel Iterator Coroutine B 1 2 3 Suspended No Closed token

Slide 57

Slide 57 text

Flows

Slide 58

Slide 58 text

Coroutine A Cold Stream Coroutine B

Slide 59

Slide 59 text

Coroutine A Cold Stream Coroutine B

Slide 60

Slide 60 text

Coroutine A Cold Stream Coroutine B 1 2 3

Slide 61

Slide 61 text

Creating Flows • flowOf(…) • flow { … } • channelFlow { … }

Slide 62

Slide 62 text

Creating Flows runBlocking { val "flow: "Flow = "flowOf(1, 2, 3) } fun "flowOf(vararg elements: T): "Flow

Slide 63

Slide 63 text

Collecting Flows runBlocking { val "flow: "Flow = "flowOf(1, 2, 3) "flow.collect { println(it) "// 1 2 3 } }

Slide 64

Slide 64 text

Creating Flows val "flow = "flow { emit(1) emit(2) emit(3) } "flow.collect { println(it) "// 1 2 3 }

Slide 65

Slide 65 text

Creating Flows val "flow = "flow { emit(1) emit(2) emit(3) } "flow.collect { println(it) } "// 1 2 3 "flow.collect { println(it) } "// 1 2 3

Slide 66

Slide 66 text

Terminal Operators • Collect • Single • Reduce • toList

Slide 67

Slide 67 text

Single Operator Collects only one value. val "flow = "flowOf(1) val num: Int = "flow.single()

Slide 68

Slide 68 text

Single Operator Collects only one value. val "flow = "flowOf(1,2,3) val num: Int = "flow.single() java.lang.IllegalStateException: Expected only one element

Slide 69

Slide 69 text

Flow Operators • Map, Zip • Buffer • Retry • Take • delayEach

Slide 70

Slide 70 text

DelayEach runBlocking { val "flow = "flowOf(1,2,3).delayEach(1000) "flow.collect { println(it) } } Operators are not suspending functions

Slide 71

Slide 71 text

DelayEach runBlocking { val "flow = get"Flow() "flow.collect { println(it) } } fun get"Flow() = "flowOf(1,2,3).delayEach(1000)

Slide 72

Slide 72 text

Flow Context runBlocking { val "flow = "flow { emit(1) emit(2) emit(3) } "flow.collect { println(it) } } Which dispatcher does this run on?

Slide 73

Slide 73 text

Flow Context runBlocking { val "flow = "flow { emit(1) emit(2) emit(3) } "flow.collect { println(it) } } BlockingEventLoop BlockingEventLoop

Slide 74

Slide 74 text

Flow Context runBlocking { val "flow = "flow { emit(1) emit(2) emit(3) }
 "flow ."flowOn(Dispatchers.IO) .collect { println(it) } } Dispatchers.IO BlockingEventLoop

Slide 75

Slide 75 text

Flow Constraints • Context Preservation • Exception Handling

Slide 76

Slide 76 text

Flow Constraints Emit from the same coroutine val "flow = "flow { launch(Dispatchers.IO) { emit(1) } } Flow invariant is violated: emission from another coroutine is detected

Slide 77

Slide 77 text

Channel Flow Builder val "flow = channel"Flow { launch(Dispatchers.IO) { send(1) } launch(Dispatchers.IO) { send(2) } } Flow Channel

Slide 78

Slide 78 text

Channel Flow Builder class Channel"FlowBuilder( val block: suspend ProducerScope.() "-> Unit, capacity: Int = BUFFERED ) : Channel"Flow(context, capacity) Uses Buffered Channel

Slide 79

Slide 79 text

Exception Handling val "flow = "flow { try { makeRequest() } catch (e: IOException) { } }

Slide 80

Slide 80 text

Exception Handling fun "Flow.catch(action: suspend "FlowCollector.(cause: Throwable) "-> Unit ): "Flow Emit exception in a wrapper

Slide 81

Slide 81 text

Exception Handling sealed class ApiResponse { data class Success(val data: T): ApiResponse() data class Error(val t: Throwable): ApiResponse() }

Slide 82

Slide 82 text

Exception Handling val "flow = "flow> { val data = makeRequest() emit(ApiResponse.Success(data)) }.catch { emit(ApiResponse.Error(it)) }

Slide 83

Slide 83 text

Channel to Flow Bridge runBlocking { val bufferedChannel = Channel(capacity = 10) val broadcastChannel = broadcast(capacity = 10) { send(1) } val "flow1: "Flow = bufferedChannel.consumeAs"Flow() val "flow2 = broadcastChannel.as"Flow() }

Slide 84

Slide 84 text

Use Case: Polling

Slide 85

Slide 85 text

Polling 1 second /feed Data

Slide 86

Slide 86 text

Polling interface ApiService { suspend fun getData(): Data }

Slide 87

Slide 87 text

Polling interface Poller { fun poll(delay: Long): "Flow fun close() }

Slide 88

Slide 88 text

Polling class CoroutinePoller( val apiService: ApiService, val dispatcher: CoroutineDispatcher ): Poller { override fun poll(delay: Long): "Flow { } override fun close() { } }

Slide 89

Slide 89 text

Polling class CoroutinePoller(…): Poller { override fun poll(delay: Long): "Flow { return channel"Flow { while (!isClosedForSend) { val data = apiService.getData() send(data) delay(delay) } }."flowOn(dispatcher) } }

Slide 90

Slide 90 text

Polling class CoroutinePoller("..): Poller { override fun cancel() { dispatcher.cancel() } }

Slide 91

Slide 91 text

Polling Usage class MyViewModel(val poller: Poller): ViewModel { fun startPolling() { viewModelScope.launch { val "flow: "Flow = poller.poll(1000) "flow.collect { data !-> processData(data) } } } }

Slide 92

Slide 92 text

Testing Strategy Create coroutine for test • Run blocking Coroutine
 • Coroutine with test dispatcher and scope

Slide 93

Slide 93 text

Testing

Slide 94

Slide 94 text

Testing • Dispatchers.setMain • runBlockingTest • TestCoroutineDispatcher • advanceTimeBy, pauseDispatcher, resumeDispatcher

Slide 95

Slide 95 text

Testing

Slide 96

Slide 96 text

Testing Strategy • Mock API Service • Advance time every second • Assert on Flow

Slide 97

Slide 97 text

Testing @Test fun `should poll every second`() = runBlockingTest { val data = mock() val apiService = mock() { onBlocking { getData() } doReturn data } val testDispatcher = TestCoroutineDispatcher() val poller = CoroutinePoller(apiService, testDispatcher) }

Slide 98

Slide 98 text

Testing @Test fun `should poll every second`() = runBlockingTest { val "flow = poller.poll(1_000) launch { val dataList = "flow.take(2).toList() assertEquals(dataList.size, 2) } testDispatcher.advanceTimeBy(2_000) poller.close() }

Slide 99

Slide 99 text

Testing

Slide 100

Slide 100 text

Flow Assert suspend fun "Flow.test( validate: suspend "FlowAssert.() "-> Unit ) { }

Slide 101

Slide 101 text

Flow Assert sealed class Event { object Complete : Event() data class Error(val t: Throwable) : Event() data class Item(val item: T) : Event() }

Slide 102

Slide 102 text

Flow Assert suspend fun "Flow.test( validate: suspend "FlowAssert.() "-> Unit ) { coroutineScope { val events = Channel>(UNLIMITED) launch { try { collect { item !-> events.send(Event.Item(item)) } Event.Complete } }

Slide 103

Slide 103 text

Flow Assert suspend fun "Flow.test( validate: suspend "FlowAssert.() "-> Unit ) { coroutineScope { val events = Channel>(UNLIMITED) launch { try {
 … } catch (t: Throwable) { Event.Error(t) } if (terminalEvent "!= null) { events.send(terminalEvent) } events.close()

Slide 104

Slide 104 text

Flow Assert class FlowAssert(val events: Channel>) { suspend fun expectItem(): T suspend fun expectComplete() suspend fun expectError(): Throwable … }

Slide 105

Slide 105 text

Flow Assert @Test fun `should poll every second`() = runBlockingTest { val "flow = poller.poll(1_000) launch { "flow.test { assertEquals(expectItem(), data) assertEquals(expectItem(), data) expectComplete() } } testDispatcher.advanceTimeBy(2_000) poller.close()

Slide 106

Slide 106 text

Resources • Kotlinx-coroutines-test
 https://github.com/Kotlin/kotlinx.coroutines/tree/master/kotlinx-coroutines-test • Coroutine JUnit Test Rule
 https://craigrussell.io/2019/11/unit-testing-coroutine-suspend-functions-using-testcoroutinedispatcher/ • Corbind 
 https://github.com/LDRAlighieri/Corbind • FlowAssert 
 
 https://github.com/cashapp/sqldelight/blob/master/extensions/coroutines-extensions/src 
 commonTest/kotlin/com/squareup/sqldelight/runtime/coroutines/FlowAssert.kt

Slide 107

Slide 107 text

Thank you! twitter.com/heyitsmohit