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

Introduction to Kotlin Coroutines

Introduction to Kotlin Coroutines

Introduction to kotlin coroutines, talk given at Berlin Android Monthly meetup @Berlindroid

Adegeye Mayowa

December 07, 2018
Tweet

More Decks by Adegeye Mayowa

Other Decks in Programming

Transcript

  1. fun login(username: String, password: String): User fun getOrders(userId: String): List<Orders>

    fun fetchUserOrders() { val user = login(username, password) val orders = getOrders(user.userId) showUserOrders(orders) }
  2. fun login(username: String, password: String): User fun getOrders(userId: String): List<Orders>

    fun fetchUserOrders() { val user = login(username, password) val orders = getOrders(user.userId) showUserOrders(orders) } I/O calls
  3. fun login(username: String, password: String): User fun getOrders(userId: String): List<Orders>

    fun fetchUserOrders() { val user = login(username, password) val orders = getOrders(user.userId) showUserOrders(orders) } UI thread
  4. Callback fun fetchUserOrders(username: String, password: String) { login(username, password) {

    user -> getOrders(user.userId) { orders -> showUserOrders(orders) } } }
  5. RxJava fun fetchUserOrders(username: String, password: String) { login(username, password) .flatMap

    { user → getOrders(user.userId) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.main()) .subscribe({orders -> showUserOrders(orders)}) }
  6. Is it possible to write async code in a sequential

    manner without getting lost in the implementation details ?
  7. What is a coroutine • A lightweight thread that can

    run a computation without blocking.
  8. What is a coroutine • A lightweight thread that can

    run a computation without blocking. • It can be suspended and resumed at a later time.
  9. What is a coroutine • A lightweight thread that can

    run a computation without blocking. • It can be suspended and resumed at a later time. • Suspension points are defined by the programmer.
  10. Suspending function • A function that can suspend the execution

    of a coroutine • It can be started, paused and resumed at a later time
  11. Suspending function...contd • They can only be called within a

    coroutine or from another suspending function
  12. Suspending function • They can only be called within a

    coroutine or from another suspending function • They can take parameters and have return types
  13. fun doHeavyComputation(param: Int,callback:Continuation<Int>) interface Continuation<in T> { public val context:

    CoroutineContext public fun resumeWith(result: Result<T>) } fun<T> Continuation<T>.resume(value: T) { } fun<T> Continuation<T>.resumeWithException(exception: Throwable)
  14. Coroutine Dispatchers • CoroutineContext Element that determines the thread a

    coroutine runs on. • It can confine a coroutine to a specific thread, a threadpool or let it run unconfined.
  15. Coroutine Builders Acts as the bridge that allows us to

    call suspending functions from our code.
  16. Coroutine Builders Acts as the bridge that allows us to

    call suspending functions from our code. • launch - fire and forget
  17. Coroutine Builders Acts as the bridge that allows us to

    call suspending functions from our code. • launch - fire and forget • async - returns a Deferred<T>
  18. launch fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, Start: CoroutineStart =

    CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit ): Job
  19. async fun <T> CoroutineScope.async( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart

    = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> T ): Deferred<T>
  20. Async • You need to call await() on the returned

    Deferred<T> to get the actual result.
  21. Async • You need to call await() on the returned

    Deferred<T> to get the actual result. • async...await can be used to run several tasks in parallel.
  22. suspend fun firstValue(): Int { delay(500) //delays for 500ms return

    10 } suspend fun secondValue(): Int { delay(300) //delays for 300ms return 20 }
  23. suspend fun firstValue(): Int { delay(500) //delays for 500ms return

    10 } suspend fun secondValue(): Int { delay(300) //delays for 300ms return 20 } fun main() = runBlocking { val time = measureTimeMillis { val first = async { firstValue() } val second = async { secondValue() } val sum = first.await() + second.await() } println(time) }
  24. suspend fun firstValue(): Int { delay(500) //delays for 500ms return

    10 } suspend fun secondValue(): Int { delay(300) //delays for 300ms return 20 } fun main() = runBlocking { val time = measureTimeMillis { val first = async { firstValue() } val second = async { secondValue() } val sum = first.await() + second.await() } println(time) }
  25. suspend fun firstValue(): Int { delay(500) //delays for 500ms return

    10 } suspend fun secondValue(): Int { delay(300) //delays for 300ms return 20 } fun main() = runBlocking { val time = measureTimeMillis { val first = async { firstValue() } val second = async { secondValue() } val sum = first.await() + second.await() } println(time) }
  26. suspend fun firstValue(): Int { delay(500) //delays for 500ms return

    10 } suspend fun secondValue(): Int { delay(300) //delays for 300ms return 20 } fun main() = runBlocking { val time = measureTimeMillis { val first = async { firstValue() } val second = async { secondValue() } val sum = first.await() + second.await() } println(time) //prints 518 }
  27. withContext Can be used to run a block of code

    that returns in a value in another context.
  28. withContext Can be used to run a block of code

    that returns in a value in another context. It suspends until completion and returns the result
  29. Get user orders revisited fun login(username: String, password: String): User

    fun getOrders(userId: String): List<Orders> fun fetchUserOrders() { val user = login(username, password) val orders = getOrders(user.userId) showUserOrders(orders) }
  30. Get user orders revisited suspend fun login(username: String, password: String):

    User { return withContext(Dispatcher.IO) { //returns User } }
  31. Get user orders revisited suspend fun login(username: String, password: String):

    User { return withContext(Dispatcher.IO) { //returns User } } suspend fun getOrders(userId: String): List<Orders> { return withContext(Dispatchers.IO) { //return list of orders } }
  32. Get user orders revisited fun fetchUserOrders() = GlobalScope.launch(Dispatchers.MAIN){ val user

    = login(username, password) val orders = getOrders(user.userId) showUserOrders(orders) }
  33. Get user orders revisited fun fetchUserOrders() = GlobalScope.launch(Dispatchers.MAIN){ val user

    = login(username, password) //first suspension point val orders = getOrders(user.userId) showUserOrders(orders) }
  34. Get user orders revisited fun fetchUserOrders() = GlobalScope.launch(Dispatchers.MAIN){ val user

    = login(username, password) val orders = getOrders(user.userId) //second suspension point showUserOrders(orders) }
  35. Get user orders revisited fun fetchUserOrders() = GlobalScope.launch(Dispatchers.MAIN){ val user

    = login(username, password) val orders = getOrders(user.userId) showUserOrders(orders) } back on main thread and can update the UI
  36. Structured concurrency • All coroutine builders are extension function of

    CoroutineScope • We need to be able to keep track of all coroutines launched
  37. Structured concurrency • All coroutine builders are extension function of

    CoroutineScope • We need to be able to keep track of all coroutines launched • CoroutineScope should be implemented by an object with a lifeycle i.e Activity, Fragment, ViewModel
  38. class MainActivity: AppCompatActivity(), CoroutineScope { private lateint var job override

    val coroutineContext: CoroutineContext get() = Dispatchers.Main private fun loadData() { } }
  39. class MainActivity: AppCompatActivity(), CoroutineScope { private lateint var job override

    val coroutineContext: CoroutineContext get() = Dispatchers.Main override fun onCreate(savedInstanceState: Bundle?) { job = Job() } private fun loadData() { } }
  40. class MainActivity: AppCompatActivity(), CoroutineScope { private lateint var job override

    val coroutineContext: CoroutineContext get() = Dispatchers.Main override fun onCreate(savedInstanceState: Bundle?) { job = Job() } override fun onDestroy() { job.cancel() } }
  41. class MainActivity: AppCompatActivity(), CoroutineScope { private lateint var job override

    val coroutineContext: CoroutineContext get() = Dispatchers.Main + job override fun onCreate(savedInstanceState: Bundle?) { job = Job() } override fun onDestroy() { job.cancel() } }
  42. class MainActivity: AppCompatActivity(), CoroutineScope { private lateint var job override

    val coroutineContext: CoroutineContext get() = Dispatchers.Main + job override fun onCreate(savedInstanceState: Bundle?) { job = Job() } override fun onDestroy() { job.cancel() } }
  43. Conventions • Functions declared as extension function on CoroutineScope should

    return immediately and executes its task concurrently with the rest of the program.
  44. Conventions • Functions declared as extension function on CoroutineScope should

    return immediately and executes its task concurrently with the rest of the program. • Suspending functions should do its task separately in another context and return to caller when done. It should not block the caller.
  45. Conventions • Suspending functions should do its task separately in

    another context and return to caller when done. It should not block the caller. • Functions should not be both suspending and an extension function on coroutine scope.
  46. Exception Handling • Wrap suspend functions or deferred.await() in a

    try..catch block • Launch coroutine builder propagates exceptions as an unhandled exception, while async exposes the exception to the user to consume.
  47. val job = GlobalScope.launch { println("Throwing exception from launch") throw

    IndexOutOfBoundsException() } val deferred = GlobalScope.async { println("Throwing exception from async") throw ArithmeticException() }
  48. val job = GlobalScope.launch { println("Throwing exception from launch") throw

    IndexOutOfBoundsException() //exception will be printed to console } val deferred = GlobalScope.async { println("Throwing exception from async") throw ArithmeticException() // Nothing is printed, relying on user to call await }
  49. val deferred = GlobalScope.async { println("Throwing exception from async") throw

    ArithmeticException() // Nothing is printed, relying on user to call await } try { deferred.await() println("Unreached") } catch (e: ArithmeticException) { println("Caught ArithmeticException") //printed to the console }
  50. val exceptionHandler = CoroutineExceptionHandler { _, exception -> println(“Caught: $exception")

    } val job = GlobalScope.launch(exceptionHandler) { throw IOException(“error occurred”) } //console output: Caught: java.io.IOException: error occurred
  51. val exceptionHandler = CoroutineExceptionHandler { _, exception -> println(“Caught: $exception")

    } val job = GlobalScope.async(exceptionHandler) { throw IndexOutOfBoundsException(“error occurred”) } //exception will be swallowed and nothing printed to the console
  52. Testing Make your viewmodels, use cases or repositories explicitly take

    a Dispatcher and provide a replacement in your test.
  53. data class DispatcherProvider( val io: CoroutineDispatcher, val computation: CoroutineDispatcher, val

    main: CoroutineDispatcher ) @Provides @Singleton fun providesDispatchersProvider() = DispatcherProvider( io = Dispatchers.IO, computation = Dispatchers.Default, main = Dispatchers.Main )
  54. class FetchMoviesUseCase(private val moviesApi: MoviesApi, private val dispatcherProvider: DispatcherProvider) {

    suspend fun movies(): List<Movies> { return withContext(dispatcherProvider.io) { moviesApi.getMovies() } } }
  55. use Unconfined Dispatcher in test @Provides @Singleton fun providesTestDispatchers() =

    DispatcherProvider( io = Dispatchers.Unconfined, computation = Dispatchers.Unconfined, main = Dispatchers.Unconfined )
  56. TestCoroutineContext • use TestCoroutineContext if you deal with delays and

    timeout in your test. • Similar to TestScheduler in RxJava
  57. TestCoroutineContext • use TestCoroutineContext if you deal with delays and

    timeout in your test. • Similar to TestScheduler in RxJava • advanceTimeBy
  58. TestCoroutineContext • use TestCoroutineContext if you deal with delays and

    timeout in your test. • Similar to TestScheduler in RxJava • advanceTimeBy • advanceTimeTo
  59. TestCoroutineContext • use TestCoroutineContext if you deal with delays and

    timeout in your test. • Similar to TestScheduler in RxJava • advanceTimeBy • advanceTimeTo • triggerActions