Kotlin Flows in practice

Android Developers
27 Oct 202121:05

Summary

TLDRThis script introduces the concept of reactive programming in Android development using Kotlin Flow. It explains how to model data streams, optimize for lifecycle events, and handle configuration changes. The talk uses the analogy of water infrastructure to illustrate the benefits of setting up data 'pipes' instead of manual data requests. It also covers creating, transforming, and collecting flows, as well as best practices for lifecycle-aware collection and testing strategies.

Takeaways

  • πŸš€ **Reactive Programming in Android**: The talk introduces reactive programming concepts to Android development, focusing on stream data modeling using Kotlin Flow.
  • πŸ”„ **Optimizing Flows for UI Lifecycle**: Discusses how to optimize data flows for Android's unique UI lifecycle, including handling rotations and backgrounding the app.
  • πŸ’§ **Flow as a Stream of Data**: Uses the analogy of water flow to explain the concept of data streams in Android, emphasizing the efficiency of 'observing' data rather than 'requesting' it.
  • πŸ”§ **Building Infrastructure for Data**: Advocates for creating infrastructure (like pipes for water) in apps to manage data flow more effectively, reducing redundancy and potential errors.
  • πŸ”€ **Unidirectional Data Flow**: Highlights the importance of keeping data flow in one direction to simplify management and reduce bugs in the application.
  • πŸ› οΈ **Kotlin Flow for Data Management**: Recommends using Kotlin Flow, part of the Coroutines library, for sophisticated data combination and transformation within an app.
  • πŸ“‘ **Creating and Transforming Flows**: Covers the creation of flows using flow builders and the transformation of data through intermediate operators like map and filter.
  • πŸ›‘ **Error Handling in Flows**: Introduces the catch operator for handling exceptions within a flow, ensuring robust data stream management.
  • πŸ”— **Collecting Flows in UI Layer**: Explains the process of collecting data from flows in the UI layer using terminal operators and the importance of doing so efficiently.
  • πŸ”„ **Lifecycle Awareness in Flow Collection**: Discusses the use of lifecycle-aware APIs like repeatOnLifecycle and flowWithLifecycle to manage flow collection based on the UI's visibility.
  • πŸ” **Testing Flows**: Provides insights into testing flows, emphasizing the need for strategies to handle the asynchronous nature of stream data.

Q & A

  • What is the main concept discussed in the script related to Android redevelopment?

    -The main concept discussed is reactive programming, specifically using Kotlin Flow for modeling streams of data in Android redevelopment.

  • How does the script illustrate the idea of reactive programming in the context of fetching data?

    -The script uses the analogy of Pancho fetching water from a lake, explaining that instead of repeatedly requesting data (walking to the lake), it's more efficient to set up an infrastructure (installing pipes) to observe data changes automatically.

  • What is the significance of keeping data flow in one direction in the script's explanation?

    -Keeping data flow in one direction is highlighted as a best practice because it reduces the likelihood of errors and simplifies the management of data streams within an application.

  • What is the role of a producer in the context of Kotlin Flow as described in the script?

    -A producer in Kotlin Flow is responsible for emitting data into the flow, which can then be collected by a consumer, typically a UI layer in an Android application.

  • How can intermediate operators be used to transform or filter data streams in Kotlin Flow?

    -Intermediate operators like 'map' can transform data to a different type, while 'filter' can be used to select only certain items that meet specific criteria, such as containing important notifications.

  • What is the purpose of the 'catch' operator in handling errors within a flow in Kotlin?

    -The 'catch' operator is used to handle exceptions that may occur while processing items in the upstream flow, allowing the application to either rethrow exceptions or emit alternative values.

  • Why is it recommended to use 'repeatOnLifecycle' or 'flowWithLifecycle' when collecting flows from the Android UI layer?

    -These APIs are lifecycle-aware and help optimize the collection of flows by automatically managing the lifecycle of coroutines based on the UI's visibility, preventing unnecessary resource usage when the app is in the background.

  • What is the difference between 'repeatOnLifecycle' and 'flowWithLifecycle' in collecting flows?

    -'repeatOnLifecycle' is a suspend function that automatically launches and cancels coroutines based on lifecycle states, suitable for collecting from multiple flows. 'flowWithLifecycle', on the other hand, is used when there is only one flow to collect, as it emits items and manages the lifecycle of the producer directly.

  • How does the script suggest handling configuration changes like device rotation in the context of data flows?

    -The script recommends using StateFlow, which acts as a buffer holding data and can share it between multiple collectors, ensuring a smooth transition during configuration changes without restarting upstream flows.

  • What is the role of 'StateFlow' in the context of view models and activities with different lifecycles?

    -StateFlow is used to safely expose flows from a view model to activities or fragments, ensuring that the data remains consistent and available even when the UI elements are recreated due to lifecycle changes.

  • How can testing of flows be approached as discussed in the script?

    -Testing flows can involve replacing dependencies with fake producers to emit specific test data, or by using terminal operators like 'first' or 'take' to collect and verify the output of the flow under test.

