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

Diving into Coroutines

Diving into Coroutines

Diving into Coroutines

Bangkok Mobile Days

Enrique López Mañas

September 07, 2020
Tweet

More Decks by Enrique López Mañas

Other Decks in Programming

Transcript

  1. Diving into
    coroutines for JVM
    Enrique López Mañas

    View Slide

  2. View Slide

  3. Why do we want
    coroutines?
    val user = fetchUserData()
    textView.text = user.name NetworkOnMainThreadException

    View Slide

  4. Why do we want
    coroutines?
    thread {
    val user = fetchUserData()
    textView.text = user.name
    }
    CalledFromAnotherThreadException

    View Slide

  5. Why do we want
    coroutines?
    fetchUserData { user -> //callback
    textView.text = user.name
    }
    Leaking callbacks all the time

    View Slide

  6. Why do we want
    coroutines?

    View Slide

  7. Why do we want
    coroutines?
    val subscription = fetchUserData { user ->
    textView.text = user.name
    }
    override fun onStop() {
    subscription.cancel()
    }

    View Slide

  8. Why do we want
    coroutines?
    override fun onStop() {
    subscription.cancel()
    subscription1.cancel()
    subscription2.cancel()
    subscription3.cancel()
    subscription4.cancel()
    subscription5.cancel()
    subscription6.cancel()
    subscription7.cancel()
    subscription8.cancel()
    subscription9.cancel()
    }

    View Slide

  9. Why do we want
    coroutines?
    object MyTask: AsyncTask() {
    override fun doInBackground { code }
    override fun onPostExecute { code }
    }

    View Slide

  10. RxJava
    fun fetchUser() : Observable = ...
    fetchUser()
    .as(autoDisposable(AndroidLifecycleScopeProvider.from(this)))
    .subscribe{ user ->
    textView.text = user.name
    }

    View Slide

  11. LiveData
    fun fetchUser() : LiveData = ...
    fetchUser().observe(viewLifecycleOwner) {
    textView.text = user.name
    }

    View Slide

  12. LiveData RxJava
    Observable Data Holder
    Observable +
    Schedulers +
    Observers
    Coroutines
    Suspendable computations

    View Slide

  13. LiveData
    Not fully complete (only supports MainThread)

    View Slide

  14. RxJava
    Complete, but:

    - Easy to misuse

    - Feels like an overkill

    - Learning curve

    View Slide

  15. Coroutines
    - Simplified

    - Comprehensive

    - Robust

    First class support from Google in Jetpack

    View Slide

  16. Coroutines
    In a nutshell:

    Coroutines simplify async code by replacing callbacks

    View Slide

  17. Coroutines
    blocking.kt
    fun loadUser() {
    val user = api.fetchUser()
    show(user)
    }
    fetchUser
    onDraw
    onDraw
    onDraw
    onDraw
    Show

    View Slide

  18. Coroutines
    async.kt
    fun loadUser() {
    api.fetchUser { user ->
    show(user)
    }
    }
    fetchUser
    onDraw
    onDraw
    onDraw
    onDraw
    Show
    Ready!

    View Slide

  19. Coroutines
    coroutines.kt
    suspend fun loadUser() {
    val user = api.fetchUser()
    show(user)
    }
    fetchUser
    onDraw
    onDraw
    onDraw
    onDraw
    Show
    Ready!
    suspend
    resume

    View Slide

  20. Coroutines
    Suspend and resume replace callbacks

    View Slide

  21. Coroutines
    suspend fun loadData(): Data
    fun loadData(listener: Continuation)

    View Slide

  22. Coroutines

    View Slide

  23. Coroutines
    coroutines.kt
    suspend fun loadUser() {
    val user = api.fetchUser()
    show(user)
    }
    async.kt
    fun loadUser() {
    api.fetchUser { user ->
    show(user)
    }
    }

    View Slide

  24. Coroutines
    coroutines.kt
    suspend fun loadUser() {
    val user = api.fetchUser()
    show(user)
    }
    suspend fun fetchUser() =
    withContext(Dispatchers.IO) {
    }

    View Slide

  25. Dispatchers
    .Default

    .IO

    .Main

    CPU
    Network, Disk
    Main Thread on Android

    View Slide

  26. Coroutines
    suspend fun fetchUser() =
    withContext(Dispatchers.IO) {
    /*put your blocking calls here*/
    }

    View Slide

  27. Under the hood
    suspend fun loadUser() {
    val user = api.fetchUser()
    show(user)
    }
    Main Thread Stack
    >
    Suspend marker
    Everything above a coroutine
    Everything underneath a regular function
    loadUser()
    api.fetchUser()

    View Slide

  28. Under the hood
    suspend fun loadUser() {
    val user = api.fetchUser()
    show(user)
    }
    Main Thread Stack
    suspend
    resume
    api.fetchUser()
    loadUser()

    View Slide

  29. Under the hood
    suspend fun loadUser() {
    val user = api.fetchUser()
    show(user)
    }
    Main Thread Stack
    suspend
    resume
    api.fetchUser()
    loadUser()

    View Slide

  30. Summarizing…
    -They replace callbacks

    - They provide Main Thread safety

    View Slide

  31. Building blocks
    withContext() launch() async()
    T Job Deferred
    Returns result Fire and forget Returns non-blocking

    future
    Uncaught exception

    Crashes app

    Uncaught exception

    Crashes app

    Uncaught exception is

    returned in deferred

    View Slide

  32. Building blocks
    CoroutineContext
    UI CommonPool Unconfined
    Dispatch execution into

    Android MainThread
    Dispatch execution into

    Android Background
    Dispatch execution into

    Current thread

    View Slide

  33. Launch

    View Slide

  34. Launch
    val uiContext: CoroutineContext = UI
    val bgContext: CoroutineContext = CommonPool

    View Slide

  35. Launch
    private fun loadData() {
    view.showLoading()
    val result = dataProvider.provideData()
    view.showData(result)
    view.hideLoading()
    }

    View Slide

  36. Launch
    private fun loadData() = launch(uiContext) {
    view.showLoading()
    val result = dataProvider.provideData()
    view.showData(result)
    view.hideLoading()
    }

    View Slide

  37. Launch
    private fun loadData() = launch(uiContext) {
    view.showLoading()
    val result = withContext(bgContext) { dataProvider.provideData() }
    view.showData(result)
    view.hideLoading()
    }

    View Slide

  38. Launch two tasks
    sequentially
    private fun loadData() = launch(uiContext) {
    view.showLoading() // ui thread
    // non ui thread, suspend until task is finished
    val result1 = withContext(bgContext) { dataProvider.provideData() }
    // non ui thread, suspend until task is finished
    val result2 = withContext(bgContext) { dataProvider.provideData() }
    val result = "$result1 $result2" // ui thread
    view.showData(result)
    view.hideLoading()
    }

    View Slide

  39. Launch two tasks
    sequentially
    private fun loadData() = launch(uiContext) {
    view.showLoading() // ui thread
    // non ui thread, suspend until task is finished
    val result1 = withContext(bgContext) { dataProvider.provideData() }
    // non ui thread, suspend until task is finished
    val result2 = withContext(bgContext) { dataProvider.provideData() }
    val result = "$result1 $result2" // ui thread
    view.showData(result)
    view.hideLoading()
    }

    View Slide

  40. Launch two tasks in parallel
    private fun loadData() = launch(uiContext) {
    view.showLoading() // ui thread
    val task1 = async(bgContext) { dataProvider.provideData() }
    val task2 = async(bgContext) { dataProvider.provideData() }
    // non ui thread, suspend until finished
    val result = "${task1.await()} ${task2.await()}"
    view.showData(result) // ui thread
    view.hideLoading()
    }

    View Slide

  41. Launch two tasks in parallel
    private fun loadData() = launch(uiContext) {
    view.showLoading() // ui thread
    val task1 = async(bgContext) { dataProvider.provideData() }
    val task2 = async(bgContext) { dataProvider.provideData() }
    // non ui thread, suspend until finished
    val result = "${task1.await()} ${task2.await()}"
    view.showData(result) // ui thread
    view.hideLoading()
    }

    View Slide

  42. Launch two tasks in parallel
    private fun loadData() = launch(uiContext) {
    view.showLoading() // ui thread
    val deferred = listOf(
    async(bgContext) { dataProvider.provideData() }
    async(bgContext) { dataProvider.provideData() }
    )
    deferred.awaitAll()
    }

    View Slide

  43. Launch coroutine with
    timeout
    private fun loadData() = launch(uiContext) {
    view.showLoading() // ui thread
    // non ui thread, suspend until the task is finished
    // or return null in 2 sec
    val result = withTimeoutOrNull(2, TimeUnit.SECONDS) {
    withContext(bgContext) { dataProvider.provideData() }
    }
    view.showData(result) // ui thread
    view.hideLoading()
    }

    View Slide

  44. Launch coroutine with
    timeout
    private fun loadData() = launch(uiContext) {
    view.showLoading() // ui thread
    // non ui thread, suspend until the task is finished
    // or return null in 2 sec
    val result = withTimeoutOrNull(2, TimeUnit.SECONDS) {
    withContext(bgContext) { dataProvider.provideData() }
    }
    view.showData(result) // ui thread
    view.hideLoading()
    }

    View Slide

  45. Cancelation

    View Slide

  46. Cancel a coroutine
    private var job: Job? = null
    fun onNextClicked() {
    job = loadData()
    }
    fun onBackClicked() {
    job?.cancel()
    }
    private fun loadData() = launch(uiContext) {
    // code
    }

    View Slide

  47. Cancel a coroutine
    private var job: Job? = null
    fun onNextClicked() {
    job = loadData()
    }
    fun onBackClicked() {
    job?.cancel()
    }
    private fun loadData() = launch(uiContext) {
    // code
    }

    View Slide

  48. Cancel a coroutine
    private var job: Job? = null
    fun onNextClicked() {
    job = loadData()
    }
    fun onBackClicked() {
    job?.cancel()
    }
    private fun loadData() = launch(uiContext) {
    // code
    }

    View Slide

  49. More with jobs
    job?.isActive
    job?.isCancelled
    job?.isComplete
    job?.getCancellationException()
    job?.children
    job?.cancelChildren
    job?.invokeOnCompletion {}

    View Slide

  50. Error Handling

    View Slide

  51. Try-catch
    private fun loadData() = launch(uiContext) {
    view.showLoading() // ui thread
    try {
    val result = withContext(bgContext) { dataProvider.provideData() }
    view.showData(result) // ui thread
    } catch (e: IllegalArgumentException) {
    e.printStackTrace()
    }
    view.hideLoading()
    }

    View Slide

  52. Store inside deferred
    private fun loadData() = async(uiContext) {
    view.showLoading() // ui thread
    val task = async(bgContext) { dataProvider.provideData() }
    val result = task.await() // non ui thread
    view.showData(result) // ui thread
    view.hideLoading()
    }

    View Slide

  53. Store inside deferred
    var job: Deferred = loadData()
    job.invokeOnCompletion { it: Throwable? ->
    it?.printStackTrace()
    }

    View Slide

  54. Exception Handler
    val exceptionHandler= CoroutineExceptionHandler { _, throwable ->
    throwable.printStackTrace()
    }
    private fun loadData() = launch(uiContext + exceptionHandler) {
    // code
    }

    View Slide

  55. Return null if exception
    suspend fun Deferred.awaitSafe(): T? = try {
    await()
    } catch (e: Exception) {
    e.printStackTrace()
    null
    }

    View Slide

  56. Return null if exception
    private fun loadData() = launch(uiContext) {
    val task = async(bgContext) { dataProvider.provideData() }
    val result = task.awaitSafe()
    if(result != null) {
    // success
    } else {
    // failure
    }
    }

    View Slide

  57. Return
    suspend fun Deferred.awaitResult(): Result = try {
    Result(success = await(), failure = null)
    } catch (e: Exception) {
    Result(success = null, failure = e)
    }
    data class Result(val success: T?, val failure: Exception?)

    View Slide

  58. Return
    private fun loadData() = launch(uiContext) {
    val task = async(bgContext) { dataProvider.provideData() }
    val (success, failure) = task.awaitResult()
    if(success != null) {
    // success T
    } else {
    // failure Exception
    }
    }

    View Slide

  59. Testing

    View Slide

  60. Testing
    class MainPresenter(val uiContext: CoroutineContext = UI,
    val bgContext: CoroutineContext = CommonPool) {
    fun loadData() = launch(uiContext) { ... }
    }

    View Slide

  61. Testing
    @Test
    fun test() {
    val presenter = MainPresenter(Unconfined, Unconfined)
    // test
    presenter.loadData()
    // verify
    verify(mockView).showLoading()
    verify(mockDataProvider).provideData()
    }

    View Slide

  62. Does this replace RxJava?

    View Slide

  63. RxJava analogies
    SubscribeOn = Initial Context
    ObserveOn = withContext
    Disposable <> Jobs

    View Slide

  64. More
    // retrofit 2
    interface MyService {
    @GET("/user")
    fun getUser(): Deferred
    // or
    @GET("/user")
    fun getUser(): Deferred>
    }

    View Slide

  65. More
    @GET("users/{id}")
    suspend fun user(@Path("id") id: Long): User

    View Slide

  66. Room
    @Dao
    interface UsersDao {
    @Query("SELECT * FROM users")
    suspend fun getUsers(): List
    @Query("UPDATE users SET age = age + 1 WHERE userId = :userId")
    suspend fun incrementUserAge(userId: String)
    @Insert
    suspend fun insertUser(user: User)
    @Update
    suspend fun updateUser(user: User)
    @Delete
    suspend fun deleteUser(user: User)
    }

    View Slide

  67. Room
    @Dao
    abstract class UsersDao {
    @Transaction
    open suspend fun setLoggedInUser(loggedInUser: User) {
    deleteUser(loggedInUser)
    insertUser(loggedInUser)
    }
    @Query("DELETE FROM users")
    abstract fun deleteUser(user: User)
    @Insert
    abstract suspend fun insertUser(user: User)
    }

    View Slide

  68. Room
    class Repository(val database: MyDatabase) {
    suspend fun clearData(){
    database.withTransaction {
    database.userDao().deleteLoggedInUser() // suspend function
    database.commentsDao().deleteComments() // suspend function
    }
    }
    }

    View Slide

  69. Room - Testing
    @Test fun insertAndGetUser() = runBlocking {
    // Given a User that has been inserted into the DB
    userDao.insertUser(user)
    // When getting the Users via the DAO
    val usersFromDb = userDao.getUsers()
    // Then the retrieved Users matches the original user object
    assertEquals(listOf(user), userFromDb)
    }

    View Slide

  70. Room - Testing

    View Slide

  71. Kotlin/Native
    Common
    JVM Native
    iOS
    framework
    Your iOS dev
    Your
    Android Dev
    JS

    View Slide

  72. Considerations in state
    An object belongs to one thread

    View Slide

  73. Considerations in state
    Frozen objects can be shared by threads

    View Slide

  74. Concurrency - frozen objects
    Everything you have written until now is not
    frozen

    View Slide

  75. Considerations in state

    View Slide

  76. Survey

    View Slide

  77. What to do?
    • Greenfield project? Probably try

    View Slide

  78. What to do?
    • Existing project?

    - Do they suit you?

    - Refactoring?

    - Time?

    - Small sections?

    View Slide

  79. Further resources
    Kotlin Slack (kotlinlang.slack.com)
    Kotlin Weekly (http://kotlinweekly.net)
    Coroutines (https://github.com/Kotlin/kotlinx.coroutines)
    Coroutines Guide (https://kotlinlang.org/docs/reference/coroutines/coroutines-guide.html)
    Codelabs (https://codelabs.developers.google.com/codelabs/kotlin-coroutines/#0)

    View Slide

  80. Further resources

    View Slide

  81. Further resources

    View Slide

  82. View Slide