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

Android Suspenders

Chris Banes
October 04, 2018

Android Suspenders

So you’ve read the Coroutines guide and you’re ready to start using them in your Android app to coroutines? Great!

This talk will focus on the best practices of using coroutines in your app, including how to handle lifecycle changes with Architecture Components, integration with background job processing, and moving away from RxJava.

Chris Banes

October 04, 2018
Tweet

More Decks by Chris Banes

Other Decks in Programming

Transcript

  1. SUSPENDERS
    ANDROID
    @chrisbanes

    View Slide

  2. ANDROID SUSPENDERS
    chris.banes.me/suspenders

    View Slide

  3. Android’s main thread

    View Slide

  4. Android’s main thread
    measure + layout
    draw
    View inflation
    and many more things

    View Slide

  5. ms
    60Hz
    16
    Android’s main thread

    View Slide

  6. 12 ms
    Android’s main thread
    90Hz

    View Slide

  7. ms
    120Hz
    8
    Android’s main thread

    View Slide

  8. View Slide

  9. ASYNC ALL THE THINGS

    View Slide

  10. How do we do that?

    View Slide

  11. Executors
    AsyncTask
    How do we do that?

    View Slide

  12. Loaders
    Executors
    AsyncTask
    How do we do that?

    View Slide

  13. Future
    Executors
    Loaders
    How do we do that?

    View Slide

  14. Libraries
    Loaders
    Future
    How do we do that?

    View Slide

  15. WorkManager
    Future
    Libraries
    How do we do that?

    View Slide

  16. Coroutines
    Libraries
    Coroutines
    How do we do that?

    View Slide

  17. Coroutines
    Libraries
    Coroutines
    So why use
    ?

    View Slide

  18. What does your typical
    mobile app do?

    View Slide

  19. CRUD
    What does your typical
    mobile app do?

    View Slide

  20. C R U D
    reate ead pdate elete
    What does your typical
    mobile app do?

    View Slide

  21. Sync
    What does your typical
    mobile app do?

    View Slide

  22. Sync
    What does your typical
    mobile app do?

    View Slide

  23. So why coroutines?
    Scale on limited hardware
    Easier development model
    Great for I/O tasks

    View Slide

  24. Tivi
    github.com/chrisbanes/tivi

    View Slide

  25. kotlinx-coroutines-core
    -android
    -rx2

    View Slide

  26. -core
    -android
    -rx2
    jars: 774KB

    View Slide

  27. -core
    -android
    -rx2
    jars: 774KB
    apk: 499KB
    3260 method refs

    View Slide

  28. -core
    -android
    -rx2
    jars: 774KB
    apk: 499KB
    3260 method refs
    r8: 113KB
    1208 method refs

    View Slide

  29. -core
    -android
    -rx2
    jars: 774KB
    apk: 499KB
    3260 method refs
    r8: 113KB
    1208 method refs
    r8

    optimized:
    99KB
    834 method refs

    View Slide

  30. -keepclassmembernames class kotlinx.** {
    volatile ;
    }

    View Slide

  31. ANDROID SUSPENDERS
    Creating routines

    View Slide

  32. override suspend fun updateShow(showId: Long) {
    val localResult = localShowStore.getShow(showId)
    val result1 = remoteSource1.getShow(showId)
    val result2 = remoteSource2.getShow(showId)
    val merged = mergeShow(localResult, result1, result2)
    localShowStore.saveShow(merged)
    }.

    View Slide

  33. override suspend fun updateShow(showId: Long) {
    val localResult = localShowStore.getShow(showId)
    val result1 = remoteSource1.getShow(showId)
    val result2 = remoteSource2.getShow(showId)
    val merged = mergeShow(localResult, result1, result2)
    localShowStore.saveShow(merged)
    }.
    1

    View Slide

  34. override suspend fun updateShow(showId: Long) {
    val localResult = localShowStore.getShow(showId)
    val result1 = remoteSource1.getShow(showId)
    val result2 = remoteSource2.getShow(showId)
    val merged = mergeShow(localResult, result1, result2)
    localShowStore.saveShow(merged)
    }.
    1
    2

    View Slide

  35. override suspend fun updateShow(showId: Long) {
    val localResult = localShowStore.getShow(showId)
    val result1 = remoteSource1.getShow(showId)
    val result2 = remoteSource2.getShow(showId)
    val merged = mergeShow(localResult, result1, result2)
    localShowStore.saveShow(merged)
    }.
    1
    2
    3

    View Slide

  36. override suspend fun updateShow(showId: Long) {
    val localResult = localShowStore.getShow(showId)
    val result1 = remoteSource1.getShow(showId)
    val result2 = remoteSource2.getShow(showId)
    val merged = mergeShow(localResult, result1, result2)
    localShowStore.saveShow(merged)
    }.

    View Slide

  37. override suspend fun updateShow(showId: Long) {
    val localResult = localShowStore.getShow(showId)
    val result1 = remoteSource1.getShow(showId)
    val result2 = remoteSource2.getShow(showId)
    val merged = mergeShow(localResult, result1, result2)
    localShowStore.saveShow(merged)
    }.
    1
    2
    3
    Runs in 1.2 seconds

    View Slide

  38. override suspend fun updateShow(showId: Long) {
    val localDeferred = async {

    localShowStore.getShow(showId)

    }
    val remoteDeferred1 = async {
    remoteSource1.getShow(showId)
    }
    val remoteDeferred2 = async {
    remoteSource2.getShow(showId)
    }

    val localResult = localDeferred.await()
    val result1 = remoteDeferred1.await()
    val result2 = remoteDeferred2.await()
    val merged = mergeShow(localResult, result1, result2)
    localShowStore.saveShow(merged)
    }.

    View Slide

  39. override suspend fun updateShow(showId: Long) {
    val localDeferred = async {

    localShowStore.getShow(showId)

    }
    val remoteDeferred1 = async {
    remoteSource1.getShow(showId)
    }
    val remoteDeferred2 = async {
    remoteSource2.getShow(showId)
    }

    val localResult = localDeferred.await()
    val result1 = remoteDeferred1.await()
    val result2 = remoteDeferred2.await()
    val merged = mergeShow(localResult, result1, result2)
    localShowStore.saveShow(merged)
    }.

    View Slide

  40. override suspend fun updateShow(showId: Long) {
    val localDeferred = async {

    localShowStore.getShow(showId)

    }
    val remoteDeferred1 = async {
    remoteSource1.getShow(showId)
    }
    val remoteDeferred2 = async {
    remoteSource2.getShow(showId)
    }

    val localResult = localDeferred.await()
    val result1 = remoteDeferred1.await()
    val result2 = remoteDeferred2.await()
    val merged = mergeShow(localResult, result1, result2)
    localShowStore.saveShow(merged)
    }.

    View Slide

  41. override suspend fun updateShow(showId: Long) {
    val localDeferred = async {

    localShowStore.getShow(showId)

    }
    val remoteDeferred1 = async {
    remoteSource1.getShow(showId)
    }
    val remoteDeferred2 = async {
    remoteSource2.getShow(showId)
    }

    val localResult = localDeferred.await()
    val result1 = remoteDeferred1.await()
    val result2 = remoteDeferred2.await()
    val merged = mergeShow(localResult, result1, result2)
    localShowStore.saveShow(merged)
    }.

    View Slide

  42. override suspend fun updateShow(showId: Long) {
    val localDeferred = async {

    localShowStore.getShow(showId)

    }
    val remoteDeferred1 = async {
    remoteSource1.getShow(showId)
    }
    val remoteDeferred2 = async {
    remoteSource2.getShow(showId)
    }

    val localResult = localDeferred.await()
    val result1 = remoteDeferred1.await()
    val result2 = remoteDeferred2.await()
    val merged = mergeShow(localResult, result1, result2)
    localShowStore.saveShow(merged)
    }.

    View Slide

  43. override suspend fun updateShow(showId: Long) {
    val localDeferred = async {

    localShowStore.getShow(showId)

    }
    val remoteDeferred1 = async {
    remoteSource1.getShow(showId)
    }
    val remoteDeferred2 = async {
    remoteSource2.getShow(showId)
    }

    val localResult = localDeferred.await()
    val result1 = remoteDeferred1.await()
    val result2 = remoteDeferred2.await()
    val merged = mergeShow(localResult, result1, result2)
    localShowStore.saveShow(merged)
    }.
    Runs in 0.8 second

    View Slide

  44. How do we use them
    on Android?

    View Slide

  45. class ShowDetailsViewModel: ViewModel() {
    val showRepository: ShowRepository = // ...
    val state: LiveData
    init {
    refresh()
    }
    private fun refresh() {
    launch {
    showRepository.updateShow(showId)
    // update view state
    }
    }
    }

    View Slide

  46. class ShowDetailsViewModel: ViewModel() {
    val showRepository: ShowRepository = // ...
    val state: LiveData
    init {
    refresh()
    }
    private fun refresh() {
    launch {
    showRepository.updateShow(showId)
    // update view state
    }
    }
    }

    View Slide

  47. class ShowDetailsViewModel: ViewModel() {
    val showRepository: ShowRepository = // ...
    val state: LiveData
    init {
    refresh()
    }
    private fun refresh() {
    launch {
    showRepository.updateShow(showId)
    // update view state
    }
    }
    }

    View Slide

  48. class ShowDetailsViewModel: ViewModel() {
    val showRepository: ShowRepository = // ...
    val state: LiveData
    init {
    refresh()
    }
    private fun refresh() {
    launch {
    showRepository.updateShow(showId)
    // update view state
    }
    }
    }

    View Slide

  49. class ShowDetailsViewModel: ViewModel() {
    val showRepository: ShowRepository = // ...
    val state: LiveData
    init {•
    refresh()
    }.
    private fun refresh() {•
    launch {•
    showRepository.updateShow(showId)
    // update view state
    }.
    }.
    }.

    View Slide

  50. Jobs

    View Slide

  51. class ShowDetailsViewModel: ViewModel() {
    private fun refresh() {
    launch {
    showRepository.updateShow(showId)
    // update view state
    }.
    }.
    }.

    View Slide

  52. class ShowDetailsViewModel: ViewModel() {
    private fun refresh() {
    val job = launch {
    showRepository.updateShow(showId)
    // update view state
    }.
    }.
    }.

    View Slide

  53. class ShowDetailsViewModel: ViewModel() {
    private fun refresh() {
    val job = launch {
    showRepository.updateShow(showId)
    // update view state
    }.
    // can call job.cancel()
    }.
    }.

    View Slide

  54. class ShowDetailsViewModel: ViewModel() {
    private var updateShowJob: Job? = null
    private fun refresh() {
    val job = launch {
    showRepository.updateShow(showId)
    // update view state
    }.
    // can call job.cancel()
    }.
    }.

    View Slide

  55. class ShowDetailsViewModel: ViewModel() {
    private var updateShowJob: Job? = null
    private fun refresh() {
    updateShowJob = launch {
    showRepository.updateShow(showId)
    // update view state
    }.
    // can call job.cancel()
    }.
    }.

    View Slide

  56. class ShowDetailsViewModel: ViewModel() {
    private var updateShowJob: Job? = null
    private fun refresh() {
    updateShowJob = launch {
    showRepository.updateShow(showId)
    // update view state
    }.
    }.
    }.

    View Slide

  57. class ShowDetailsViewModel: ViewModel() {
    private var updateShowJob: Job? = null
    private fun refresh() {
    updateShowJob = launch {
    showRepository.updateShow(showId)
    // update view state
    }.
    }.
    override fun onCleared() {
    updateShowJob ?.cancel()
    }
    }.

    View Slide

  58. class ShowDetailsViewModel: ViewModel() {
    private var updateShowJob: Job? = null
    private var updateEpisodesJob: Job? = null
    private fun refresh() {
    updateShowJob = launch {
    showRepository.updateShow(showId)
    // update view state
    }.
    updateShowJob = launch { ... }
    }.
    override fun onCleared() {
    updateShowJob ?.cancel()
    updateEpisodesJob ?.cancel()
    }
    }.

    View Slide

  59. class ShowDetailsViewModel: ViewModel() {
    private var updateShowJob: Job? = null
    private var updateEpisodesJob: Job? = null
    private var updateCastJob: Job? = null
    private fun refresh() {
    updateShowJob = launch {
    showRepository.updateShow(showId)
    // update view state
    }.
    updateShowJob = launch { ... }
    updateCastJob = launch { ... }
    }.
    override fun onCleared() {
    updateShowJob ?.cancel()
    updateEpisodesJob ?.cancel()
    updateCastJob ?.cancel()
    }
    }.

    View Slide

  60. Coroutines can have a
    parent-child relationship

    View Slide

  61. Coroutines can have a
    parent-child relationship
    Expressed via Jobs
    (in a context)

    View Slide

  62. class ShowDetailsViewModel: ViewModel() {
    val showRepository: ShowRepository = // ...
    val state: LiveData
    private val job = Job()
    init {•
    refresh()
    }.
    private fun refresh() {•
    launch(parent = job) {•
    showRepository.updateShow(showId)
    // update view state
    }.
    }.

    View Slide

  63. class ShowDetailsViewModel: ViewModel() {
    val showRepository: ShowRepository = // ...
    val state: LiveData
    private val job = Job()
    init {
    refresh()
    }.
    private fun refresh() {
    launch(parent = job) {
    showRepository.updateShow(showId)
    // update view state
    }.
    }.
    override onCleared() {

    View Slide

  64. private val job = Job()
    init {
    refresh()
    }.
    private fun refresh() {
    launch(parent = job) {
    showRepository.updateShow(showId)
    // update view state
    }.
    }.
    override onCleared() {
    job.cancel()
    } ..
    }.

    View Slide

  65. private val job = Job()
    init {
    refresh()
    }.
    private fun refresh() {
    launch(parent = job) {
    showRepository.updateShow(showId)
    // update view state
    }.
    }.
    override onCleared() {
    job.cancel()
    }
    } ..

    View Slide

  66. private val job = Job()
    init {
    refresh()
    }.
    private fun refresh() {
    launch(parent = job) {
    showRepository.updateShow(showId)
    // update view state
    }.
    }.
    override onCleared() {
    job.cancel()
    }
    } ..

    View Slide

  67. private val job = Job()
    init {
    refresh()
    }.
    private fun refresh() {
    launch(parent = job) {
    showRepository.updateShow(showId)
    // update view state
    }.
    }.
    override onCleared() {
    job.cancel()
    }
    } ..
    launch is now scoped to the
    lifetime of the ViewModel

    View Slide

  68. Scopes

    View Slide

  69. private val job = Job()
    init {
    refresh()
    }.
    private fun refresh() {
    launch(parent = job) {
    showRepository.updateShow(showId)
    // update view state
    }.
    }.
    override onCleared() {
    super.onCleared()
    job.cancel()
    }
    } ..
    Explicit

    View Slide

  70. launch(parent = job) {
    }
    suspend fun updateShow(showId: Long) {

    val local = async {

    localShowStore.getShow(showId)

    }}

    val remote = async {
    remoteSource.getShow(showId)
    }}
    val merged = mergeShow(local.await(), remote.await())
    localShowStore.saveShow(merged)
    }.

    View Slide

  71. launch(parent = job) {
    }
    suspend fun updateShow(showId: Long) {

    val local = async {

    localShowStore.getShow(showId)

    }}

    val remote = async {
    remoteSource.getShow(showId)
    }}
    val merged = mergeShow(local.await(), remote.await())
    localShowStore.saveShow(merged)
    }.
    New coroutines
    No parent
    Suspended
    job.cancel()
    Cancelled

    View Slide

  72. init {
    refresh()
    }.
    private fun refresh() {
    launch(parent = job) {
    showRepository.updateShow(showId)
    // update view state
    }.
    }.
    override onCleared() {
    Deprecated in v0.26
    async too

    View Slide

  73. New!
    v0.26
    CoroutineScope
    async & launch become instance methods
    Allows objects to provide scope for coroutines
    You provide a default context

    View Slide

  74. class ShowDetailsViewModel: ViewModel() {•
    val showRepository: ShowRepository = // ...
    val state: LiveData
    private val job = Job()
    init {
    refresh()
    }.
    private fun refresh() {•
    launch(parent = job) {•
    showRepository.updateShow(showId)
    // update view state
    }.
    }.
    override onCleared() {

    View Slide

  75. class ShowDetailsViewModel: ViewModel(), CoroutineScope {•
    val showRepository: ShowRepository = // ...
    val state: LiveData
    private val job = Job()
    init {
    refresh()
    }.
    private fun refresh() {•
    launch(parent = job) {•
    showRepository.updateShow(showId)
    // update view state
    }.
    }.
    override onCleared() {

    View Slide

  76. class ShowDetailsViewModel: ViewModel(), CoroutineScope {•
    val showRepository: ShowRepository = // ...
    val state: LiveData
    private val job = Job()
    override val coroutineContext: CoroutineContext
    get() = job + Dispatchers.Main
    init {
    refresh()
    }.
    private fun refresh() {•
    launch(parent = job) {•
    showRepository.updateShow(showId)
    // update view state
    }.

    View Slide

  77. override val coroutineContext: CoroutineContext
    get() = job + Dispatchers.Main
    init {
    refresh()
    }.
    private fun refresh() {•
    launch(parent = job) {•
    showRepository.updateShow(showId)
    // update view state
    }.
    }.
    override onCleared() {
    super.onCleared()
    job.cancel()
    }.
    }.

    View Slide

  78. override val coroutineContext: CoroutineContext
    get() = job + Dispatchers.Main
    init {
    refresh()
    }.
    private fun refresh() {•
    launch {•
    showRepository.updateShow(showId)
    // update view state
    }.
    }.
    override onCleared() {•
    super.onCleared()
    job.cancel()
    }.
    }.

    View Slide

  79. override val coroutineContext: CoroutineContext
    get() = job + Dispatchers.Main
    init {
    refresh()
    }.
    private fun refresh() {•
    this.launch(context = coroutineContext) {•
    showRepository.updateShow(showId)
    // update view state
    }.
    }.
    override onCleared() {•
    super.onCleared()
    job.cancel()
    }.
    }.

    View Slide

  80. open class ScopedViewModel : ViewModel(), CoroutineScope {
    private val job = Job()
    override val coroutineContext: CoroutineContext
    get() = job + Dispatchers.Main
    override fun onCleared() {
    super.onCleared()
    job.cancel()
    }
    }

    View Slide

  81. launch {
    }
    suspend fun updateShow(showId: Long) {}

    val local = async {}

    localShowStore.getShow(showId)

    }}

    val remote = async {}
    remoteSource.getShow(showId)
    }}
    val merged = mergeShow(local.await(), remote.await())
    localShowStore.saveShow(merged)
    }.

    View Slide

  82. launch {
    }
    suspend fun updateShow(showId: Long) = coroutineScope {}

    val local = async {}

    localShowStore.getShow(showId)

    }}

    val remote = async {}
    remoteSource.getShow(showId)
    }}
    val merged = mergeShow(local.await(), remote.await())
    localShowStore.saveShow(merged)
    }.

    View Slide

  83. launch {
    }
    suspend fun updateShow(showId: Long) = coroutineScope {}

    val local = async {}

    localShowStore.getShow(showId)

    }}

    val remote = async {}
    remoteSource.getShow(showId)
    }}
    val merged = mergeShow(local.await(), remote.await())
    localShowStore.saveShow(merged)
    }.
    Child coroutines
    Suspended
    job.cancel()
    Cancelled
    Children are
    cancelled too

    View Slide

  84. What if I’m not using
    ViewModels?

    View Slide

  85. open class ScopedFragment : Fragment(), CoroutineScope {
    private val job = Job()
    override val coroutineContext: CoroutineContext
    get() = job + Dispatchers.Main
    override fun onDestroy() {
    super.onDestroy()
    job.cancel()
    }
    }

    View Slide

  86. // In your Activity / Fragment
    val observer = MyObserver()
    getLifecycle().addObserver(observer)
    class MyObserver : DefaultLifecycleObserver {{
    override fun onCreate(owner: LifecycleOwner) {{
    // we’re created
    }-
    override fun onDestroy(owner: LifecycleOwner) {{
    // we’re destroyed
    })
    }{

    View Slide

  87. class MyObserver : DefaultLifecycleObserver {{
    override fun onCreate(owner: LifecycleOwner) {{
    // we’re created
    }-
    override fun onDestroy(owner: LifecycleOwner) {{
    // we’re destroyed
    })
    }{
    // In your Activity / Fragment
    val observer = MyObserver()
    getLifecycle().addObserver(observer)

    View Slide

  88. class LifecycleScope : DefaultLifecycleObserver {{
    override fun onDestroy(owner: LifecycleOwner) {{
    // we’re destroyed
    })
    }{

    View Slide

  89. class LifecycleScope : DefaultLifecycleObserver {{
    val job = Job(){
    override fun onDestroy(owner: LifecycleOwner) {{
    // we’re destroyed
    })
    }{

    View Slide

  90. class LifecycleScope : DefaultLifecycleObserver {{
    val job = Job()
    override fun onDestroy(owner: LifecycleOwner) {
    // we’re destroyed{
    job.cancel()
    })
    }{

    View Slide

  91. class LifecycleScope : DefaultLifecycleObserver, CoroutineScope {{
    val job = Job()
    override fun onDestroy(owner: LifecycleOwner) {
    // we’re destroyed{
    job.cancel()
    })
    }{

    View Slide

  92. class LifecycleScope : DefaultLifecycleObserver, CoroutineScope {{
    val job = Job()
    override val coroutineContext = job + Dispatchers.Main
    override fun onDestroy(owner: LifecycleOwner) {
    // we’re destroyed{
    job.cancel()
    })
    }{
    class DetailsFragment : Fragment() {
    private val scope = LifecycleScope()
    init {
    lifecycle.addObserver(scope)
    }}

    View Slide

  93. class DetailsFragment : Fragment() {
    private val scope = LifecycleScope()
    init {
    lifecycle.addObserver(scope)
    }}
    override fun onCreateView( ...) {
    // ...
    }}
    }}
    // we’re destroyed{
    job.cancel()
    })
    }{

    View Slide

  94. class DetailsFragment : Fragment() {
    private val scope = LifecycleScope()
    init {
    lifecycle.addObserver(scope)
    }}
    override fun onCreateView( ...) {
    // ...
    }}
    }}

    View Slide

  95. class DetailsFragment : Fragment() {
    private val scope = LifecycleScope()
    init {
    lifecycle.addObserver(scope)
    }}
    override fun onStart() {
    scope.launch {
    // something async
    }
    }
    override fun onCreateView( ...) {
    // ...
    }}
    }}

    View Slide

  96. class DetailsFragment : Fragment() {
    private val scope = LifecycleScope()
    init {
    lifecycle.addObserver(scope)
    }}
    override fun onStart() {
    scope.launch {
    // something async
    }
    }
    override fun onCreateView( ...) {
    // ...
    }}
    }}
    Scoped to
    Fragment lifecycle

    View Slide

  97. gist.louiscad.com/LifecycleCoroutines

    View Slide

  98. Cancellation

    View Slide

  99. Cancellation
    requires co-operation

    View Slide

  100. Cancellation
    requires co-operation
    Need to check if cancelled

    View Slide

  101. Cancellation
    requires co-operation
    Need to check if active

    View Slide

  102. Call a suspending function
    Check isActive
    Two ways to co-operate

    View Slide

  103. Call a suspending function
    Check isActive
    if (job != null && !job.isActive) {
    throw job.getCancellationException()
    }
    Two ways to co-operate
    yield()

    View Slide

  104. Two ways to co-operate
    Call a suspending function
    Check isActive
    launch {
    files.forEach { file ->
    if (isActive) doSomethingWith(file)
    }
    }

    View Slide

  105. Exceptions

    View Slide

  106. launch rethrows
    exceptions

    View Slide

  107. suspend fun updateShow(showId: Long) = coroutineScope {}
    val local = async {}
    localShowStore.getShow(showId)
    }}
    val remote = async {}
    remoteSource.getShow(showId)
    }}
    val merged = mergeShow(local.await(), remote.await())
    localShowStore.saveShow(merged)
    }}

    View Slide

  108. suspend fun updateShow(showId: Long) = coroutineScope {}
    val local = async {}
    localShowStore.getShow(showId)
    }}
    val trakt = async {}
    remoteSource.getShow(showId)
    }}
    val merged = mergeShow(local.await(), remote.await())
    localShowStore.saveShow(merged)
    // throws CrashyMcCrashfaceException()
    }}

    View Slide

  109. suspend fun updateShow(showId: Long) = coroutineScope {}
    val local = async {}
    localShowStore.getShow(showId)
    }}
    val trakt = async {}
    remoteSource.getShow(showId)
    }}
    val merged = mergeShow(local.await(), remote.await())
    localShowStore.saveShow(merged)
    // throws CrashyMcCrashfaceException()
    }}
    Treated like an uncaught
    exception

    View Slide

  110. async doesn’t throw
    until await()

    View Slide

  111. suspend fun updateShow(showId: Long) = coroutineScope {}
    val local = async {}
    localShowStore.getShow(showId)
    }}
    val remote = async {}
    remoteSource.getShow(showId)
    }}
    val merged = mergeShow(local.await(), remote.await())
    localShowStore.saveShow(merged)
    }}

    View Slide

  112. suspend fun updateShow(showId: Long) = coroutineScope {}
    val local = async {}
    localShowStore.getShow(showId)
    // throws CrashyMcCrashfaceException()
    }}
    val remote = async {}
    remoteSource.getShow(showId)
    }}
    val merged = mergeShow(local.await(), remote.await())}
    localShowStore.saveShow(merged)}
    }}

    View Slide

  113. suspend fun updateShow(showId: Long) = coroutineScope {}
    val local = async {}
    localShowStore.getShow(showId)
    // throws CrashyMcCrashfaceException()
    }}
    val remote = async {}
    remoteSource.getShow(showId)
    }}
    val merged = mergeShow(local.await(), remote.await())}
    localShowStore.saveShow(merged)}
    }}
    Thrown here

    View Slide

  114. suspend fun updateShow(showId: Long) = coroutineScope {}
    val local = async {}
    localShowStore.getShow(showId)
    // throws CrashyMcCrashfaceException()
    }}
    val remote = async {}
    remoteSource.getShow(showId)
    }}
    try {}
    val merged = mergeShow(local.await(), remote.await())
    localShowStore.saveShow(merged)
    } catch (t: Throwable) {}
    // Handle the exception
    }}
    }}

    View Slide

  115. Dispatchers

    View Slide

  116. launch {{
    // fun things
    }}

    View Slide

  117. launch(context = Dispatchers.Default) {{
    // fun things
    }}

    View Slide

  118. What’s a
    CoroutineDispatcher?

    View Slide

  119. What’s a
    CoroutineDispatcher?
    The thing which runs and
    schedules coroutines

    View Slide

  120. DefaultScheduler
    Dispatchers.Default
    ===

    View Slide

  121. DefaultScheduler
    New!
    v0.30
    Elastic thread executor
    Uses cpuCount threads (core)
    Now set as Dispatchers.Default in 0.30

    View Slide

  122. New!
    v0.25
    IO
    Designed for blocking I/O tasks
    Uses 64 <= cpuCount threads
    launch(IO) {{
    // network things
    }}

    View Slide

  123. IO
    New!
    v0.25
    Shares threads
    IO dispatcher uses DefaultScheduler

    View Slide

  124. New!
    v0.25
    async {
    val local = withContext(IO) {
    fileStore.loadImage( ...)
    }}
    // No thread context switch!
    return processImage(local)
    }}
    Shares threads
    IO dispatcher uses DefaultScheduler
    IO

    View Slide

  125. async {
    val local = withContext(IO) {
    fileStore.loadImage( ...)
    }}
    // No thread context switch!
    return processImage(local)
    }}
    Shares threads
    IO dispatcher uses DefaultScheduler
    New!
    v0.25
    IO

    View Slide

  126. async {
    val local = withContext(IO) {
    fileStore.loadImage(...)
    }}
    // No thread context switch!
    return processImage(local)
    }}
    Shares threads
    IO dispatcher uses DefaultScheduler
    New!
    v0.25
    IO

    View Slide

  127. ANDROID SUSPENDERS
    Reactivity

    View Slide

  128. Most devs use RxJava
    for easy threading

    View Slide

  129. End up mainly using Single,
    Maybe & Completable

    View Slide

  130. End up mainly using Single,
    Maybe & Completable
    Only exist in RxJava
    RxScala
    RxGroovy

    View Slide

  131. Coroutines
    can replace
    Single / Maybe / Completable

    View Slide

  132. service.trendingShows()
    .scheduleOn(schedulers.io)
    .subscribe(::onTrendingLoaded, ::onError)
    interface RemoteService {}
    @GET("/trendingshows")
    fun trendingShows(): Single>
    }}

    View Slide

  133. val show = withContext(dispatchers.io) {}
    service.trendingShows().await()
    }}
    interface RemoteService {}
    @GET("/trendingshows")
    fun trendingShows(): Single>
    }}

    View Slide

  134. val show = withContext(dispatchers.io) {}
    service.trendingShows().await()
    }}
    interface RemoteService {}
    @GET("/trendingshows")
    fun trendingShows(): Single>
    }}

    View Slide

  135. interface RemoteService {}
    @GET("/trendingshows")
    suspend fun trendingShows(): List
    }}
    val show = withContext(dispatchers.io) {}
    service.trendingShows().await()
    }}

    View Slide

  136. interface RemoteService {}
    @GET("/trendingshows")
    suspend fun trendingShows(): List
    }}
    val show = withContext(dispatchers.io) {}
    service.trendingShows().await()
    }}
    Coming to Retrofit soon™

    View Slide

  137. interface RemoteService {}
    @GET("/trendingshows")
    suspend fun trendingShows(): List
    }}
    val show = withContext(dispatchers.io) {}
    service.trendingShows().await()
    }}

    View Slide

  138. interface RemoteService {}
    @GET("/trendingshows")
    suspend fun trendingShows(): List
    }}
    val show = withContext(dispatchers.io) {}
    service.trendingShows()
    }}

    View Slide

  139. Channels

    View Slide

  140. Be aware that Channels
    is experimental
    Channels

    View Slide

  141. Channels
    Not a replacement for RxJava Publishers
    Describe an asynchronous stream of things

    View Slide

  142. val channel = Channel()
    launch {
    // First coroutine to load images
    files.forEach { file ->
    val bitmap = loadBitmap(file)
    channel.send(bitmap)
    }
    // We're done with the channel
    channel.close()
    }
    launch {
    // Second coroutine to process them

    View Slide

  143. val channel = Channel()
    launch {
    // First coroutine to load images
    files.forEach { file ->
    val bitmap = loadBitmap(file)
    channel.send(bitmap)
    }
    // We're done with the channel
    channel.close()
    }
    launch {
    // Second coroutine to process them

    View Slide

  144. val channel = Channel()
    launch {
    // First coroutine to load images
    files.forEach { file ->
    val bitmap = loadBitmap(file)
    channel.send(bitmap)
    }
    // We're done with the channel
    channel.close()
    }
    launch {
    // Second coroutine to process them

    View Slide

  145. val channel = Channel()
    launch {
    // First coroutine to load images
    files.forEach { file ->
    val bitmap = loadBitmap(file)
    channel.send(bitmap)
    }
    // We're done with the channel
    channel.close()
    }
    launch {
    // Second coroutine to process them

    View Slide

  146. files.forEach { file ->
    val bitmap = loadBitmap(file)
    channel.send(bitmap)
    }
    // We're done with the channel
    channel.close()
    }
    launch {
    // Second coroutine to process them
    channel.consumeEach { bitmap ->
    processBitmap(bitmap)
    }
    }

    View Slide

  147. BroadcastChannels ≈ Subjects
    ConflatedBroadcastChannel ≈ BehaviorSubject
    Channels

    View Slide

  148. Channels support null values
    Channels
    Channels only support streams
    "hot"

    View Slide

  149. Operators

    View Slide

  150. all any associate associateBy associateByTo
    associateTo consume consumeEach consumeEachIndexed
    consumes count distinct distinctBy drop dropWhile
    elementAt elementAtOrElse elementAtOrNull filter
    filterIndexed filterIndexedTo filterNot filterNotNull
    filterNotNullTo filterNotTo filterTo find findLast
    first firstOrNull flatMap fold foldIndexed groupBy
    groupByTo indexOf indexOfFirst indexOfLast last
    l a s t I n d e x O f l a s t O r N u l l m a p m a p I n d e x e d
    mapIndexedNotNull mapIndexedNotNullTo mapIndexedTo
    mapNotNull mapNotNullTo mapTo maxBy maxWith minBy
    minWith none partition reduce reduceIndexed
    requireNoNulls single singleOrNull sumBy sumByDouble
    take takeWhile toChannel toCollection toList toMap
    toMutableList toMutableSet toSet withIndex zip
    Channels has 73
    built-in operators

    View Slide

  151. aggregate
    all
    amb
    ambWith
    and
    apply
    asObservable
    asyncAction
    asyncFunc
    averageDouble
    length
    limit
    longCount
    map
    mapMany
    materialize
    max
    maxBy
    merge
    mergeDelayError
    Compare that to RxJava
    230ish

    View Slide

  152. Operators are easier to write in
    coroutines
    fun Publisher.map(
    context: CoroutineContext,
    mapper: (T) -> R
    ) = GlobalScope.publish(context) {
    consumeEach {
    send(mapper(it))
    }
    }

    View Slide

  153. ANDROID SUSPENDERS
    Using Android APIs

    View Slide

  154. Get last known location
    One shot callback

    View Slide

  155. Get last known location
    FusedLocationProviderClient
    In Google Play Services
    client.getLastLocation(): Task

    View Slide

  156. Get last known location
    FusedLocationProviderClient
    client.getLastLocation().addOnCompleteListener { task ->
    // where am i?
    val location = task.result
    }
    Location
    In Google Play Services

    View Slide

  157. callback ➡ suspend

    View Slide

  158. callback ➡ suspend
    suspendCoroutine { ... }

    View Slide

  159. suspendCoroutine { ... }
    Given a continuation to later resume
    Will run the lambda and then immediately suspend
    How you pass the result back

    View Slide

  160. Get last known location
    suspend fun getLastLocation(): Location {
    }}
    // TODO

    View Slide

  161. Get last known location
    suspend fun getLastLocation(): Location {
    return suspendCoroutine { continuation ->
    }}
    }}
    // TODO

    View Slide

  162. Get last known location
    suspend fun getLastLocation(): Location {
    return suspendCoroutine { continuation ->
    }}
    }}

    View Slide

  163. Get last known location
    suspend fun getLastLocation(): Location {
    return suspendCoroutine { continuation ->
    locationClient.lastLocation.addOnCompleteListener { task ->
    continuation.resume(task.result)
    }}
    }}
    }}

    View Slide

  164. Get last known location
    suspend fun getLastLocation(): Location {
    return suspendCoroutine { continuation ->
    locationClient.lastLocation.addOnCompleteListener { task ->
    continuation.resume(task.result)
    }}
    }}
    }}
    Resumes coroutine
    with result

    View Slide

  165. Get last known location
    suspend fun getLastLocation(): Location {
    return suspendCoroutine { continuation ->
    locationClient.lastLocation.addOnCompleteListener { task ->
    if (task.isSuccessful) {
    continuation.resume(task.result)
    } else {
    continuation.resumeWithException(task.exception !!)
    }
    }}
    }}
    }}

    View Slide

  166. Observe location updates
    Multiple value callback

    View Slide

  167. FusedLocationProviderClient
    Takes a callback parameter
    fun requestLocationUpdates(
    request: LocationRequest,
    callback: LocationCallback,
    looper: Looper
    ): Task

    View Slide

  168. fun requestLocationUpdates(
    request: LocationRequest,
    callback: LocationCallback,
    looper: Looper
    ): Task

    View Slide

  169. fun requestLocationUpdates(
    request: LocationRequest,
    callback: LocationCallback,
    looper: Looper
    ): Task

    View Slide

  170. fun requestLocationUpdates(
    request: LocationRequest,
    callback: LocationCallback,
    looper: Looper
    ): Task

    View Slide

  171. fun requestLocationUpdates(
    request: LocationRequest,
    callback: LocationCallback,
    looper: Looper
    ): Task

    View Slide

  172. fun removeLocationUpdates(
    callback: LocationCallback
    ): Task
    Remove the callback
    when done
    request: LocationRequest,
    callback: LocationCallback,
    looper: Looper
    ): Task

    View Slide

  173. fun observeLocation(
    locationRequest: LocationRequest
    ): ReceiveChannel
    Lets build our function...

    View Slide

  174. fun observeLocation(
    locationRequest: LocationRequest
    ): ReceiveChannel {}
    }}

    View Slide

  175. fun observeLocation(
    locationRequest: LocationRequest
    ): ReceiveChannel {}
    val channel = Channel()
    // TODO update channel
    return channel
    }}

    View Slide

  176. fun observeLocation(
    locationRequest: LocationRequest
    ): ReceiveChannel {}
    val channel = Channel()
    val callback = object : LocationCallback() {
    override fun onLocationResult(result: LocationResult) {
    channel.sendBlocking(result.getLastLocation())
    }}
    }}
    // TODO request location updates
    return channel
    }}

    View Slide

  177. fun observeLocation(
    locationRequest: LocationRequest
    ): ReceiveChannel {
    val channel = Channel()
    val callback = object : LocationCallback() {
    override fun onLocationResult(result: LocationResult) {
    channel.sendBlocking(result.getLastLocation())
    }
    }
    // TODO request location updates
    return channel
    }

    View Slide

  178. fun observeLocation(
    locationRequest: LocationRequest
    ): ReceiveChannel {
    val channel = Channel()
    val callback = object : LocationCallback() {
    override fun onLocationResult(result: LocationResult) {
    channel.sendBlocking(result.getLastLocation())
    }}
    }}
    // TODO request location updates
    return channel
    }}

    View Slide

  179. fun observeLocation(
    locationRequest: LocationRequest
    ): ReceiveChannel {
    val channel = Channel()
    val callback = object : LocationCallback() {
    override fun onLocationResult(result: LocationResult) {
    channel.sendBlocking(result.getLastLocation())
    }}
    }}
    return channel
    }}

    View Slide

  180. fun observeLocation(
    locationRequest: LocationRequest
    ): ReceiveChannel {
    val channel = Channel()
    val callback = object : LocationCallback() {
    override fun onLocationResult(result: LocationResult) {
    channel.sendBlocking(result.getLastLocation())
    }}
    }}
    // Start location updates, sending locations to the
    // channel
    locationClient.requestLocationUpdates(
    locationRequest,
    callback,
    Looper.myLooper()
    )}

    View Slide

  181. val channel = Channel()
    val callback = object : LocationCallback() {
    override fun onLocationResult(result: LocationResult) {
    channel.sendBlocking(result.getLastLocation())
    }}
    }}
    // Start location updates, sending locations to the
    // channel
    locationClient.requestLocationUpdates(
    locationRequest,
    callback,
    Looper.myLooper()
    )}
    return channel
    }}

    View Slide

  182. }}
    // Start location updates, sending locations to the
    // channel
    locationClient.requestLocationUpdates(
    locationRequest,
    callback,
    Looper.myLooper()
    )}
    // Remove the location callback when the channel
    // is closed
    channel.invokeOnClose {
    locationClient.removeLocationUpdates(callback)
    }
    return channel
    }}

    View Slide

  183. lateinit var locationChannel: ReceiveChannel
    override fun onStart() {
    val locationRequest = LocationRequest()
    .setSmallestDisplacement(100f) // 100m
    locationChannel = observeLocation(locationRequest)
    launch(Main) {
    locationChannel.consumeEach {
    // We have a new location!
    updateUiForLocation(it)
    }
    }
    }
    override fun onStop() {
    // Stop observing the location and

    View Slide

  184. lateinit var locationChannel: ReceiveChannel
    override fun onStart() {
    val locationRequest = LocationRequest()
    .setSmallestDisplacement(100f) // 100m
    locationChannel = observeLocation(locationRequest)
    launch(Main) {
    locationChannel.consumeEach {
    // We have a new location!
    updateUiForLocation(it)
    }
    }
    }
    override fun onStop() {
    // Stop observing the location and

    View Slide

  185. lateinit var locationChannel: ReceiveChannel
    override fun onStart() {
    val locationRequest = LocationRequest()
    .setSmallestDisplacement(100f) // 100m
    locationChannel = observeLocation(locationRequest)
    launch(Main) {
    locationChannel.consumeEach {
    // We have a new location!
    updateUiForLocation(it)
    }
    }
    }
    override fun onStop() {
    // Stop observing the location and

    View Slide

  186. lateinit var locationChannel: ReceiveChannel
    override fun onStart() {
    val locationRequest = LocationRequest()
    .setSmallestDisplacement(100f) // 100m
    locationChannel = observeLocation(locationRequest)
    launch(Main) {
    locationChannel.consumeEach {
    // We have a new location!
    updateUiForLocation(it)
    }}
    }}
    }}
    override fun onStop() {
    // Stop observing the location and

    View Slide

  187. .setSmallestDisplacement(100f) // 100m
    locationChannel = observeLocation(locationRequest)
    launch(Main) {
    locationChannel.consumeEach {
    // We have a new location!
    updateUiForLocation(it)
    }}
    }}
    }}
    override fun onStop() {
    // Stop observing the location and
    // close the channel
    locationChannel.cancel()
    }}

    View Slide

  188. .setSmallestDisplacement(100f) // 100m
    locationChannel = observeLocation(locationRequest)
    launch(Main) {
    locationChannel.consumeEach {
    // We have a new location!
    updateUiForLocation(it)
    }}
    }}
    }}
    override fun onStop() {
    // Stop observing the location and
    // close the channel
    locationChannel.cancel()
    }}

    View Slide

  189. ANDROID SUSPENDERS
    What's next?

    View Slide

  190. Codelab
    https:/
    /codelabs.developers.google.com/codelabs/kotlin-coroutines/

    View Slide

  191. RTFM

    View Slide

  192. Exploring Coroutines in
    Kotlin
    Kotlin Coroutines in
    Practice
    Coroutines and Reactive
    Programming - friends or foes?
    Effectenbeurzaal
    Tomorrow, 13:00
    Effectenbeurzaal
    Tomorrow, 10:15
    Granbeurszaal
    Tomorrow, 15:15

    View Slide

  193. Over and out…
    @chrisbanes

    View Slide