Outlines

00:00

🌐 Introduction to Reactive Programming with Kotlin Flows

The video script introduces the concept of reactive programming in the context of Android development, focusing on Kotlin Flows for data stream management. It discusses the importance of optimizing data flows in response to UI lifecycle events such as screen rotations and app backgrounding. The talk aims to demonstrate loading, transforming, and displaying data using flows, and emphasizes the benefits of setting up infrastructure for data management, illustrated with the analogy of Pancho fetching water from a lake and evolving to a more efficient system with pipes. The reactive system is highlighted as one where observers automatically respond to data source changes, promoting a unidirectional data flow to reduce errors and simplify management.

05:01

πŸ›  Creating and Transforming Flows in Android

This section delves into the mechanics of creating and customizing flows in Android. It explains the use of the flow builder for periodic tasks, such as fetching user messages, and how to emit data into a flow using suspend functions within a coroutine context. The summary covers intermediate operators like map for data transformation, filter for stream refinement, and catch for error handling within flows. The importance of collecting flows, particularly in the UI layer using terminal operators, is underscored. The discussion also touches on the efficiency of not collecting from flows when the UI is not active, hinting at lifecycle-aware collection strategies.

10:03

πŸ”„ Optimal Collection of Flows in Android UI

The paragraph discusses best practices for collecting flows within the Android UI, taking into account the UI lifecycle and resource optimization. It introduces lifecycle-aware APIs like 'asLiveData', 'repeatOnLifecycle', and 'flowWithLifecycle' for efficient flow collection that ceases when the UI is not displayed. The summary explains how 'repeatOnLifecycle' automatically manages coroutines based on lifecycle states, and the risks of collecting flows inappropriately, such as during app backgrounding or configuration changes. The paragraph also contrasts newer APIs with older practices, advocating for safe and efficient flow management to prevent resource wastage and potential app crashes.

15:06

πŸ”§ Handling Configuration Changes with StateFlow

This part of the script addresses the complexities of handling data flow across different lifecycles, especially during configuration changes like device rotations. It introduces StateFlow as a robust solution, capable of holding data and sharing it among multiple collectors, even if they are recreated after lifecycle changes. The summary explains how StateFlow acts as a buffer, ensuring a seamlessη”¨ζˆ·δ½“ιͺŒ during transitions. It also discusses the use of 'stateIn' for converting flows to StateFlow and the significance of the 'WhileSubscribed' timeout in determining whether to stop upstream flows during backgrounding or short stops like rotations. The paragraph emphasizes the importance of testing flows and provides strategies for doing so effectively.

20:09

πŸ“š Conclusion and Further Learning on Reactive Architecture

The final paragraph wraps up the discussion on reactive programming with Kotlin Flows, highlighting the benefits of investing in such an architecture for Android development. It encourages further exploration of the topic through guides, blog posts, and real-world examples like the Google I/O app. The summary stresses the importance of testing flows and provides methods for unit testing both when a flow is received and when it is exposed. The paragraph concludes with an invitation for viewers to learn more about StateFlow and SharedFlow, and to apply the concepts discussed in their own Android development projects.

Mindmap

Keywords

