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.