[MUSIC PLAYING] MANUEL VIVO: Hi, everyone. Today, Jose and I want to bring
their reactive programming concept close to
Android redevelopment. Then we'll see how the work will
flow, Kotlin type for modeling streams of data. Android has a
particular UI lifecycle. We'll see how to optimize
flows for things like rotations and sending the app
to the background. Lastly, as with
all good stories, we need to test that
everything works as expected. JOSE ALCÉRRECA: Every Android
app needs to send data around one way or another. And there is a million
different use cases, loading a username
from a database, fetching a document from a
server, authenticating a user. In this talk, we'll look at how
you can load data into a flow, transform it, and expose it to
a view so that it's displayed. To help me explain why we
use flow, here's Pancho. Pancho lives on a mountain. And when Pancho wants
fresh water from a lake, they do what any
beginner would do. They grab a bucket and walk
up to the lake and down again. But sometimes, Pancho
finds that the lake is dry. So they wasted time
walking up to the lake, as they have to find
water elsewhere. After doing this
multiple times, they realize it would be
much better to create some kind of infrastructure. So the next time they
walk up to the lake, they install some pipes. Now if they need more water
and the lake is not dry, they just open the tap. Once you know how
to install pipes, it's easy to get fancy and
combine multiple sources of data-- sorry, water-- so that Pancho
doesn't have to check if the lake is dry anymore. In an Android app, you can take
the easy path and request data every time you need it. For example, when
the view starts, you request data
to a view model, which in turn requests
data to the data layer. And then everything happens
in the other direction. You can do this easily
with suspend functions. However, after doing that for
a while, developers like Poncho tend to realize investing in
some infrastructure really pays out. Instead of requesting
data, we observe it. Observing data is like
installing tubes for water. Once they're in place, any
update to the source of data will flow down to the
view automatically. You don't have to walk
to the lake anymore. We call a system that uses
these patterns reactive because observers react
automatically to changes in the things being observed. Another important
design choice is to keep data flowing in
just one direction, as this is less prone to errors
and easier to manage. In this example app, the auth
manager tells the database that a user logged in. And this one, in turn, has to
tell the remote data source to load a different
set of items, all of this while
telling the view to show a loading spinner
while they grab new data. I mean, this is doable,
but prone to bugs. A better way to do this is
to let data flow in just one direction and create
some infrastructure, some tubes to
combine and transform these streams of data. If something changes that
requires modifications, such as when the user logs out,
the tubes can be reinstalled. As you can imagine, we
need sophisticated tools to do all these combinations
and transformations. And in this talk, we're going
to use Kotlin flow for that. It's not the only streams
builder out there, but it's part of coroutines
and very well supported. MANUEL VIVO: The stream of water
analogy we've been using so far can be modeled in
a concrete type called flow, a type that is
part of the coroutines library. Instead of water, flows
can be of any type thing, for example, user
data or UI state. There is some terminology we'll
be using during the talk that is important to define. A producer emits
data into the flow that a consumer
collects from the flow. And under it, a data
source, or repository, is typically a producer
of application data that has the UI as the
consumer that ultimately displays the data on screen. Let's start with how
flows are created. For that, let's take
a walk to the lake. Most of the time, you don't
need to create a flow yourself. The libraries you depend
on in your data sources are already integrated
with coroutines and flows. This is the case of popular
libraries such as DataStore, Retrofit, Room, or WorkManager. They act like a water dam. They provide you
data using flows. You just plug into a
pipe without knowing how the data is being produced. Taking Room as an
example, you can get notified of
changes in the database by exposing a flow of type x. The Room library
acts as a producer and emits the content
of the query every time an update happens. If you really need to
create a flow yourself, there are different
alternatives you can choose. One of the options
is the flow builder. Imagine that we are in a
user messages data source and you want to check
for messages every so often from your app. We can expose the user
messages as a flow of type list of messages. To create a flow, we
use the flow builder. The flow builder takes
us to suspend block as a parameter, which means it
can call suspended functions. And this is because
the flow is executed in the context of a coroutine. Inside it, we can have our while
true loop to repeat our logic periodically. First, we fetch the
messages from the API. And then we add the
result into the flow using the emit suspend function. This step suspends the
coroutine until the collector receives the item. Lastly, we suspend the
coroutine for some time. In our flow, operations
are executed sequentially in the same coroutine. Due to the while
true loop, this flow keeps infinitely fetching
the latest messages until the observer goes away
and stops collecting items. Also, the suspend block
passed to the flow builder is often called producer block. In Android, layers in between
the producer and consumer can modify the stream
of data to adjust it to the requirements of
the following layer. To transform flows, you can
use intermediate operators. If we consider the
latest messages stream as the flow starting point,
we can use the map operator to transform the data
to a different type. For example, inside
the map lambda, we are transforming
the Room messages coming from the data
source to a messages UI model that is a
better abstraction for this layer of the app. Each operator creates a
new flow that emits data according to its functionality. We can also filter
the stream to get the flow for those
messages that contain important notifications. Now, how can we
handle errors that happen as part of the stream? The catch operator
catches exceptions that could happen
while processing items in the upstream flow. The upstream flow
refers to the flow produced by the producer block
and those operators called before the current one. Similarly, we can
refer to everything that happens after
the current operator as the downstream flow. Catch can also
rethrow the exception if needed or emit new values. For example, this code rethrows
IllegalArgumentExceptions, but emits an empty list if
any other exception occurs. At this point, we've seen
how streams are produced and how they can be modified. It's time to learn about
how to collect them. Collecting flows usually
happens from the UI layer, as it is where we want to
display the data on the screen. In our example, we want to
display the latest messages on a list so that Poncho can
keep up with what's going on. We need to use a
terminal operator to start listening for values. To get all the values in the
stream as they are limited, use collect. Collect takes a function
as a parameter that is called on every new value. And as it is a result
of suspend function, it needs to be executed
within a coroutine. When you apply a terminal
operator to a flow, the flow is created on demand
and starts emitting values. On the contrary,
intermediate operators just set up a
chain of operations that are executed lazily when an
item is emitted into the flow. Every time collect is
called on user messages, a new flow, or pipe,
will be created. And its producer
block will start refreshing the messages from
the API at its own interval. In coroutines jargon, we
refer to this type of flows as called flows as they are
created on demand and emit data only when they are
being observed. Let's see now how to
optimally collect flows from the Android UI. There are two main
things to consider. The first one is about
not wasting resources when the app is
in the background, and the second one is about
configuration changes. Let's imagine we are
in messages activity and we want to display the
list of messages on the screen. For how long should we be
collecting from the flow? The UI should be a
good citizen and stop collecting from the
flow when the UI is not displayed on the screen. Back to the water
analogy, Pancho should close the tap
while brushing their teeth or going for a nap. Poncho shouldn't
be wasting water. Similarly, the UI shouldn't
be collecting from flows if the information isn't going
to be displayed on the screen. To do this, there are
different alternatives. And all of them are aware
of the UI life cycle. You can use life
data or lifecycle coroutine-specific APIs such
as repeat on lifecycle and flow with lifecycle. The asLiveData flow
operator compares the flow to live data that observes
items only while the UI is visible on the screen. This conversion is something we
can do in the view model class. In the UI, we just consume
the LiveData as usual. But OK, this is cheating
a bit because it's adding a different technology
into the mix, which shouldn't be needed. RepeatOnLifecycle is
the recommended way to collect flows
from the UI layer. RepeatOnLifecycle is
a suspend function that takes a life cycle
step as a parameter. This API is lifecycle
aware, as it automatically launches a new coroutine
with a block pass to it when the lifecycle
reaches that step. Then, when the lifecycle
falls below that state, the ongoing coroutine
is canceled. Inside the block, we can
call collect, as we are in the context of a coroutine. As repeatOnLifecycle
is a suspend function, it also needs to be
called in a coroutine. As you are in an
activity, we can use lifecycleScope to start one. As you can see,
the best practice is to call this function when
the lifecycle is initialized, for example, in onCreate
in this activity. RepeatOnLifecycle's
restartable behavior takes into account the UI
lifecycle automatically for you. Something important to note
is that the coroutine that calls repeatOnLifecycle
won't resume executing until the
lifecycle is destroyed. So if you need to collect
from multiple flows, you should create
multiple coroutines using launch inside the
repeatOnLifecycle block. You can also use the
flowWithLifecycle operator instead of repeatOnLifecycle
when you have only one flow to collect. This API emits items and
cancels the underlying producer when the lifecycle moves in
and out of the target state. To show how this works
visually, let's take a tour through the activity lifecycle
when it's first created, then sent to the background
because the user pressed the Home button, which makes
the activity receive the onStop signal, and then opening the app
again when onStart is called. When you call repeatOnLifecycle
with the started state, the UI processes flow
emissions while it's visible on the screen. And the collection
is canceled when the app goes to the background. RepeatOnLifecycle
and flowWithLifecycle are new APIs added in the stable
2.4 version of the Lifecycle runtime ktx library. Because they are new, you
might be collecting flows from the Android UI
in a different way. For example, you might
be collecting directly from a coroutine launched
by lifecycleScope. While this might seem OK to use,
collecting flows in this way is not always safe. This collects from the flow
and updates UI elements, even if the app is
in the background. In fact, this is
not the only case. Other solutions like the
lifecycle coroutine scope launch when X API family
suffer from similar problems. You don't like Poncho
wasting water, do you? Then we shouldn't be
collecting from the flow if those items aren't going
to be displayed on the screen. If you collect directly
from lifecycleScope.launch, the activity keeps
receiving flow updates while in the background. That can be both
wasteful and dangerous, as, for example,
showing dialogues when the app is
in the background can make your application crash. To solve this issue,
you could manually start collecting in onStart
and stop collecting in onStop. While that's OK, using
repeatOnLifecycle removes all that
boilerplate code. If we look at launchWhenStarted
as an alternative, it is better than
lifecycleScope.launch because it suspends the flow
collection while the app is in the background. However, this solution keeps
the flow producer active, potentially emitting items
in the background that can fill the memory with
items that aren't going to be displayed on the screen. As the UI doesn't really
know how the flow producer is implemented, it is always
better to play safe and use repeatOnLifecycle or
flowWithLifecycle to avoid collecting items and keeping
the flow producer active when the UI is in the background. If this optimizes flow
collection when the app goes to the background, Jose
is going to tell you some tricks for when the app
goes through configuration changes. JOSE ALCÉRRECA: When you
expose a flow to a view, you have to take into account
that you are trying to pass data between two elements that
have different lifecycles. And not any lifecycle. The lifecycle of activities and
fragments, which can be tricky. As a crucial example,
remember that when a device is rotated or receives
a configuration change, all activities
might be restarted, but a view model survives that. So from a view
model, you can't just expose any flow, for example,
a call flow like this one. A call flow results
every time it's collected for the first time. So the repository would be
called again after a rotation. What we need is some
kind of buffer, something that can hold data and share
it between multiple collectors no matter how many times
they are recreated. StateFlow was created
for exactly that. A StateFlow is a water
tank in our lake analogy. It holds data even if
there are no collectors. You can collect
multiple times from it, so it's safe to use with
activities or fragments. You could use the mutable
version of StateFlow and update its value whenever
you want, for example, from a coroutine like in here. But that's not very
reactive, is it? Pancho would suggest
you improve your game. Instead, you can convert
any flow to a StateFlow. If you do that, the StateFlow
receives all the updates from the upstream flows and
stores the latest value. And it can have zero
or more collectors, so this is perfect
for view models. There are more types
of flows, but this is what we recommend
because we can optimize StateFlow very precisely. To convert a flow
to a StateFlow, you can use the
stateIn operator on it. It takes three parameters. Initial value, because
a StateFlow always needs to have a value, a
coroutine scope, which controls when the serving is started. We can use the ViewModel
scope for this. And started, which is
the interesting one. We're going to get to what that
WhileSubscribed(5000) means. But first, let's look
at two scenarios. The first scenario
is a rotation where the activity, which is
the collector of the flow, is destroyed for a short period
of time and then recreated. The second scenario is
a navigation to home where our app is put
in the background. In the rotation
scenario, we don't want to restart any flows
to make the transition as fast as possible. In the navigation
to home however, we want to stop all flows to save
battery and other resources. So how do we detect
which one is which? We do that with a timeout. When a StateFlow
stops being collected, we don't immediately stop
all the upstream flows. Instead, we wait for some time,
for example, five seconds. If the flow is collected
again before the timeout, no upstream flows are canceled. That is exactly what the
WhileSubscribed(5000) does. In this diagram, we
show what happens when the app goes to the background. Before the Home
button is pressed, the view is receiving
updates and the StateFlow has its upstream flows
producing normally. Now when the view stops, the
collection ends immediately. However, the StateFlow, because
of how we configured it, takes five seconds to
stop its upstream flows. Now the timeout passes and
upstream flows are canceled. Only when the user opens the
app again, if that ever happens, the upstream flows are
automatically restarted. In the rotation scenario
however, the view is only stopped for a very short
time, less than five seconds anyway. So the StateFlow never gets
restored and keeps all upstream flows active, acting as
though nothing happened and making the rotation
instant to the user. So in summary, we
recommend that you use StateFlow to expose
your flows from a view model or keep using as
like data, which does exactly the same thing. If you want to learn
even more about StateFlow or its parent, SharedFlow, check
out the resources at the end. Now, you might be wondering,
how do I test this? Well, testing a
flow can get tricky because you're dealing
with streams of data. So there are a couple
of tricks you can use. First, there are two scenarios. In the first one, the
unit and the test, whatever you're testing,
is receiving a flow. The easy way to test this
is to replace the dependency with a fake producer. You would program this fake
repository, in this example, to emit whatever you need
for the different test cases, for example, with
a simple call flow. The test itself
would make assertions on the output of the
subject and the test, which is a flow
or something else. Secondly, if the unit under
test is exposing a flow and that value or
stream of values is what you want to verify,
you have multiple ways to collect it. You can call the first
method on the flow, and this is going
to collect until it receives the first item
and then stop collecting. But also, you can use
operators, such as take(5), and call the two list
terminal operator to collect exactly five
messages, which can be useful. Hopefully, in this
talk you've learned why a reactive architecture
is a good investment and how to build your
infrastructure with Kotlin flow. If you feel inspired, we have
a ton of content around this, a guide that covers the
basics, and blog posts where we do deep
dives on some topics. Also, if you want to see
all of this in context, check out the Google
I/O app, which we updated earlier this year
to include flows everywhere. Thank you. [MUSIC PLAYING]