πŸ’‘Reactive Programming

Reactive Programming is a programming paradigm concerned with data streams and the propagation of change. It is the underlying theme of the video, which discusses how to apply this concept to Android development. The video uses the analogy of water flow to explain the concept of data streams, emphasizing the importance of setting up infrastructure to handle data changes automatically, much like setting up pipes to get water from a lake to a home.

πŸ’‘Kotlin Flow

Kotlin Flow is a type in the Coroutines library used for modeling streams of data in a reactive programming context. The video focuses on how Kotlin Flow can be used to manage data in Android applications, allowing developers to observe data changes instead of actively requesting data. It is a key tool for building the 'infrastructure' that the video discusses, enabling automatic updates to the UI when the underlying data changes.

πŸ’‘UI Lifecycle

UI Lifecycle refers to the stages a user interface goes through, from creation to destruction, including events like configuration changes and being sent to the background. The video discusses optimizing data flows for these lifecycle events in Android, ensuring that data streams are managed efficiently and do not waste resources when the app is not in the foreground.

πŸ’‘Data Transformation

Data Transformation is the process of converting data from one format to another, often to better suit the needs of the application layer that will consume it. In the video, the concept is illustrated by using the 'map' operator on a flow to transform data types, such as converting 'Room messages' to a 'messages UI model', which is more appropriate for the UI layer.

πŸ’‘Error Handling

Error Handling in the context of the video refers to managing exceptions that may occur during the processing of data streams. The 'catch' operator is highlighted as a means to handle errors within a flow, allowing developers to either rethrow exceptions or provide fallback values, ensuring the stability of the data stream even when errors occur.

πŸ’‘Flow Collection

Flow Collection is the process of listening to and responding to the data emitted by a flow. The video explains how to collect flows in the UI layer using terminal operators like 'collect', which is a suspend function that needs to be executed within a coroutine. It also discusses best practices for collecting flows in response to the UI lifecycle to prevent unnecessary resource usage.

πŸ’‘Lifecycle-aware Collection

Lifecycle-aware Collection is the practice of collecting data streams in a way that respects the lifecycle of the UI components. The video introduces APIs like 'repeatOnLifecycle' and 'flowWithLifecycle' that allow for collecting flows only when the UI is in an active state, preventing resource wastage when the app is in the background or not visible.

πŸ’‘StateFlow

StateFlow is a type of flow in Kotlin's Coroutine library that holds and shares the latest value with multiple collectors, even if they are recreated due to configuration changes. The video describes StateFlow as a 'water tank' in the analogy, capable of buffering data and ensuring that the UI remains consistent and responsive during activities like screen rotations.

πŸ’‘Configuration Changes

Configuration Changes refer to alterations in the system environment that can affect the UI, such as screen rotations or language changes. The video explains how StateFlow can be used to handle these changes gracefully, ensuring that data flows are not unnecessarily restarted and that the user experience remains smooth.

πŸ’‘Testing Flows

Testing Flows is the process of verifying the behavior of data streams in an application. The video outlines strategies for testing flows, such as using fake producers to emit predefined data for unit tests and utilizing flow operators to collect and assert specific numbers of items from a stream.

Highlights

Introduction to reactive programming concepts applied to Android redevelopment.

Exploration of Kotlin Flow for modeling streams of data in Android.

Optimizing flows to handle Android's UI lifecycle events like rotations and backgrounding.

The importance of testing in ensuring the functionality of Android apps.

Use cases for data handling in Android apps, including database operations and server communication.

Loading data into a flow, transforming it, and exposing it to views for display.

The story of Pancho and the analogy of installing pipes for efficient data retrieval.

Comparing the traditional data request method with the reactive approach of observing data.

Benefits of a unidirectional data flow to reduce errors and simplify management.

The use of Kotlin Flow within the Coroutines library for data stream manipulation.

Terminology explanation: producers, consumers, and the flow of data in Android.

How to create flows using flow builders and the role of suspend functions.

Transformation of data streams using intermediate operators like map and filter.

Error handling in data streams with the catch operator.

Collecting flows in the UI layer and the use of terminal operators.

