Kotlin Flows in practice

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
[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]
Info
Channel: Android Developers
Views: 32,639
Rating: undefined out of 5
Keywords: Android Developer Summit, Android Dev Summit, Android Dev Summit 2021, Android developer, Android development, new in android, new in android development, type: Conference Talk (Full production), pr_pr: Android
Id: fSB6_KE95bU
Channel Id: undefined
Length: 21min 5sec (1265 seconds)
Published: Wed Oct 27 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.