Kotlin Coroutines 101 - Android Conference Talks

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
MANUEL VIVO: Hi. I'm Manuel Vivo. I'm an Android engineer in the Developer Relations Team at Google. This video is about an introduction to coroutines. We will talk about the problems that coroutines are trying to solve, how to use coroutines in Android, how to test them, et cetera. The first thing we want to talk about is, what programs are they actually trying to solve? They basically simplify asynchronous programming on Android. So whenever we talk about asynchronous programming, we cannot forget about synchronous programming. Here, we have the function loadData, which is going to display on the screen the result of a networkRequest. And because this is synchronous, it means that it will block the thread where this has been running on. So imagine if we call loadData from the main UI thread, networkRequest will block their thread whenever it is waiting for that networkRequest to happen. And so in this case, if the operating system is trying to call onDraw, that is not going to happen. And therefore, the user will see a frozen UI and unresponsive application. It will get unblocked whenever the networkRequest finishes and it calls show, as we can see. Let's see the definition of networkRequest here. networkRequest is just a function that returns data. And inside it, we have the blocking network request code. Blocking the main UI thread is something that we shouldn't do in Android. So how can we move this networkRequest to a different thread? How can we make it asynchronous? A solution for this is using callbacks. We have the same version of loadData. We're just going to make the networkRequest on a different thread. Here, as we can see, if we execute this code, we can see that networkRequest will be called, and we will see later that the function networkRequest is going to be moving the execution of the networkRequest to a different thread. And in this case, the main UI thread is free, and the operating system can call onDraw and refresh the screen as much as it wants. Whenever the networkRequest finishes, it will call this lambda, a callback, that is going to display the result of the networkRequest on the screen. If we take a look at networkRequest, now it's just a function that doesn't return data anymore. Returns unit. And instead, it takes in a callback, a lambda that we call in this case onSuccess, with what to do after the networkRequest finishes. We can call DefaultScheduler.execute to move the execution of the networkRequest to a different thread. And after that finishes, then we can post to the main thread calling onSuccess. So while callbacks might be OK solution for some use cases, it has some problems. First thing is that now this is a simple function that it just makes another request and displays something on the screen. But now, the logic can get complicated. We start adding a nesting networkRequest after the other one happens. And now we can add more, and more, and more, and more stuff. This is what becomes the callback hell-- whenever there is a lot of indentation, error propagation might be difficult. You have hard-coded posting to main thread wherever something happens, and you might not want to do that. It's complicated. What if we could have the best of both worlds-- the simplicity of synchronous code with all the power from asynchronicity and moving things between threads? And this is where current things come in. Here, we have the same function, loadData, written with coroutines. It might look a little bit suspicious, and you might think it will block the main UI thread, but it won't. The only difference that we have is this suspend modifier in the function definition. That's basically telling the Kotlin compiler that this function needs to be executed within a coroutine. So how does it work? How can networkRequest not block the UI thread whenever it moves to a different thread? Well, coroutines can suspend execution without blocking the thread. And that's going to happen whenever we move to execute something to a different thread. So that's what we call the suspension point. But also, whenever that networkRequest finishes, then it will resume execution. So whenever the networkRequest is done with whatever it has to do, loadData can resume execution and continuing with the result of that networkRequest. If you take a closer look, you will see that apart from networkRequest, the rest of the function is that what we used to have before as a callback. So what is happening with the callback? The Kotlin compiler will write that callback for you under the hood when the computation can suspend. And actually, coroutines call those callbacks a continuation. Continuation is just a generated callback interface with extra information in it. So how is the compiler going to modify the function loadData? Well, it will take the suspend modifier and replace it by a parameter of type continuation. And continuation form this state machine in which it will be executed depending if the function is suspended or not. So we can say that when it starts, the function start, it will start with a state 0. When it will suspend, it will change state. When it will resume, it will be back to a different state, and then we will finish. This is what it's called continuation-passing style. And because it's quite a complicated topic, it is not going to be covered in this presentation. But as you can see, just a fancy way to say callbacks. So with coroutines, computations can get suspended without blocking the thread, as we said before. Back to our function loadData, we're going to see what networkRequest looks like, in this case. We will see that networkRequest is another suspend function, but now it returns data as the synchronous version of it. How can networkRequest the execution to a different thread? Well, it uses with context. With context is a suspend function from the coroutines that takes in a dispatcher as a parameter. Dispatcher is basically a way to say, hey, I want to run this computation in this particular thread. And in this case, in IO. And inside with context, we can have our blocking networkRequest code. It doesn't matter if it's blocking the IO thread. What is important is that it's not blocking the main UI thread. What other dispatchers do we have available? We have Dispatchers.IO, but also .Default and Dispatchers.Main. IO is optimized to do network and disk cooperations. They use default for CPU-intensive tasks and main for UI code or non-blocking code that executes quite fast. So here, networkRequest, we can say that this is main safe. You can call networkRequest from the main UI thread, and it will be OK, because it will be in chart to move the execution to a different thread. So now, we saw what problems coroutine are trained to solve, which is simply find asynchronous programming in Android. But what is a coroutine? You can think of a coroutine as a runnable with super powers. If you do think about it, it is going to take a block of code, and it will be able to run it in a particular thread. What I like about coroutines is that asynchronicity can be expressed in a sequential way. And that's going to be easier to read and understand. Also, it comes with other perks, such as exceptional handling and cancellation. That is typically more difficult to do with other versions-- for example, callbacks. Back to our function loadData. Imagine that we want to execute that whenever the user taps on a button. For example, we can have this function onButtonClicked that will trigger loadData. But this is not going to compile, because suspend functions must be called inside a coroutine. We don't know how to create coroutines yet. We will see that later. But in a nutshell, you can see we can use launch to trigger a coroutine. So imagine that this works. Here, some problems may arise. For example, who can cancel the execution of that coroutine? Does it follow a particular lifecycle? For example, if the [INAUDIBLE] is moving away from the screen, can you automatically cancel it? Who is going to get the exception if that fails? These are the questions that a structured concurrency is trying to solve. So structured concurrency is a design pattern system in coroutines that tries to solve memory leaks. And structured concurrency forces you to think about those questions whenever you are working with coroutines. And it does it by introducing this new concept, which is a scope-- a coroutine scope. Coroutine scope is going to keep track of the coroutines it creates. It's going to give you the ability to cancel them. And it's going to be notified whenever a failure happens. So now, back to our function onButtonClicked, that will actually give another compiler error. That's because launch must be called within a scope. How can we create a coroutine scope? A scope is just a simple variable that is really [INAUDIBLE] to credit. It is not going to hold references to heavy objects. So whenever you want to control the lifecycle of a coroutine, you can create a coroutine scope. So in this case, we can use this scope to trigger the computation. And that computation will follow, that coroutine will follow the lifecycle of that particular scope. So in this case, if loadData throws an exception, the scope will get that exception and will handle it in some way. Coroutines also create this hierarchy in which the root of the hierarchy, it's going to be the scope, which is going to be the parent of the other coroutines that it creates. So in this case, for example, whenever we don't need the scope anymore-- for example, if we are in a view model, whenever we call onCleared, we can call a scope.cancel. And cancelling a scope means that it will counsel all the children coroutines that it started, and it means as well that you cannot start more coroutines with that scope. So with our function loadData, we can see that this suspend function, because this will suspend, means that it has to be run inside a coroutine. It will run in a scope. And this is quite important. If we take a step back and we think about synchronous code again, we can see that when a function returns, it means that it has completed all work. So imagine about the synchronous version of loadData. Even it block the thread, it's OK, but it returned whenever it completed everything it had to do. With coroutines, it's kind of the same. When a suspend function returns, means that it has completed all work. And this is a very nice contract to have in asynchronous operations. So now we're going to see how to handle exceptions. Scopes can also take a job. And a job is going to define the lifecycle of either the scope, and also the coroutines. And whenever we passing a job to a scope, that means that it's going to handle exceptions in a particular way. When a child fails, it is going to provide the cancellation to other children. And when a failure is notified, the scope is going to cancel himself and propagate the exception up. That means that in this case, whenever loadData fails, it will notify the scope that it failed. The scope will cancel the other children that it may have created before it will cancel itself and will propagate the exception up. In this case, because we are in a view model and there is nothing else in the hierarchy, it will make your application crash. But this might not be desired in every situation. For example, here we are in a UI-related scope, and therefore, we might not want that behavior. So you can use the alternative, which is SupervisorJob. And with a SupervisorJob, the failure of a child, it is not going to affect other children. And so when a failure is notified, the scope is not going to do anything. So in this case, loadData, if it throws an exception, the scope will say, OK, I have these exceptions, but it's not going to cancel other children. But FYI, the exception can be propagated still, so you might have to try catch it. TL;DR for a structured concurrency. When I suspend function returns, it means that it has completed all work. When the scope is canceled, it means that the children will be canceled too. When the coroutine errors out, the scope will get notified, and depending on the job, it will behave in a way or another. We're going to see how to create coroutines now. Before, we saw that we can use launch to do that, but it is not the only way you can do it. You have to launch and async as well. We are going to compare them with the similarities and differences between these two different approaches. As I said, the first similarity is that they can create a new coroutine. They can start a computation where you can call suspend functions. But they create coroutines with a different purpose. So launch is going to create a coroutine that is meant to be fired and forgotten. So imagine that we have our loggingService where we can upload logs that happened in our application. So we can use this scope.launch to trigger that computation, and that's it. We don't care about it anymore. That's launch. Async is going to create a new coroutine that can return a value. So for example, we have this function, getUser, that takes in a user ID as a parameter, and it returns a user object. Because we are in a suspend function, we don't have a scope available, so if we want to create a coroutine, we can use coroutine scope. And inside here, we can fetch our user with our user service. And that computation can be happening in Dispatchers.IO, for example. So we have that. And that is going to-- async is going to return a deferred object. And you can think of a deferred object as a future or a promise in Java. And with that future, or that deferred object, you can call await. And await will wait-- actually, it will suspend execution of the coroutine until async has finished doing its computation, and it will return the value of that coroutine. And this is what we return back to getUser. Another similarity, as you saw, is that both of them take a dispatcher. Where do you want to run that computation? Also, both of them are executed in a scope. Launch and async are extension functions on the scope. On coroutine scope, in this case. And so in order to create [INAUDIBLE],, you need a scope. They are not a suspend functions, so launch and async are the entry point to coroutines. How you can create a coroutine so that you can call suspend functions, but they are not suspend functions. And they differ on the way they handle exceptions. So launch is going to throw the exception as soon as it happens, and async is going to hold on that exception until you call await. We're going to see more of that in a second. So how are you going to handle those exceptions? Basically with a try-catch. Here in our version of launch we had before, because loggingService can throw an exception, you can grab it inside a try-catch, and that will handle the exception thrown by the logging service. With async, it's a bit different. As we said, async is not going to throw the exception. Therefore, you don't need to grab it inside the try-catch. You have to do it with await. Await can throw the exception that happened in the async block of code. Therefore, you have to wrap it inside try-catch. And there you will handle that exception. Now we are going to move on to a different topic. I have this coroutine on the screen that is called with launch, and it's going to happen in an IO thread. So we have a for loop, which is going to read files from a list. What happens if we call scope.cancel? Is that going to cancel the execution of that coroutine or not? Well, the fact it is not going to do that, because cancellation requires cooperation. Whenever you are doing something very expensive-- in this case, we can spend a lot of time reading the files-- you have to cooperate and make this canceller work. If you think about it, the coroutines or the thread is going to be really busy reading files, and it is not going to be listening for cancellation. So in this case, you have to cooperate and check if the coroutine is active or not. And you can do that by checking, for example, or calling ensureActive or yield. Whenever the coroutine is cancelled and that function is called, then it will stop the execution of this coroutine. So again, if you are doing a heavy computation, make sure that you check for cancellation. We created a lot of functions throughout the presentation. And some of them were marked with suspend, some others weren't. When do you actually have to mark something as suspend? Well, this is easy. Whenever it calls other suspend functions. So imagine our loadData function we had before. It is a suspend function. Why is that a suspend function? Because it calls networkRequest. That is also a suspend function. Why is networkRequest a suspend function? Well, because it called with context. And with context, it is a suspend function that comes from the coroutines library. This is the reason why. But now, when don't you have to mark it as a suspend? Well, whenever it doesn't call other suspend functions. So onButtonClicked is just a function that triggers the coroutine. It calls launch. And because a launch is not as a suspend function, onButtonClicked doesn't need to be either. So the tip is don't mark a function suspend unless you are forced to. And in this case, you either mark it as a suspend or you start the coroutine, as we did with onButtonClicked. We're going to see how to test coroutines now. So testing asynchronous code is quite difficult, because you want a deterministic behavior. You want the test to behave always in the same way. And we're going to see different use cases for the different ways that you have to do that. The first use case, for those tests that don't trigger the execution of a new coroutine. So for example, let's say we want to test loadData. loadData, it is not calling async or launch. It's a suspend function. It's not triggering new coroutines. Even though it is moving to a different thread with context-- that is what networkRequest is doing-- it is not creating a new coroutine, so we are OK here. This is the first use case. You can test these using runBlocking. runBlocking is a method from the coroutines library that is going to start a coroutine, and it's going to block the thread where it's been called until everything finishes, until the block of code finishes. So in this case, we can create a viewModel instance called loadData, and loadData is going to be executed synchronously. And so we can make sure that the next line after loadData means that loadData has finished doing everything. So now we can assert that show did something. That was quite easy, right? The second use case is more complicated, and it's for tests that trigger new coroutines. So for example, we have MyViewModel, and we want to test onButtonClicked. onButtonClicked is triggering a new computation, is triggering a new coroutine calling launch. Can we use the same way as we did before runBlocking? Well, if we think about it, whenever you call onButtonClicked, it is going to run a new coroutine. And that coroutine might be potentially running on a different thread. And so it is going to happen asynchronously. So onButtonClicked is going to call launch, and it's going to move on. It's going to end the function. So whenever you call assert, it might be happening that the other coroutine is still running. Therefore, it's not reliable. You cannot use it. Another way is waiting for something to happen. So imagine that you update some result somehow, and you wait for that to have a new value. Either having a CountDownLatch, or using LiveDataTestUtil if you're using live data, or with Mockito await. You have different ways to wait for something to be there. But that's a code smell. That's because the test is not going to be running fast. Might take a couple of seconds for something to be there. And that's kind of a bad practice. The thing is that you might have to run it sequentially, as we did before with runBlocking. And how can you do that? Well, basically, we can do that by forcing the coroutine to run in a particular dispatcher. And a good practice for this is injecting dispatchers. So in this case for the viewModel, you can inject the dispatcher in the constructor, and then use that dispatcher to trigger the coroutine inside onButtonClicked. And now we know our tests, we can use a test coroutine dispatcher. We create this test coroutine dispatcher instance that we are going to pass into the viewModel. So that coroutine is going to be executed in this test coroutine dispatcher. And in the test, instead of using runBlocking to wrap our test body within it, we are going to use the function runBlockingTest from the testDispatcher. That means that every coroutine running on that dispatcher is going to be executed simultaneously. In that case, whenever we call onButtonClicked, it is going to execute that coroutine simultaneously. That means that whenever onButtonClicked finishes, means that the coroutine has finished too. And now you can assert whenever show did something without having to wait for that to happen. But now, imagine that onButtonClicked has to do something else before running the coroutine. How can we test that? Well, testCoroutineDispatcher comes with nice functions to be able to post execution of coroutines, and to resume them. So in this case, we can call testDispatcher.pauseDispatcher to assert that something happened before the coroutine, and then call testDispatcher.resumeDispatcher to test what happened after the coroutine run. So for testing, use runBlocking when the test doesn't create new coroutines. And as a good practice, inject dispatchers and use TestCoroutineDispatcher in tests whenever you want to test something that is going to trigger new coroutines. That's going to be the end of the video. We went through a lot of stuff. So we said what problems coroutines are trying to solve in Android, which is simplifying asynchronous programming. How we can move the execution to a different thread using dispatchers and withContext. What a coroutine is. Basically, you can think of it as a runnable with super powers. How coroutines work under the hood. The Kotlin compiler will rewrite the suspend functions to use callbacks. The principles of structure concurrency that is going to avoid memory leaks and is going to force you to think of who is going to manage the lifecycle of that coroutine whenever you are working with them. How to create coroutines, and the differences between launch and async. How to handle exceptions. When to mark a function as a suspend, or not. And testing coroutines, and the usage of TestCoroutineDispatcher. If you are interested in more things about coroutines, I suggest that you go through the different Codelabs that we have available. We have two of them-- a basic Codelab and advanced Codelab that uses live data, coroutine builder, and more advanced topics. If you are more interested in testing coroutines, there is a video at Android Developer Summit 2019 where I give a talk with [INAUDIBLE]. And now, for cancellation and exceptions, another video in KotlinConf 2019 with Florina Muntenescu. Thank you for watching. Bye.
Info
Channel: Android Developers
Views: 74,941
Rating: 4.9460239 out of 5
Keywords: Kotlin Coroutines 101, Kotlin 101, Kotlin, Kotlin Coroutines, what is Kotlin, how to use Kotlin, what are Kotlin Coroutines, how to use coroutines in Android, how to test coroutines, what can you use Kotlin Coroutines for, how to use Kotlin Coroutines, apps, Kotlin tutorial, Android talks, Google talks, Android, Android Devs, Android Developers, Google, Android Conference Talk, Manuel Vivo, GDS: Yes;
Id: ZTDXo0-SKuU
Channel Id: undefined
Length: 24min 49sec (1489 seconds)
Published: Thu Apr 02 2020
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.