Optimizing flow collection during app backgrounding and UI lifecycle awareness.

Using StateFlow to handle data sharing between activities and fragments during lifecycle changes.

Conversion of flows to StateFlow for buffering and sharing data across multiple collectors.

Testing strategies for flows, including fake producers and collecting specific numbers of items.

Conclusion on the benefits of a reactive architecture using Kotlin Flow and further learning resources.

Transcripts

play00:08

Hi, everyone.

play00:09

Today, Jose and I want to bring their reactive programming concept

play00:14

close to Android redevelopment.

play00:16

Then, we'll see how to work with flow Kotlin type for modeling streams of data.

play00:22

Android has a particular UI lifecycle.

play00:24

We'll see how to optimize flows for things like rotations

play00:29

and sending the app to the background.

play00:31

Lastly, as with all good stories,

play00:33

we need to test that everything works as expected.

play00:37

Every Android app needs to send data around one way or another.

play00:41

And there is a million different use cases:

play00:43

loading a username from a database,

play00:45

fetching a document from a server, authenticating a user.

play00:49

In this talk, we'll look at how you can load data into a flow,

play00:52

transform it, and expose it to a view so that it's displayed.

play00:56

To help me explain why we use flow, here's Pancho.

play01:00

Pancho lives on a mountain.

play01:02

And when Pancho wants fresh water from a lake,

play01:04

they do what any beginner would do.

play01:06

They grab a bucket and walk up to the lake and down again.

play01:10

But sometimes, Pancho finds that the lake is dry.

play01:12

So they wasted time walking up to the lake,

play01:15

as they have to find water elsewhere.

play01:17

After doing this multiple times, they

play01:19

realize it would be much better to create some kind of infrastructure.

play01:24

So the next time they walk up to the lake, they install some pipes.

play01:27

Now, if they need more water and the lake is not dry,

play01:31

they just open the tap.

play01:32

Once you know how to install pipes,

play01:34

it's easy to get fancy and combine multiple sources of data--

play01:37

sorry, water--

play01:39

so that Pancho doesn't have to check if the lake is dry anymore.

play01:43

In an Android app, you can take the easy path

play01:45

and request data every time you need it.

play01:47

For example, when the view starts,

play01:50

you request data to a view model,

play01:51

which in turn requests data to the data layer.

play01:54

And then, everything happens in the other direction.

play01:57

You can do this easily with suspend functions.

play02:01

However, after doing that for a while,

play02:03

developers like Poncho tend to realize

play02:05

investing in some infrastructure really pays out.

play02:09

Instead of requesting data, we observe it.

play02:13

Observing data is like installing tubes for water.

play02:15

Once they're in place,

play02:17

any update to the source of data will flow down to the view automatically.

play02:20

You don't have to walk to the lake anymore.

play02:23

We call a system that uses these patterns reactive

play02:27

because observers react automatically to changes in the things being observed.

play02:32

Another important design choice is

play02:34

to keep data flowing in just one direction,

play02:36

as this is less prone to errors and easier to manage.

play02:41

In this example app, the auth manager tells the database

play02:44

that a user logged in.

play02:46

And this one, in turn, has to tell the remote data source

play02:49

to load a different set of items,

play02:51

all of this while telling the view

play02:53

to show a loading spinner while they grab new data.

play02:56

I mean, this is doable, but prone to bugs.

play02:59

A better way to do this is to let data flow in just one direction

play03:03

and create some infrastructure, some tubes

play03:06

to combine and transform these streams of data.

play03:10

If something changes that requires modifications,

play03:13

such as when the user logs out, the tubes can be reinstalled.

play03:18

As you can imagine, we need sophisticated tools

play03:20

to do all these combinations and transformations.

play03:23

And in this talk, we're going to use Kotlin Flow for that.

play03:25

It's not the only streams builder out there,

play03:28

but it's part of Coroutines and very well supported.

play03:31

The stream of water analogy we've been using so far

play03:34

can be modeled in a concrete type called flow,

play03:38

a type that is part of the Coroutines library.

play03:41

Instead of water, flows can be of any type thing,

play03:45

for example, user data or UI state.

