[MUSIC PLAYING]
MANUEL VICENTE VIVO: If you're a library author,
you might want to make your Java-based or callback-based
library easier to consume from Kotlin
using coroutines and Flow.
Or maybe, if you're an API consumer,
you might be willing to adopt a third-party API
surface to coroutines to make it more Kotlin-friendly.
In this video, you will learn how
to simplify APIs using coroutines and Flow,
as well as how to build your own adapters
using suspendCancellableCoroutine
and callbackFlow APIs.
For the most curious ones, those APIs will be dissected,
and you will see how they work under the hood.
OK, let's start.
Before writing your own wrappers for existing APIs,
check if an adapter or extension function
is available for your use case.
There are existing libraries with coroutines adapters
for common types.
Future types mapped to a single one-shot suspend
function in coroutines.
With a suspend function, you can get rid of callbacks,
since the coroutines suspends until the result comes back.
There is an existing integration for Java 8's CompletableFuture.
This helper function will wait for the completion stage
to finish without blocking the thread.
It uses the suspendCancellableCoroutine API
that we'll explain later.
And as a bonus, it is also cancellable,
as you can see in the invokeOnCancellation lambda
code.
If you use Guava instead, there is also an integration
for ListenableFuture.
The implementation is quite similar
and also supports cancellation.
There are also integrations for reactive string libraries,
such as RxJava and Java 9 APIs.
You can convert an observable source to Flow
using the asFlow function, as well as calling
completable, maybe, single, or observable sources
within coroutines using suspend functions.
If you think you need to improve existing Jetpack libraries
or Android platform APIs, take a look
at the Jetpack ktx libraries list.
Currently, more than 20 libraries
have a ktx version creating suite idiomatic versions
of Java APIs, ranging from SharedPreferences
to ViewModels, SQLite, and even Play code.
Find out more about them from our previous episode.
Callbacks are a very common solution
for asynchronous communication.
However, they come with some drawbacks.
Its design leads to nested callbacks, which ends up
in incomprehensible code.
Also, error handling is more complicated,
as there isn't an easy way to propagate errors.
In Kotlin, you can simplify callbacks using coroutines.
If you don't find an adapter for your use case,
it's usually quite straightforward
to write your own.
For one-shot async calls, use the suspendCancellableCoroutine
API.
If you remember, the Future type's
extension functions that we said before used this API.
And for streaming data, use the callbackFlow API.
But enough theory.
Let's take a practical example and code a little bit.
We are going to work together on the code
of the Building a Kotlin Extension Library Colab.
It uses the fusedLocationProvider API
from Google Play Services to get location data.
The API surface is simple, but it uses callbacks
to perform async operations.
With coroutines, we can get rid of callbacks
that can quickly make our code unreadable when
the logic gets complicated.
Let's start with one-shot async calls.
The fused Location Provider has the getLastLocation method
to obtain the last known location.
The ideal API for coroutines is a suspend function
that returns exactly that.
We can activate by creating an extension function
on fusedLocationProviderClient.
As this is our one-shot async operation,
we use the suspendCancellableCoroutine
function, a low-level building block
for creating suspending functions
from the coroutines library.
suspendCancellableCoroutine executes
the block of code passed to it as a parameter.
And then it suspends the coroutine execution
while waiting for the signal to continue.
The coroutine will resume executing
when the resume or resumeWithException
method is called in the coroutine's continuation
object.
We use the callbacks that can be added to the getLastLocation
method to resume the coroutine appropriately.
Note that you can also find a non-cancellable version
of this coroutine builder in the coroutines library
with the suspendCoroutine method.
It is preferable to always to suspend cancellable coroutine
to handle cancellation of the coroutine's code,
or to provide cancellation from the underlying API.
If we take the suspendCancellableCoroutine
implementation, it uses suspendCoroutine
UninterceptedOrReturn method to get
the continuation of the coroutine
inside the suspend function.
That continuation object is intercepted
by a cancellable continuation that
will control the lifecycle of that coroutine from that point.
Its implementation has the functionality
of a job with some restrictions.
After that, the lambda passed to suspendCancellableCoroutine
will be executed, and the coroutine
will either resume immediately if the block of code
returns a result, or will be suspended
until the cancellable continuation is resumed
manually from the lambda.
Instead of the last known location,
we might want to receive periodic location updates
using the requestLocationUpdates function
whenever the user's device moves in the real world.
We would need to create a stream of data using Flow.
The ideal API would look like this.
It will be another extension function
and will return a flow of locations.
To convert streaming callback-based APIs to Flow,
use the callbackFlow flow builder
that creates a new flow.
In the callbackFlow lambda, we're
in the context of a coroutine.
Therefore, suspend functions can be called.
Unlike the Flow flow builder, channelFlow
allows values to be emitted from a different coroutine context
or outside a coroutine with the offer method.
Normally, flow adapters using callbackFlow
follow three steps.
First, we create the callback that adds elements
into the flow using offer.
In here, we create a location callback.
If the result is no, we ignore it.
If not, we call offer to send a location to the flow.
As offer comes through, we should grab it in a try/catch.
Second, we register the callback in fusedLocationProviderClient,
calling the requestLocationUpdates method,
There, we pass in a generic location
request, the callback we just created, and the main looper.
For the error cases, we can add an OnFailureListener
to get notified of any errors.
If that's the case, we close the flow.
And third, as we want this to be an endless flow,
we need to wait for the consumer to cancel the coroutine
and unregister the callback.
For that, we use the awaitClose method.
And in its lambda, we can have cleanup code.
In this case, we remove the callback
calling removeLocationUpdates, because the consumer is
no longer listening for values.
If we look at the callbackFlow implementation,
it uses a channel, which is conceptually very
similar to a BlockingQueue.
A channel is configured with a capacity,
the number of elements that can be buffered.
The channel created in callbackFlow
has the default capacity of 64 elements.
When adding a new element to an already full channel,
send will suspend the producer until build suspends
for the new element in the channel,
whereas offer won't add the element to the channel
and will return false immediately.
If we now look at the implementation of awaitClose,
you see it also uses suspendCancellableCoroutine
under the hood.
It will suspend forever and resume the coroutine
successfully when the flow is closed.
In the final block of code, it executes the caller's cleanup
code.
Flows are cold and lazy unless specified otherwise
with intermediate operators, such as config.
This means that the building block
will be executed each time a terminal
operator is called on the flow.
This might not be a huge problem in our case,
as adding new location listeners is simple.
However, it might make a difference
in other implementations.
To reduce the same flow across multiple collectors
and make the cold flow hot, use the shareIn intermediate
operator.
Here, we configure it with an application scope
that we receive as a dependency.
We specify a reply of 1 to emit the last emitted element
to new collectors.
And we use the WhileSubscribed started policy
to keep the producer active while there
are active subscribers.
Consider creating coroutine adapters
to make your APIs resistant APIs, concise, readable,
and Kotlin-idiomatic.
First, check if the adapter is already available, and if not,
create your own one using suspendCancellableCoroutine
for one-shot calls, or callbackFlow
for streaming data.
In the following episodes in the series,
we'll show how to use the Kotlin-specific
APIs for popular libraries such as Room and WorkManager.
So make sure this to subscribe to the Android Developers
channel.
[MUSIC PLAYING]