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

Structured Concurrency - Droidcon Lisbon 2019

Manuel Vivo
September 09, 2019

Structured Concurrency - Droidcon Lisbon 2019

Before Kotlin Coroutines (async programming in Kotlin) was stable, they were launched in a way that made them difficult to maintain and track down.

Structured Concurrency introduced changes to solve these problems but also involved an ideological shift in order to use them.

In this talk, you'll learn how to do async programming in Kotlin using Structured Concurrency principles covering topics such as launching coroutines, scopes, error handling and testing.

Manuel Vivo

September 09, 2019
Tweet

More Decks by Manuel Vivo

Other Decks in Science

Transcript

  1. fun loadData() { networkRequest { data -> show(data) } }

    Async with callbacks networkRequest show onDraw onDraw onDraw Network thread
  2. fun networkRequest(onSuccess: (Data) -> Unit) { DefaultScheduler.execute { // Blocking

    network request code postToMainThread(onSuccess(result)) } } Async with callbacks
  3. fun networkRequest(onSuccess: (Data) -> Unit) { DefaultScheduler.execute { // Blocking

    network request code postToMainThread(onSuccess(result)) } } Async with callbacks
  4. fun networkRequest(onSuccess: (Data) -> Unit) { DefaultScheduler.execute { // Blocking

    network request code postToMainThread(onSuccess(result)) } } Async with callbacks
  5. fun loadData() { networkRequest { data -> anotherRequest(data) { otherData

    -> networkRequest { data -> anotherRequest(data) { otherData -> networkRequest { data -> anotherRequest(data) { otherData -> networkRequest { data -> anotherRequest(data) { otherData -> networkRequest { data -> anotherRequest(data) { otherData -> networkRequest { data -> anotherRequest(data) { otherData -> networkRequest { data -> anotherRequest(data) { otherData -> networkRequest { data -> anotherRequest(data) { otherData -> networkRequest { data -> anotherRequest(data) { otherData -> networkRequest { data -> anotherRequest(data) { otherData -> // Hey there! You want more? } } } } } Callback Hell fun loadData() { networkRequest { data -> anotherRequest(data) { otherData ->
  6. suspend fun loadData() { val data = networkRequest() show(data) }

    Async with coroutines onDraw onDraw onDraw Network thread networkRequest show
  7. suspend fun loadData() { val data = networkRequest() show(data) }

    suspend fun networkRequest(): Data = withContext(Dispatchers.IO) { }
  8. suspend fun loadData() { val data = networkRequest() show(data) }

    suspend fun networkRequest(): Data = withContext(Dispatchers.IO) { // Blocking network request code } Dispatchers.IO
  9. suspend fun loadData() { val data = networkRequest() show(data) }

    suspend fun networkRequest(): Data = withContext(Dispatchers.IO) { // Blocking network request code }
  10. suspend fun loadData() { val data = networkRequest() show(data) }

    suspend fun networkRequest(): Data = withContext(Dispatchers.IO) { // Blocking network request code } Main Safe
  11. Asynchronicity expressed as sequential code that is easy to read

    and reason about Other perks: exception handling and cancellation ❤ Coroutine
  12. suspend fun loadData() { val data = networkRequest() show(data) }

    fun onButtonClicked() { loadData() } Suspend fun ‘loadData’ must be called from a coroutine
  13. suspend fun loadData() { val data = networkRequest() show(data) }

    fun onButtonClicked() { launch { loadData() } }
  14. suspend fun loadData() { val data = networkRequest() show(data) }

    fun onButtonClicked() { launch { loadData() } } Who can cancel this execution? Does it follow a particular lifecycle? Who gets exceptions if it fails?
  15. fun onButtonClicked() { launch { loadData() } } Launch must

    be called in a scope // MyViewModel.kt
  16. fun onButtonClicked() { scope.launch { loadData() } } val scope

    = CoroutineScope(Dispatchers.Main) // MyViewModel.kt
  17. fun onButtonClicked() { scope.launch { loadData() } } val scope

    = CoroutineScope( Dispatchers.Main + Job() ) // MyViewModel.kt
  18. Scope with a Job When a child fails, it propagates

    cancellation to other (scope) children When a failure is notified, the scope propagates the exception up
  19. fun onButtonClicked() { scope.launch { loadData() } } val scope

    = CoroutineScope( Dispatchers.Main + Job() ) // MyViewModel.kt
  20. fun onButtonClicked() { scope.launch { loadData() } } val scope

    = CoroutineScope( Dispatchers.Main + SupervisorJob() ) // MyViewModel.kt
  21. Scope with a SupervisorJob The failure of a child doesn’t

    affect other (scope) children When a failure is notified, the scope doesn’t do anything
  22. fun onButtonClicked() { scope.launch { loadData() } } val scope

    = CoroutineScope( Dispatchers.Main + SupervisorJob() ) // MyViewModel.kt
  23. Async Creates a new Coroutine Returns a value suspend fun

    getUser(userId: String): User = coroutineScope { val deferred = async(Dispatchers.IO) { userService.getUser(userId) } deferred.await() }
  24. Async Creates a new Coroutine Returns a value suspend fun

    getUser(userId: String): User = coroutineScope { val deferred = async(Dispatchers.IO) { userService.getUser(userId) } deferred.await() }
  25. Async Creates a new Coroutine Returns a value suspend fun

    getUser(userId: String): User = coroutineScope { val deferred = async(Dispatchers.IO) { userService.getUser(userId) } deferred.await() }
  26. Async Creates a new Coroutine Returns a value suspend fun

    getUser(userId: String): User = coroutineScope { val deferred = async(Dispatchers.IO) { userService.getUser(userId) } deferred.await() }
  27. Async Creates a new Coroutine Returns a value suspend fun

    getUser(userId: String): User = coroutineScope { val deferred = async(Dispatchers.IO) { userService.getUser(userId) } deferred.await() }
  28. Async Creates a new Coroutine Returns a value suspend fun

    getUser(userId: String): User = coroutineScope { val deferred = async(Dispatchers.IO) { userService.getUser(userId) } deferred.await() }
  29. Creates a new Coroutine Creates a new Coroutine Fire and

    Forget Returns a value Launch Async
  30. Creates a new Coroutine Creates a new Coroutine Takes a

    Dispatcher Takes a Dispatcher Fire and Forget Returns a value Launch Async
  31. Creates a new Coroutine Creates a new Coroutine Takes a

    Dispatcher Takes a Dispatcher Executed in a Scope Executed in a Scope Fire and Forget Returns a value Launch Async
  32. Creates a new Coroutine Creates a new Coroutine Takes a

    Dispatcher Takes a Dispatcher Executed in a Scope Executed in a Scope Not a suspend function Not a suspend function Fire and Forget Returns a value Launch Async
  33. Creates a new Coroutine Creates a new Coroutine Takes a

    Dispatcher Takes a Dispatcher Executed in a Scope Executed in a Scope Re-throws exceptions Not a suspend function Not a suspend function Fire and Forget Returns a value Launch Async
  34. Creates a new Coroutine Creates a new Coroutine Takes a

    Dispatcher Takes a Dispatcher Executed in a Scope Executed in a Scope Not a suspend function Not a suspend function Holds on exceptions until await is called Fire and Forget Returns a value Launch Async Re-throws exceptions
  35. suspend fun getUser(userId: String): User { coroutineScope { val deferred

    = async(Dispatchers.IO) { userService.getUser(userId) } try { deferred.await() } catch(e: Exception) { // Handle exception } } }
  36. suspend fun getUser(userId: String): User { coroutineScope { val deferred

    = async(Dispatchers.IO) { userService.getUser(userId) } try { deferred.await() } catch(e: Exception) { // Handle exception } } }
  37. suspend fun getUser(userId: String): User { coroutineScope { val deferred

    = async(Dispatchers.IO) { userService.getUser(userId) } try { deferred.await() } catch(e: Exception) { // Handle exception } } }
  38. Check if the coroutine is Active scope.launch(Dispatchers.IO) { for (name

    in files) { if (!isActive) break readFile(name) } }
  39. @Test fun `Test loadData happy path`() = runBlocking { val

    viewModel = MyViewModel() viewModel.loadData() // Assert show did something } // MyViewModelTest.kt
  40. class MyViewModel { val scope = CoroutineScope( Dispatchers.Main + SupervisorJob()

    ) fun onButtonClicked() { scope.launch { loadData() } } }
  41. class MyViewModel { val scope = CoroutineScope( Dispatchers.Main + SupervisorJob()

    ) fun onButtonClicked() { scope.launch { loadData() } } }
  42. @Test fun `Test loadData happy path`() = runBlocking { val

    viewModel = MyViewModel() viewModel.onButtonClicked() // Assert show did something } // MyViewModelTest.kt
  43. @Test fun `Test loadData happy path`() = runBlocking { val

    viewModel = MyViewModel() viewModel.onButtonClicked() // Assert show did something } // MyViewModelTest.kt
  44. @Test fun `Test loadData happy path`() { val viewModel =

    MyViewModel() viewModel.onButtonClicked() // Wait for the result -> using a CountDownLatch // LiveDataTestUtil, Mockito await, etc. } // MyViewModelTest.kt
  45. @Test fun `Test loadData happy path`() { val viewModel =

    MyViewModel() viewModel.onButtonClicked() // Wait for the result -> using a CountDownLatch // LiveDataTestUtil, Mockito await, etc. } // MyViewModelTest.kt Code Smell!! Bad practice
  46. class MyViewModel { val scope = CoroutineScope( Dispatchers.Main + SupervisorJob()

    ) fun onButtonClicked() { scope.launch { loadData() } } }
  47. class MyViewModel( private val dispatcher: CoroutineDispatcher ) { val scope

    = CoroutineScope( Dispatchers.Main + SupervisorJob() ) fun onButtonClicked() { scope.launch(dispatcher) { loadData() } } }
  48. class MyViewModel( private val dispatcher: CoroutineDispatcher ) { val scope

    = CoroutineScope( Dispatchers.Main + SupervisorJob() ) fun onButtonClicked() { scope.launch(dispatcher) { loadData() } } }
  49. val testDispatcher = TestCoroutineDispatcher() @Test fun `Test loadData happy path`()

    = testDispatcher.runBlockingTest { val viewModel = MyViewModel(testDispatcher) viewModel.onButtonClicked() // Assert show did something } // MyViewModelTest.kt
  50. val testDispatcher = TestCoroutineDispatcher() @Test fun `Test loadData happy path`()

    = testDispatcher.runBlockingTest { val viewModel = MyViewModel(testDispatcher) viewModel.onButtonClicked() // Assert show did something } // MyViewModelTest.kt
  51. val testDispatcher = TestCoroutineDispatcher() @Test fun `Test loadData happy path`()

    = testDispatcher.runBlockingTest { val viewModel = MyViewModel(testDispatcher) viewModel.onButtonClicked() // Assert show did something } // MyViewModelTest.kt
  52. class MyViewModel( private val dispatcher: CoroutineDispatcher ) { val scope

    = CoroutineScope( Dispatchers.Main + SupervisorJob() ) fun onButtonClicked() { // Do something else scope.launch(dispatcher) { loadData() } } }
  53. val testDispatcher = TestCoroutineDispatcher() @Test fun `Test loadData happy path`()

    = testDispatcher.runBlockingTest { val viewModel = MyViewModel(testDispatcher) testDispatcher.pauseDispatcher() viewModel.onButtonClicked() // Assert onButtonClicked did something else } // MyViewModelTest.kt
  54. val testDispatcher = TestCoroutineDispatcher() @Test fun `Test loadData happy path`()

    = testDispatcher.runBlockingTest { val viewModel = MyViewModel(testDispatcher) testDispatcher.pauseDispatcher() viewModel.onButtonClicked() // Assert onButtonClicked did something else testDispatcher.resumeDispatcher() // Assert show did something } // MyViewModelTest.kt
  55. What we covered What problems Coroutines solve Dispatchers & withContext

    What a Coroutine is How coroutines work under the hood Principles of Structured Concurrency How to create Coroutines Exception Handling When to mark a function as suspend Testing Coroutines & TestCoroutineDispatcher