play03:49

There is some terminology we'll be using during the talk

play03:52

that is important to define.

play03:55

A producer emits data into the flow

play03:58

that a consumer collects from the flow.

play04:01

In Android, a data source, or repository,

play04:04

is typically a producer of application data

play04:07

that has the UI as the consumer

play04:10

that ultimately displays the data on screen.

play04:13

Let's start with how flows are created.

play04:16

For that, let's take a walk to the lake.

play04:20

Most of the time, you don't need to create a flow yourself.

play04:23

The libraries you depend on in your data sources

play04:26

are already integrated with coroutines and flows.

play04:30

This is the case of popular libraries such as DataStore,

play04:33

Retrofit, Room, or WorkManager.

play04:37

They act like a water dam.

play04:39

They provide you data using flows.

play04:42

You just plug into a pipe without knowing

play04:45

how the data is being produced.

play04:48

Taking Room as an example,

play04:50

you can get notified of changes in the database

play04:53

by exposing a flow of type x.

play04:56

The Room library acts as a producer

play04:59

and emits the content of the query

play05:01

every time an update happens.

play05:04

If you really need to create a flow yourself,

play05:06

there are different alternatives you can choose.

play05:09

One of the options is the flow builder.

play05:13

Imagine that we are in a user messages data source

play05:16

and you want to check for messages every so often from your app.

play05:20

We can expose the user messages as a flow of type list of messages.

play05:26

To create a flow, we use the flow builder.

play05:29

The flow builder takes us to suspend block as a parameter,

play05:33

which means it can call suspended functions.

play05:36

And this is because the flow is executed in the context of a coroutine.

play05:41

Inside it, we can have our WhileTrue loop to repeat our logic periodically.

play05:46

First, we fetch the messages from the API.

play05:49

And then, we add the result into the flow

play05:52

using the emit suspend function.

play05:55

This step suspends the coroutine until the collector receives the item.

play06:00

Lastly, we suspend the coroutine for some time.

play06:05

In our flow, operations are executed sequentially

play06:08

in the same coroutine.

play06:10

Due to the WhileTrue loop,

play06:12

this flow keeps infinitely fetching the latest messages

play06:16

until the observer goes away and stops collecting items.

play06:20

Also, the suspend block passed to the flow builder

play06:23

is often called producer block.

play06:27

In Android, layers in between the producer and consumer

play06:31

can modify the stream of data

play06:33

to adjust it to the requirements of the following layer.

play06:37

To transform flows, you can use intermediate operators.

play06:41

If we consider the latest messages stream as the flow starting point,

play06:46

we can use the map operator to transform the data to a different type.

play06:51

For example, inside the map lambda,

play06:54

we are transforming the Room messages coming from the data source

play06:57

to a messages UI model

play06:59

that is a better abstraction for this layer of the app.

play07:03

Each operator creates a new flow

play07:06

that emits data according to its functionality.

play07:10

We can also filter the stream to get the flow for those messages

play07:14

that contain important notifications.

play07:17

Now, how can we handle errors that happen as part of the stream?

play07:22

The catch operator catches exceptions

play07:25

that could happen while processing items in the upstream flow.

play07:29

The upstream flow refers to the flow produced by the producer block

play07:34

and those operators called before the current one.

play07:37

Similarly, we can refer to everything that happens after the current operator

play07:42

as the downstream flow.

play07:45

Catch can also rethrow the exception if needed

play07:48

or emit new values.

play07:50

For example, this code rethrows IllegalArgumentExceptions,

play07:54

but emits an empty list if any other exception occurs.

play07:58

At this point, we've seen how streams are produced

play08:01

and how they can be modified.

play08:03

It's time to learn about how to collect them.

play08:06

Collecting flows usually happens from the UI layer,

play08:09

as it is where we want to display the data on the screen.

play08:13

In our example, we want to display the latest messages on a list

play08:17

so that Poncho can keep up with what's going on.

play08:21

We need to use a terminal operator to start listening for values.

play08:26

To get all the values in the stream as they are limited, use collect.

play08:30

Collect takes a function as a parameter that is called on every new value.

