Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Channels & Flows in Practice

Mohit S
January 16, 2020

Channels & Flows in Practice

In this talk, Mohit will present on how to use constructs such as channels and flows in various use cases. These use cases are modeling streams that are hot or cold, multicasting, polling, handling errors and testing. A Channel is similar to a blocking queue or a subject from RxJava. There are different types of channels that fit different use cases. On the other hand, a Flow represents a cold stream. We’ll look at how they work and thier APIs for consuming and producing a stream. I also cover how to go by testing your code using the coroutines testing library that is provided. I’ll share various helper extensions that you could use to improve your testing. By the end of this talk, you will have expanded your toolset for solving problems using Coroutines.

Mohit S

January 16, 2020
Tweet

More Decks by Mohit S

Other Decks in Programming

Transcript

  1. Channels & Flows in Practice • Channels • Inside Channels

    • Flows • Implementing & Testing Polling
  2. What is a Channel? interface Channel<E> : SendChannel<E>, ReceiveChannel<E> interface

    ReceiveChannel<out E> { suspend fun receive(): E } interface SendChannel<in E> { suspend fun send(element: E) }
  3. Creating Channel fun <E> Channel(capacity: Int = RENDEZVOUS) = when

    (capacity) { RENDEZVOUS "-> RendezvousChannel() UNLIMITED "-> LinkedListChannel() CONFLATED "-> Con"flatedChannel() BUFFERED "-> ArrayChannel(CHANNEL_DEFAULT_CAPACITY) else "-> ArrayChannel(capacity) }
  4. Using Channels runBlocking {
 val channel = Channel<Int>() launch {

    channel.send(1) } val num = channel.receive() } Consumer Producer
  5. Rendezvous Channel runBlocking { val channel = Channel<Int>() launch {

    channel.send(1) channel.send(2) }
 val num = channel.receive() } Receiver
  6. runBlocking { val channel = Channel<Int>() launch { channel.send(1) channel.send(2)

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

    }
 val num = channel.receive() } Rendezvous Channel SUSPEND Received value 1
  8. runBlocking { val channel = Channel<Int>(capacity = 3) launch {

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

    channel.send(1) channel.send(2) channel.send(3)
 channel.send(4) } } Buffered Channel SUSPEND 1 2 3 Buffer
  10. Broadcast Channel runBlocking { val channel = broadcast<Int>(capacity = 10,

    block = { send(1) }) val consumer1 = channel.openSubscription() val consumer2 = channel.openSubscription() launch { println(consumer1.receive()) "// 1 println(consumer2.receive()) "// 1 }
  11. Creating Producers runBlocking {
 val channel = Channel<Int>() launch {

    for (i in 1"..5) { channel.send(i) } channel.close() } } Producer
  12. Producer Builder fun <E> CoroutineScope.produce( context: CoroutineContext, capacity: Int =

    0, block: suspend ProducerScope<E>.() "-> Unit ): ReceiveChannel<E>
  13. Creating Producers runBlocking {
 val channel: ReceiveChannel<Int> = producer() }


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

    (num in channel) { println(num) } }
 suspend fun CoroutineScope.producer(): ReceiveChannel<Int> = produce { for (i in 1"..5) { send(i) } close() }
  15. Unlimited Channel runBlocking { val channel = Channel<Int>(Channel.UNLIMITED) launch {

    channel.send(1) channel.send(2) channel.send(3) } } SendBuffered(1) SendBuffered(2) SendBuffered(3)
  16. 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) } …
 } }
  17. Creating Flows runBlocking { val "flow: "Flow<Int> = "flowOf(1, 2,

    3) } fun <T> "flowOf(vararg elements: T): "Flow<T>
  18. Collecting Flows runBlocking { val "flow: "Flow<Int> = "flowOf(1, 2,

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

    } "flow.collect { println(it) "// 1 2 3 }
  20. 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
  21. Single Operator Collects only one value. val "flow = "flowOf(1,2,3)

    val num: Int = "flow.single() java.lang.IllegalStateException: Expected only one element
  22. DelayEach runBlocking { val "flow = get"Flow() "flow.collect { println(it)

    } } fun get"Flow() = "flowOf(1,2,3).delayEach(1000)
  23. Flow Context runBlocking { val "flow = "flow { emit(1)

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

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

    emit(2) emit(3) }
 "flow ."flowOn(Dispatchers.IO) .collect { println(it) } } Dispatchers.IO BlockingEventLoop
  26. 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
  27. Channel Flow Builder val "flow = channel"Flow { launch(Dispatchers.IO) {

    send(1) } launch(Dispatchers.IO) { send(2) } } Flow Channel
  28. Channel Flow Builder class Channel"FlowBuilder<T>( val block: suspend ProducerScope<T>.() "->

    Unit, capacity: Int = BUFFERED ) : Channel"Flow<T>(context, capacity) Uses Buffered Channel
  29. Exception Handling sealed class ApiResponse<T> { data class Success<T>(val data:

    T): ApiResponse<T>() data class Error<T>(val t: Throwable): ApiResponse<T>() }
  30. Exception Handling val "flow = "flow<ApiResponse<Data">> { val data =

    makeRequest() emit(ApiResponse.Success(data)) }.catch { emit(ApiResponse.Error(it)) }
  31. Channel to Flow Bridge runBlocking { val bufferedChannel = Channel<Int>(capacity

    = 10) val broadcastChannel = broadcast(capacity = 10) { send(1) } val "flow1: "Flow<Int> = bufferedChannel.consumeAs"Flow() val "flow2 = broadcastChannel.as"Flow() }
  32. Polling class CoroutinePoller( val apiService: ApiService, val dispatcher: CoroutineDispatcher ):

    Poller { override fun poll(delay: Long): "Flow<Data> { } override fun close() { } }
  33. Polling class CoroutinePoller(…): Poller { override fun poll(delay: Long): "Flow<Data>

    { return channel"Flow { while (!isClosedForSend) { val data = apiService.getData() send(data) delay(delay) } }."flowOn(dispatcher) } }
  34. Polling Usage class MyViewModel(val poller: Poller): ViewModel { fun startPolling()

    { viewModelScope.launch { val "flow: "Flow<Data> = poller.poll(1000) "flow.collect { data !-> processData(data) } } } }
  35. Testing @Test fun `should poll every second`() = runBlockingTest {

    val data = mock<Data>() val apiService = mock<ApiService>() { onBlocking { getData() } doReturn data } val testDispatcher = TestCoroutineDispatcher() val poller = CoroutinePoller(apiService, testDispatcher) }
  36. 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() }
  37. Flow Assert sealed class Event<out T> { object Complete :

    Event<Nothing>() data class Error(val t: Throwable) : Event<Nothing>() data class Item<T>(val item: T) : Event<T>() }
  38. Flow Assert suspend fun <T> "Flow<T>.test( validate: suspend "FlowAssert<T>.() "->

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

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

    T suspend fun expectComplete() suspend fun expectError(): Throwable … }
  41. 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()
  42. 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