Hi. Android 11 is currently in beta,
offering a bunch of new features, as well as deprecations.
I'm sure you are all familiar with the AsyncTask class, correct?
Just in case you haven't already moved to other solutions for async programming,
in Android 11, we've marked this class as deprecated.
One of the replacements listed in the documentation
is the cryptic Kotlin concurrency utilities,
which is, of course, Kotlin coroutines.
We've recently announced that coroutines
are the recommended solution for async code on Android.
For some time already, it's been our focus
to explore and deliver solutions based on coroutines
in the form of new APIs, samples, codelabs, and documentation.
In fact, many people in Android are falling in love with coroutines.
But why?
I tried to keep the list short,
as we are going to discuss some of it in more practical examples.
However, the unique properties of coroutines
make it especially well-suited for Android.
Structured concurrency helps developers scope their work
to the application's components lifecycle and prevents memory leaks.
Callback-free top-down code is much easier to read and understand.
Cancellation support and natural exception handling
make a great addition.
And finally, many coroutines can run on a single thread,
suspending their work instead of blocking, making them fast and lightweight.
We believe in coroutines on Android so much
that we decided to put them up on our redesigned website
as the fourth top reason to adopt Kotlin in your Android app.
Check it out if you haven't seen it already.
Okay, but let's talk about more practical examples.
A great API to look at is WorkManager from the JetPack libraries
because it comes in with all the possible async flavors.
The most basic one, simply called Worker,
already calls the method in which you do work on a background thread.
So you can do blocking operations like reading from a database,
uploading to a server,
and then writing the result to a database again.
At first glance, this code actually doesn't look too bad.
It is easy to read and really explicit in what it does.
One problem is that we are only allowed to do synchronous requests here.
The moment you need to use an async API or parallelize things,
it could get really ugly.
Another problem is that WorkManager can order to stop the work
at any moment.
And while we could just add checks to every other line of code,
which is not a great solution, but it works,
imagine, for a minute that upload server actually does
three long-running operations to complete.
How do you pass in this stop signal to it when it's in the middle of doing its work?
Clearly, this example leads nowhere.
Even though it has natural exceptions
and easy-to-read, top-down sequential code.
Okay, let's try something else.
Another way of dealing with asynchronous requests are futures.
For example, using ListenableFuture from the Guava library.
There is another version of our worker called ListenableWorker
that expects exactly that.
So let's write.
Assume we have some background executor of type listening executor service,
and we submit some work to it as a lambda
and get a ListenableFuture of data back.
Now, for training the next operations, I look up the correct utility function,
transform async, then make it depend on the previous one,
submit a lambda again, get ListenableFuture back.
It is not ideal.
The nice thing is we got cancellation and error propagation.
What is not so nice?
Everything else.
Look how difficult it is to find the actual work
that I need to do in all that boilerplate code.
For completeness, let me mention that there is also
an RxWorker available.
However, that also comes with drawbacks,
such as having to learn the Rx operators,
and it simply can't escape the Java programming languages limitations,
like having to chain calls, making everything less readable.
And finally, we get to coroutine land with the CoroutineWorker.
I assume here that our database and HTTP client
provide suspending versions of their APIs,
meaning it is suspending calls all the way down.
Everything looks sequential and is readable.
Cancellation happens more or less automatically
as it was handled by the library authors.
Errors propagate naturally.
So we could just use a try-catch as in the first example, if needed.
So did I come here to say coroutines are simple?
No.
I think they are ingenious, elegant, but fairly complex underneath.
What we can do as developers is first and foremost, educate ourselves,
educate our teams.
There is great technical documentations for coroutines from JetBrains
on the kotlinlang.org website.
But we also produced a ton of developer materials recently
for using coroutines on Android.
Articles, videos, codelabs, and samples
make it easier to understand what's going on
in addition to the documentation.
But even then, I believe the whole trick to making coroutines simple for the user,
in this case, the developer writing application code,
is to move the ecosystem forward.
We have a huge job to do here
by providing the necessary plumbing behind each API
and the necessary tools to seamlessly work with coroutines.
As in my example,
I assumed I have coroutine versions of the APIs I needed,
which enables me to write three lines of code instead of 20.
Fortunately, all of that is already happening.
We've already started with the most populated JetPack libraries,
with more to come.
Going back to our example of reading from the database,
let's talk about Room for a bit.
Reading from disc, in this case, database queries,
is a great example of one of those things that you should not do on the main thread.
Room is an example of a library
that gives you a suspend version of its calls out of the box.
Other libraries are already adopting a similar approach,
including third-party libraries like Retrofit.
What's really nice about this is that this suspend function is main-safe,
which means that it is safe to call from the main thread
or the main dispatcher, as it will not block the thread.
The library takes care of moving execution to the background
and returning the result.
This might not sound like a huge deal,
but someone could just make a withContext(Dispatchers.IO) call
inside viewModelScope.
But the fact that it is not needed reduces the cognitive load
and changes forever from the API's consumer.
So, make it the library's responsibility and not the caller's.
Room does another thing well,
and that's not only providing same defaults for the background executor,
but also giving you a way to change it.
This will not only come useful if you want to share some executor
that you already have in your app,
but especially when you need to replace the dispatcher for testing.
And by the way, Room is a Java library, so it takes executors,
but the role is almost identical to coroutine dispatchers.
So naturally, there are methods to translate between them.
So if you build your own API,
you can do something similar by not hard-coding
one of the dispatchers into the function call,
but rather make it configurable.
Usually, you want to be building your API from scratch, though.
Maybe you have a Java-based library already,
or you are trying to adopt a third-party library API surface to coroutines.
In those cases, there are two options.
The first, check if an adapter is available in coroutines
for the future type used by the library.
There are interventions for Java 8 CompletableFutures,
Guava ListenableFutures, and tasks used by Google Play Services.
If you don't find an adapter,
it's usually really easy to write your own.
And this is the second option.
Simply use the suspendCancellableCoroutine Builder function
and call the continuation.resume
or resumeWithException methods from a callback.
Of course, simple doesn't mean it is not possible to make mistakes,
as I learned when I tried to test this code.
It turns out I missed one important part.
For my API, I should have added a Fast Path
for when the task is already complete at the time of the call.
It works almost the same as the previous version,
but it might solve certain problems when running this as part of a unit test.
For a full solution, I suggest you take a look
at the code of any of the adapters in the coroutines library.
So, I talked a bit about one-shot suspend functions
that simply return a result.
Let's move on to something even more interesting: flows.
In the previous section, I've drawn a parallel between futures
and suspend functions.
Flow extends this by offering the possibility
to model streams of data, not just single values.
And it builds upon the foundation of coroutines and suspend functions
to inherit some of its best properties, such as cancellation,
structure concurrency, exceptional transparency,
and even natural back-pressure handling through suspension.
Going back to our Room database, with a simple change,
I can make it return a flow instead of having a Suspend function
that returns a single result.
Let's switch to Android Studio 4.1 that comes with a new tool
that we can use to edit a database live in a running app.
It's called the Database Inspector.
Here, I have a simple app running,
compiled from our Room With A View code lab.
The view shows a list of words coming from the database.
Now, if I go into the inspector and modify a value in the database,
the UI updates immediately
without restarting the activity or refreshing manually.
That's cool, and this wouldn't have been possible as easily
with a suspend function.
That's because observing a flow lets the app react to changes
when the database emits new data.
How is that different from other observable strings,
such as RxJava?
Conceptually, it is not that different,
other than having the whole stream processing
built on top of the same coroutines foundation
that I've mentioned before with all its benefits.
Okay then, how is it different from LiveData?
The original CodeLab code that you can find online
uses LiveData to observe state changes from the UI.
Short answer, it makes little difference in this specific case,
but the full answer is a bit more complicated.
So let's dive into this.
Conceptually, LiveData is a value holder.
It always holds a single value that can be written to
and read at any moment.
What follows from this is that when an observer connects,
they will only see the latest value and any new ones,
even though there may have been intermediate value changes
when the data wasn't observed.
What's important is only the current state.
A flow doesn't have a value outside of being collected,
and it will deliver all the values emitted into it
unless it's configured differently through operators
such as Conflate, which is a strategy for dealing
with consumers that are slower than the producer.
In our sample app situation, the UI doesn't care for any previous states.
It is only responsible for showing the latest.
So, it is fine to use LiveData.
But Flow can be more appropriate in the lower layers of the app
where, maybe, for other operations, you cannot afford to lose some values.
The latest coroutine library version
added StateFlow, which does hold the current value,
like LiveData.
The main differences are, of course,
that StateFlow is a flow with all its operators.
LiveData has other drawbacks,
such as only being able to be observed from the main thread.
So what's the future for LiveData?
It is too early to say.
You could certainly continue using it
or try replacing some of its usages with StateFlow,
although that won't currently work directly with the dividing, for example.
We're exploring this topic and working on recommendations.
The best part is that you don't have to pick one over the other,
as it's very easy to convert even from regular flows to LiveData,
and vice versa, which brings me to another topic.
You may know about View Model Scope,
a coroutine scope that is active as soon as the view model is alive.
Similar to it, we have lifecycleScopes for activities and fragments.
So, knowing that LiveData is active
when the lifecycle owner is at least started,
and that we can convert between Flow and LiveData easily,
what is the difference between observing a LiveData in an activity
and collecting a flow when the activity is started?
The first one, using Flow as LiveData,
cancels the flow collection when there are no active observers,
which means when the activity is in the stopped state.
The second one uses opposing dispatcher,
which pauses execution when the activity is not started,
but only counsels the crew team when the activity is destroyed.
This is stated in the documentation,
but it tripped me up when I first used it anyway.
If the flow can handle backpressure, that's not a problem.
But not all flows are created in the same way.
Let's see an example of this.
Similar to how you can convert single-shot callbacks APIs
to suspend functions,
callbackFlow is a builder function that lets us convert callback,
or listener-based APIs, to a flow.
Here, we take Android's location API,
which will keep us sending locations while it's active.
The source of our data is hot, because new listeners
don't trigger the location API in its code.
But our flow is cold, which means that the builder block
will be executed every time this flow is collected,
and a separate instance of the location listener will be installed.
If we combine it with the lifecycleScope dispatcher,
this will not work as you might expect.
When my activity is visible, the collecting coroutine
is active and updates the UI, while the producer coroutine
that listens for location updates supplies the values.
The interesting thing happens when I launch another activity.
The current one is not destroyed, and so, the collection is not canceled.
However, it goes into a stopped state.
So the consumer that is not suspended
no longer processes any values.
As the flow hasn't been canceled,
the producer coroutine keeps churning in the background,
getting locations and putting them into the flow,
with no one reading them until I come back to the first activity.
This way, it's CPU on battery, and it could also
start filling up the memory if configured improperly
with an unwanted buffer.
So, handle with care and learn how things work underneath
if you start building your own flow solutions.
There is one more thing that I wanted to mention,
and that's SharedFlow.
Next to StateFlow, this was another highly requested feature for flows
to be able to take a flow
and share it between multiple subscribers.
In the case of our location flow, for example,
creating a new flow is not that bad
because we are just registering a listener to the location API.
However, SharedFlow is very useful in other situations
when the flow is more expensive to create
or you want to save resources by sharing it.
What's interesting is the whileSubscribed option,
which will free up the upstream producer whenever there are no subscribers.
But that's something that is coming in a future coroutines release,
and we'll explore more of it when it's ready.
These are definitely interesting times in Android development.
Did we find the holy grail of async frameworks?
Perhaps.
With their recent additions, like StateFlow and SharedFlow,
we can see things are still in flux.
But that's a good thing.
The thoughtful, intentional design by the coroutine authors
that responds to concrete use cases is what gave us what we already have:
a great fit on Android.
So, if you are designing on an async solution for your next project,
give coroutines a try and let us know what you think.
Thanks for watching.