play08:35

And as it is a suspend function,

play08:37

it needs to be executed within a coroutine.

play08:40

When you apply a terminal operator to a flow,

play08:43

the flow is created on demand and starts emitting values.

play08:48

On the contrary, intermediate operators

play08:51

just set up a chain of operations

play08:53

that are executed lazily when an item is emitted into the flow.

play08:57

Every time collect is called on user messages,

play09:00

a new flow, or pipe, will be created.

play09:03

And its producer block will start refreshing the messages

play09:07

from the API at its own interval.

play09:10

In coroutines jargon, we refer to this type of flows as called flows

play09:15

as they are created on demand

play09:17

and emit data only when they are being observed.

play09:20

Let's see now how to optimally collect flows

play09:24

from the Android UI.

play09:26

There are two main things to consider:

play09:28

The first one is about not wasting resources

play09:31

when the app is in the background,

play09:32

and the second one is about configuration changes.

play09:36

Let's imagine we are in messages activity

play09:38

and we want to display the list of messages on the screen.

play09:42

For how long should we be collecting from the flow?

play09:46

The UI should be a good citizen

play09:48

and stop collecting from the flow

play09:50

when the UI is not displayed on the screen.

play09:54

Back to the water analogy,

play09:56

Pancho should close the tap while brushing their teeth

play09:59

or going for a nap.

play10:00

Poncho shouldn't be wasting water.

play10:03

Similarly, the UI shouldn't be collecting from flows

play10:07

if the information isn't going to be displayed on the screen.

play10:11

To do this, there are different alternatives.

play10:14

And all of them are aware of the UI life cycle.

play10:17

You can use life data or lifecycle coroutine-specific APIs

play10:21

such as repeatOnLifecycle and flowWithLifecycle.

play10:26

The asLiveData flow operator compares the flow to live data

play10:31

that observes items only while the UI is visible on the screen.

play10:36

This conversion is something we can do in the view model class.

play10:40

In the UI, we just consume the live data as usual.

play10:44

But OK, this is cheating a bit

play10:46

because it's adding a different technology into the mix,

play10:50

which shouldn't be needed.

play10:51

RepeatOnLifecycle is the recommended way to collect flows from the UI layer.

play10:58

RepeatOnLifecycle is a suspend function

play11:00

that takes a life cycle step as a parameter.

play11:03

This API is lifecycle aware,

play11:05

as it automatically launches a new coroutine with a block pass to it

play11:10

when the lifecycle reaches that step.

play11:13

Then, when the lifecycle falls below that state,

play11:16

the ongoing coroutine is canceled.

play11:19

Inside the block, we can call collect,

play11:22

as we are in the context of a coroutine.

play11:25

As repeatOnLifecycle is a suspend function,

play11:28

it also needs to be called in a coroutine.

play11:31

As you are in an activity, we can use lifecycleScope to start one.

play11:36

As you can see, the best practice

play11:38

is to call this function when the lifecycle is initialized,

play11:42

for example, in onCreate in this activity.

play11:45

RepeatOnLifecycle's restartable behavior

play11:48

takes into account the UI lifecycle automatically for you.

play11:53

Something important to note is that the coroutine that calls repeatOnLifecycle

play11:58

won't resume executing until the lifecycle is destroyed.

play12:03

So, if you need to collect from multiple flows,

play12:06

you should create multiple coroutines using launch

play12:09

inside the repeatOnLifecycle block.

play12:13

You can also use the flowWithLifecycle operator

play12:16

instead of repeatOnLifecycle

play12:17

when you have only one flow to collect.

play12:20

This API emits items and cancels the underlying producer

play12:25

when the lifecycle moves in and out of the target state.

play12:29

To show how this works visually,

play12:31

let's take a tour through the activity lifecycle

play12:34

when it's first created,

play12:35

then sent to the background because the user pressed the Home button,

play12:39

which makes the activity receive the onStop signal

play12:42

and then, opening the app again when onStart is called.

play12:47

When you call repeatOnLifecycle with the start state,

play12:50

the UI processes flow emissions while it's visible on the screen.

play12:54

