$30 off During Our Annual Pro Sale. View Details »

Dive into Async apps with Kotlin Coroutines

Adit Lal
November 02, 2019

Dive into Async apps with Kotlin Coroutines

This talk highlights the challenges underpinning some of the paradigms of asynchronous programming, in particular, the callback-based approach. The talk will address how Kotlin aims to solve this problem with coroutines by providing an asynchronous interface to the developer.

- Coroutines Generator API - Sequences or iterables in an asynchronous manner
- Coroutine Scopes and how to manage them
- How to use Coroutines to do network calls from the app.
- Actors, easing concurrency for Android Developers
- Sneak peek at channels, broadcast channel, flow
- Using Coroutines and Rx2 together

Adit Lal

November 02, 2019
Tweet

More Decks by Adit Lal

Other Decks in Programming

Transcript

  1. Dive into Async apps with
    Kotlin Coroutines

    View Slide

  2. @aditlal
    Adit Lal
    Product Engineer
    @rivuchakraborty
    Rivu Chakraborty
    Android Engineer

    View Slide

  3. Problem
    • Run multiple tasks
    • Async is easy doable

    View Slide

  4. Asynchronous
    • Threading
    • AsyncTask
    • Callbacks
    • Observable
    • Future/Promise
    • Coroutines

    View Slide

  5. Threads
    • Threads are expensive
    • occupies about 1-2 mb
    • Requires context switching
    • Thread management
    • Race conditions
    • Thread Pools

    View Slide

  6. Callbacks
    • Difficulty of nested callbacks
    • Callback Hell
    • Error Handling is a mess

    View Slide

  7. Future/Promise
    • Similar to callbacks
    • Composition model with chained calls
    • Complicated Exception handling
    • Returns Promise

    View Slide

  8. Reactive
    • Observable streams
    • Constant API across platforms
    • Great error handling

    View Slide

  9. Coroutines
    • Launch a long-running operation w/o blocking thread
    • Functions are now suspend-able
    • Platform independent

    View Slide

  10. Coroutines simplify asynchronous programming
    by providing possibility to write code
    in direct style (sequentially).

    View Slide

  11. ./fetch_data

    View Slide

  12. Blocking
    Thread
    Time
    Function A Blocked
    Blocked Function B

    View Slide

  13. Suspended fun
    Thread
    Time
    Function A
    Suspended
    Function B

    View Slide

  14. fun loadData() = GlobalScope.launch(uiContext) {
    showLoading()
    // ui thread
    val result = dataProvider.fetchData()
    // non ui thread, suspend until finished
    showText(result)
    // ui thread
    hideLoading()
    // ui thread
    }
    Suspend

    View Slide

  15. fun loadData() = GlobalScope.launch(uiContext) {
    showLoading()
    // ui thread
    val result = dataProvider.fetchData()
    // non ui thread, suspend until finished
    showText(result)
    // ui thread
    hideLoading()
    // ui thread
    }

    View Slide

  16. fun loadData() = GlobalScope.launch(uiContext) {
    showLoading()
    // ui thread
    val result = dataProvider.fetchData()
    // non ui thread, suspend until finished
    showText(result)
    // ui thread
    hideLoading()
    // ui thread
    }

    View Slide

  17. fun loadData() = GlobalScope.launch(uiContext) {
    showLoading()
    // ui thread
    val result = dataProvider.fetchData()
    // non ui thread, suspend until finished
    showText(result)
    // ui thread
    hideLoading()
    // ui thread
    }

    View Slide

  18. fun loadData() = GlobalScope.launch(uiContext) {
    showLoading()
    // ui thread
    val result = dataProvider.fetchData()
    // non ui thread, suspend until finished
    showText(result)
    // ui thread
    hideLoading()
    // ui thread
    }

    View Slide

  19. CoroutineScope
    CoroutineContext
    Coroutine Builder
    launch

    View Slide

  20. Context
    fun main() = runBlocking {
    println("My context is: $coroutineContext")
    }
    My context is: [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@73c6c3b2,
    BlockingEventLoop@48533e64]

    View Slide

  21. Scope
    interface CoroutineScope {
    val coroutineContext: CoroutineContext
    }

    View Slide

  22. ‘Dat Sequential
    suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) //some fancy magic
    return 13
    }
    suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) //some fancy magic again
    return 29
    }

    View Slide

  23. ‘Dat Sequential
    val time = measureTimeMillis {
    val one = doSomethingUsefulOne()
    val two = doSomethingUsefulTwo()
    println("The answer is ${one + two}")
    }
    println("Completed in $time ms")
    The answer is 42
    Completed in 2007 ms

    View Slide

  24. ‘Dat async
    val time = measureTimeMillis {
    val one = async(IO){ doSomethingUsefulOne() }
    val two = async(IO) { doSomethingUsefulTwo() }
    println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")
    The answer is 42
    Completed in 1014 ms

    View Slide

  25. Dispatcher
    To specify where the coroutines should run,
    Kotlin provides three Dispatchers you can use
    for thread dispatch.

    View Slide

  26. Dispatchers.Main
    Main thread on Android,
    interact with the UI and perform light work
    Great for :
    - Calling suspend functions
    - Call UI functions
    - Updating LiveData

    View Slide

  27. Dispatchers.IO
    Optimized for disk and network IO off the main thread
    Great for :
    - Database*
    - Reading/writing files
    - Networking$**

    View Slide

  28. Dispatchers.Default
    Optimized for CPU intensive work off the main thread
    Great for :
    - Sorting a list
    - Parsing JSON
    - DiffUtils

    View Slide

  29. Where thou Coroutines
    • One Shot request
    • Stream request

    View Slide

  30. class SearchViewModel(val repo: SearchRepository): ViewModel() {
    private val _sortedResults = MutableLiveData>()
    val sortedResults: LiveData> = _sortedResults
    fun onSortAscending() = sortSearchBy(sort = true)
    fun onSortDescending() = sortSearchBy(sort = false)
    fun sortSearchBy(sort: Boolean, query: String) {
    viewModelScope.launch {
    // suspend and resume make this database request main-safe
    // so our ViewModel doesn't need to worry about threading
    _sortedResults.value =
    repo.fetchSearchResults(query, sort)
    }
    }
    }

    View Slide

  31. class SearchViewModel(val repo: SearchRepository): ViewModel() {
    private val _sortedResults = MutableLiveData>()
    val sortedResults: LiveData> = _sortedResults
    fun onSortAscending() = sortSearchBy(ascending = true)
    fun onSortDescending() = sortSearchBy(ascending = false)
    fun sortSearchBy(sort: Boolean, query: String) {
    viewModelScope.launch {
    // suspend and resume make this database request main-safe
    // so our ViewModel doesn't need to worry about threading
    _sortedResults.value =
    repo.fetchSearchResults(query, sort)
    }
    }
    }

    View Slide

  32. class SearchViewModel(val repo: SearchRepository): ViewModel() {
    private val _sortedResults = MutableLiveData>()
    val sortedResults: LiveData> = _sortedResults
    fun onSortAscending() = sortSearchBy(ascending = true)
    fun onSortDescending() = sortSearchBy(ascending = false)
    fun sortSearchBy(sort: Boolean, query: String) {
    viewModelScope.launch {
    // suspend and resume make this database request main-safe
    // so our ViewModel doesn't need to worry about threading
    _sortedResults.value =
    repo.fetchSearchResults(query, sort)
    }
    }
    }

    View Slide

  33. class SearchViewModel(val repo: SearchRepository): ViewModel() {
    private val _sortedResults = MutableLiveData>()
    val sortedResults: LiveData> = _sortedResults
    fun onSortAscending() = sortSearchBy(ascending = true)
    fun onSortDescending() = sortSearchBy(ascending = false)
    fun sortSearchBy(sort: Boolean, query: String) {
    viewModelScope.launch {
    // suspend and resume make this database request main-safe
    // so our ViewModel doesn't need to worry about threading
    _sortedResults.value =
    repo.fetchSearchResults(query, sort)
    }
    }
    }

    View Slide

  34. As a general pattern,
    start coroutines in the ViewModel.

    View Slide

  35. class SearchRepository(val api: SearchAPI) {
    suspend fun fetchSearchResults(
    query: String,
    sort: Boolean
    ): List {
    api.fetchResults(query, sort)
    .map(::mapToList)
    }
    }

    View Slide

  36. class SearchRepository(val api: SearchAPI) {
    suspend fun fetchSearchResults(
    query: String,
    sort: Boolean
    ): List {
    api.fetchResults(query, sort)
    .map(::mapToList)
    }
    }

    View Slide

  37. class SearchRepository(val api: SearchAPI) {
    suspend fun fetchSearchResults(
    query: String,
    sort: Boolean
    ): List {
    api.fetchResults(query, sort)
    .map(::mapToList)
    }
    }

    View Slide

  38. //Rx
    fun fetchResults(
    query: String,
    sort: Boolean
    ): Single
    //Coroutine
    suspend fun fetchResults(
    query: String,
    sort: Boolean
    ): SearchResponse

    View Slide

  39. //Rx
    fun fetchResults(
    query: String,
    sort: Boolean
    ): Single
    //Coroutine
    suspend fun fetchResults(
    query: String,
    sort: Boolean
    ): SearchResponse

    View Slide

  40. //Rx
    fun fetchResults(
    query: String,
    sort: Boolean
    ): Single
    //Coroutine
    suspend fun fetchResults(
    query: String,
    sort: Boolean
    ): SearchResponse

    View Slide

  41. //Fast and local fetch
    fun foo(param: Param) : Result
    //Slow and IO
    suspend fun foo(param: Param) : Result
    //BG
    fun CoroutineScope.foo(param: Param): Result

    View Slide

  42. Asynchronicity Comparison
    • Observables in RxJava
    Observable.create { emitter ->
    for (i in 1..5) {
    emitter.onNext(i)
    }
    emitter.onComplete()
    }

    View Slide

  43. Asynchronicity Comparison
    • Channels
    val channel = Channel()
    launch {
    // this might be heavy CPU-consuming computation or async logic
    //we'll just send five squares
    for (x in 1..5) channel.send(x)
    }
    // here we print five received integers:
    repeat(5) { println(channel.receive()) }
    println("Done!")

    View Slide

  44. Asynchronicity Comparison
    • Channels
    val channel = Channel()
    launch {
    // this might be heavy CPU-consuming computation or async logic
    //we'll just send five squares
    for (x in 1..5) channel.send(x)
    channel.close()
    }
    // here we print five received integers:
    repeat(5) { println(channel.receive()) }
    println("Done!")

    View Slide

  45. Asynchronicity Comparison
    • Channels
    val channel = Channel()
    launch {
    // this might be heavy CPU-consuming computation or async logic
    //we'll just send five squares
    for (x in 1..5) channel.send(x)
    channel.close()
    }
    // here we print five received integers:
    repeat(5) { println(channel.receive()) }
    println("Done!")

    View Slide

  46. Asynchronicity Comparison
    • Channels
    val channel = Channel()
    launch {
    // this might be heavy CPU-consuming computation or async logic
    //we'll just send five squares
    for (x in 1..5) channel.send(x)
    channel.close()
    }
    // here we print five received integers:
    repeat(5) { println(channel.receive()) }
    println("Done!")

    View Slide

  47. Asynchronicity Comparison
    • Channels
    val sequence = produceSequence()
    sequence.consumeEach { println(it) }
    println("Done!")

    View Slide

  48. Asynchronicity Comparison
    • Channels ? Observable ?
    When you consume an object from the channel
    no other coroutine will be able to get the same object

    View Slide

  49. Asynchronicity Comparison
    • Channels ~ Race Conditions ?
    val channel = Channel()
    launch {
    val value1 = channel.receive()
    }
    launch {
    val value2 = channel.receive()
    }
    launch {
    channel.send(1)
    }

    View Slide

  50. Asynchronicity Comparison
    • Rx Subjects
    val subject: PublishSubject = PublishSubject.create()
    subject.subscribe {
    consumeValue(it)
    }
    subject.subscribe {
    println(it)
    }

    View Slide

  51. Asynchronicity Comparison
    • BroadcastChannel
    val channel = BroadcastChannel(2)
    val observer1Job = launch {
    channel.openSubscription().use { channel ->
    for (value in channel) {
    consumeValue(value)
    }
    // subscription will be closed
    }
    }

    View Slide

  52. Asynchronicity Comparison
    • BroadcastChannel
    val observer2Job = launch {
    channel.consumeEach { value ->
    consumeValue(value)
    }
    }

    View Slide

  53. Asynchronicity Comparison
    Produce = Coroutine + Channel

    View Slide

  54. Asynchronicity Comparison
    • Produce
    val publisher = produce(capacity = 2) {
    for (i in 1..5) send(i)
    }
    • Only the code inside produce can send elements to the
    channel

    View Slide

  55. Asynchronicity Comparison
    Produce is a pretty useful way to create custom
    operators

    View Slide

  56. Asynchronicity Comparison
    • Actor
    An actor also creates a coroutine with a channel built in.
    Produce implements the ReceiveChannel interface
    whereas actor implements the SendChannel interface.

    View Slide

  57. Asynchronicity Comparison
    • Actor
    val actor = actor(CommonPool) {
    for (int in channel) {
    // iterate over received Integers
    }
    }
    launch {
    actor.send(2)
    }

    View Slide

  58. Asynchronicity Comparison

    View Slide

  59. Flow
    Flow is a Stream of Data, which can emit multiple values and
    complete with or without an error

    View Slide

  60. Flow
    val myFlow: Flow = flow {
    emit(1)
    emit(2)
    delay(100)
    emit(3)
    }
    flow.collect { value ->
    println("Received $value")
    }
    Received 1
    Received 2
    Received 3
    $$==>

    View Slide

  61. Flow + Channels
    • Channels are Hot whereas Flows are cold
    • One Emits values from 1 side, receives from other side
    • A bit complex to work with

    View Slide

  62. Flow + Channels
    • Hot + Cold = Hot
    • Ex: Flow from a channel

    View Slide

  63. Flow + Channels
    object Action {
    private val channel = BroadcastChannel(1)
    button.onClick {
    channel.send(Unit)
    }
    fun getFlow(): Flow = channel.asFlow()
    }
    Action.getFlow().collect {
    println(“Received Emission”)
    }

    View Slide

  64. Flow + Channels
    object Action {
    private val channel = BroadcastChannel(1)
    button.onClick {
    channel.send(Unit)
    }
    fun getFlow(): Flow = channel.asFlow()
    }
    Action.getFlow().collect {
    println(“Received Emission”)
    }

    View Slide

  65. Flow + Channels
    object Action {
    private val channel = BroadcastChannel(1)
    button.onClick {
    channel.send(Unit)
    }
    fun getFlow(): Flow = channel.asFlow()
    }
    Action.getFlow().collect {
    println(“Received Emission”)
    }

    View Slide

  66. Flow + Channels
    object Action {
    private val channel = BroadcastChannel(1)
    button.onClick {
    channel.send(Unit)
    }
    fun getFlow(): Flow = channel.asFlow()
    }
    Action.getFlow().collect {
    println(“Received Emission”)
    }

    View Slide

  67. Flow and Rx
    compositeDisposable.add(api.search(query, sort)
    .map{response ->
    response.toList()
    }
    .flatMap {
    searchResults -> cache.saveAndReturn(searchResults)
    }
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(
    { searchResults -> displayResults(searchResults) },
    { error -> Log.e("TAG", "Failed to search", error) }
    ))

    View Slide

  68. Flow and Rx
    try {
    flow { emit(api.search(query, sort))}
    .map{response ->response.toList()}
    .flatMapConcat {
    searchResults -> cache.saveAndReturn(searchResults)
    }
    .flowOn(Dispatchers.IO)
    .collect { searchResultList ->
    withContext(Dispatchers.Main) {
    showSearchResult(searchResultList)
    }
    }
    } catch(e: Exception) {
    e.printStackTrace()
    }

    View Slide

  69. Rx / Suspend
    //Rx
    Observable.interval(1, TimeUnit.SECONDS)
    .subscribe {
    textView.text = "$it seconds have passed"
    }
    //Coroutine
    GlobalScope.launch {
    var i = 0
    while (true){
    textView.text = "${it++} seconds have passed"
    delay(1000)
    }
    }

    View Slide

  70. Rx / Suspend
    publishSubject
    .debounce(300, TimeUnit.MILLISECONDS)
    .distinctUntilChanged()
    .switchMap {
    searchQuery -> api.fetchResults(query, sort)
    }
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe({
    ...
    })

    View Slide

  71. Rx / Suspend
    launch {
    broadcast.consumeEach {
    delay(300)
    val results =
    api.fetchResults(it)
    .await()
    displayResultsLogic()
    )
    }
    }

    View Slide

  72. Rx / Suspend
    //Rx
    searchQuery.addTextChangedListener(object: TextWatcher {
    override fun afterTextChanged(s: Editable?) {
    publishSubject.onNext(s.toString())
    }
    }
    //Coroutines
    searchQuery.addTextChangedListener(object: TextWatcher {
    override fun afterTextChanged(s: Editable?) {
    broadcast.send(s.toString())
    }
    }

    View Slide

  73. bit.ly/droidcon_in_async
    Thats all folks!
    @aditlal
    Adit Lal
    adit.dev
    @rivuchakraborty
    Rivu chakraborty
    rivu.dev

    View Slide