And the collection is canceled when the app goes to the background.

play12:58

RepeatOnLifecycle and flowWithLifecycle

play13:01

are new APIs added in the stable 2.4 version

play13:05

of the Lifecycle runtime ktx library.

play13:09

Because they are new, you might be collecting flows

play13:11

from the Android UI in a different way.

play13:14

For example, you might be collecting directly from a coroutine

play13:18

launched by lifecycleScope.

play13:20

While this might seem OK to use,

play13:22

collecting flows in this way is not always safe.

play13:26

This collects from the flow and updates UI elements,

play13:30

even if the app is in the background.

play13:32

In fact, this is not the only case.

play13:35

Other solutions like the lifecycle coroutine scope

play13:38

launch when X API family suffer from similar problems.

play13:43

You don't like Poncho wasting water, do you?

play13:46

Then, we shouldn't be collecting from the flow

play13:48

if those items aren't going to be displayed on the screen.

play13:52

If you collect directly from lifecycleScope. launch,

play13:56

the activity keeps receiving flow updates while in the background.

play14:00

That can be both wasteful and dangerous,

play14:03

as, for example, showing dialogues when the app is in the background

play14:07

can make your application crash.

play14:09

To solve this issue,

play14:11

you could manually start collecting in onStart

play14:13

and stop collecting in onStop.

play14:16

While that's OK,

play14:17

using repeatOnLifecycle removes all that boilerplate code.

play14:22

If we look at launchWhenStarted as an alternative,

play14:25

it is better than lifecycleScope. launch

play14:28

because it suspends the flow collection while the app is in the background.

play14:32

However, this solution keeps the flow producer active,

play14:36

potentially emitting items in the background

play14:39

that can fill the memory with items

play14:41

that aren't going to be displayed on the screen.

play14:44

As the UI doesn't really know how the flow producer is implemented,

play14:49

it is always better to play safe and use repeatOnLifecycle

play14:53

or flowWithLifecycle to avoid collecting items

play14:56

and keeping the flow producer active when the UI is in the background.

play15:01

If this optimizes flow collection when the app goes to the background,

play15:05

Jose is going to tell you some tricks

play15:07

for when the app goes through configuration changes.

play15:11

When you expose a flow to a view,

play15:12

you have to take into account that you are trying to pass data

play15:15

between two elements that have different lifecycles.

play15:19

And not any lifecycle.

play15:21

The lifecycle of activities and fragments, which can be tricky.

play15:24

As a crucial example, remember that,

play15:27

when a device is rotated or receives a configuration change,

play15:31

all activities might be restarted,

play15:33

but a view model survives that.

play15:35

So from a view model, you can't just expose any flow,

play15:39

for example, a call flow like this one.

play15:41

A call flow results every time it's collected for the first time.

play15:45

So, the repository would be called again after a rotation.

play15:50

What we need is some kind of buffer, something that can hold data

play15:54

and share it between multiple collectors

play15:56

no matter how many times they are recreated.

play15:59

StateFlow was created for exactly that.

play16:02

A StateFlow is a water tank in our lake analogy.

play16:06

It holds data even if there are no collectors.

play16:08

You can collect multiple times from it,

play16:10

so it's safe to use with activities or fragments.

play16:14

You could use the mutable version of StateFlow

play16:16

and update its value whenever you want,

play16:18

for example, from a coroutine like in here.

play16:21

But that's not very reactive, is it?

play16:23

Pancho would suggest you improve your game.

play16:27

Instead, you can convert any flow to a StateFlow.

play16:30

If you do that, the StateFlow receives all the updates from the upstream flows

play16:34

and stores the latest value.

play16:36

And it can have zero or more collectors, so this is perfect for view models.

play16:41

There are more types of flows, but this is what we recommend

play16:44

because we can optimize StateFlow very precisely.

play16:48

To convert a flow to StateFlow,

play16:50

you can use the stateIn operator on it.

play16:53

It takes three parameters:

play16:55

Initial value, because a StateFlow always needs to have a value,

play16:58

a coroutine scope, which controls when the serving is started.

play17:02

We can use the ViewModel scope for this.

play17:05

And started, which is the interesting one.

play17:07

We're going to get to what that WhileSubscribed(5000) means.

play17:11

But first, let's look at two scenarios.

play17:14

The first scenario is a rotation where the activity,

play17:17

which is the collector of the flow,

play17:19

is destroyed for a short period of time and then recreated.

play17:23

The second scenario is a navigation to home

play17:26

where our app is put in the background.

play17:29

In the rotation scenario, we don't want to restart any flows

play17:32

to make the transition as fast as possible.

play17:35

In the navigation to home however,

play17:37

we want to stop all flows to save battery and other resources.

play17:42

So, how do we detect which one is which?

play17:45

We do that with a timeout.

play17:48

When a StateFlow stops being collected,

play17:50

we don't immediately stop all the upstream flows.

play17:53

Instead, we wait for some time, for example, five seconds.

play17:56

If the flow is collected again before the timeout,

play17:59

no upstream flows are canceled.

play18:02

That is exactly what the WhileSubscribed(5000) does.

play18:07

In this diagram, we show what happens when the app goes to the background.

play18:10

Before the Home button is pressed,

play18:12

the view is receiving updates

play18:14

and the StateFlow has its upstream flows producing normally.

play18:18

Now, when the view stops, the collection ends immediately.

play18:22

However, the StateFlow, because of how we configured it,

play18:25

takes five seconds to stop its upstream flows.

play18:28

Now the timeout passes and upstream flows are canceled.

play18:32

Only when the user opens the app again, if that ever happens,

play18:36

the upstream flows are automatically restarted.

play18:40

In the rotation scenario however,

play18:42

the view is only stopped for a very short time,

play18:45

less than five seconds anyway.

play18:46

So the StateFlow never gets restored and keeps all upstream flows active,

play18:51

acting as though nothing happened

play18:53

and making the rotation instant to the user.

play18:56

So in summary, we recommend that you use StateFlow

play19:00

to expose your flows from a view model, or keep using as like data,

play19:04

which does exactly the same thing.

play19:06

If you want to learn even more about StateFlow or its parent, SharedFlow,

play19:10

check out the resources at the end.

play19:13

Now, you might be wondering, "How do I test this?"

play19:15

Well, testing a flow can get tricky

play19:18

because you're dealing with streams of data.

play19:20

So there are a couple of tricks you can use.

play19:22

First, there are two scenarios.

play19:24

In the first one, the unit and the test,

play19:27

whatever you're testing, is receiving a flow.

play19:30

The easy way to test this is to replace the dependency

play19:33

with a fake producer.

play19:35

You would program this fake repository, in this example,

play19:38

to emit whatever you need for the different test cases,

play19:41

for example, with a simple call flow.

play19:44

The test itself would make assertions on the output of the subject and the test,

play19:49

which is a flow or something else.

play19:53

Secondly, if the unit under test is exposing a flow

play19:56

and that value or stream of values is what you want to verify,

play20:00

you have multiple ways to collect it.

play20:03

You can call the first method on the flow,

play20:05

and this is going to collect until it receives the first item

play20:08

and then stop collecting.

play20:11

But also, you can use operators, such as take(5),

play20:15

and call the two list terminal operator to collect exactly five messages,

play20:19

which can be useful.

play20:21

Hopefully, in this talk you've learned

play20:23

why a reactive architecture is a good investment

play20:25

and how to build your infrastructure with Kotlin Flow.

play20:29

If you feel inspired, we have a ton of content around this,

play20:32

a guide that covers the basics,

play20:34

and blog posts where we do deep dives on some topics.

play20:37

Also, if you want to see all of this in context,

play20:40

check out the Google I/O app,

play20:42

which we updated earlier this year to include flows everywhere.

play20:46

Thank you.

Rate This
β˜…
β˜…
β˜…
β˜…
β˜…

5.0 / 5 (0 votes)

Related Tags
Reactive ProgrammingKotlin FlowsAndroid DevelopmentData StreamsUI LifecycleOptimizationCoroutinesData ManagementUser ExperienceApp ArchitectureTesting Strategies