Reactive way of Mobile App Development using Kotlin Flow

Android Dec 21, 2022

The Android platform is asynchronous by nature. Information can flow asynchronously through your app, from multiple sources. The system will trigger the broadcast receivers, forward intents, and recreate your UI on each configuration change. Developers should handle  asynchronous programming logic to real-time updates without blocking any in-app events. Kotlin flow is a candidate in this use case.

Why do we need Kotlin Flow?

We will compare Kotlin flow with other reactive programming libraries such as LiveData and RxJava. We will explain how Kotlin flow is better compared to these two libraries.

We were using LiveData for Reactive Programming previously in Halodoc. We started shifting to Kotlin Flow recently, as it has some benefits over LiveData and RxJava.

About Kotlin Flow

Kotlin Flow can emit multiple values sequentially and returns a single value in the background thread using suspend functions which consist of a producer and a consumer. As the names suggest, a producer emits values while the consumer receives the values.

The flows are cold. We receive the data once we start to collect it. Each emitted value is processed by all the intermediate operators from upstream to downstream before it gets delivered to terminal operators.

Step 1: Creating a producer

To create a flow we need a producer. The producer acts as Data Repository or View Model to emit the values to the network or the database.

Here we have used "suspend function" for the flow builder. The "suspend function" is not necessarily needed for the producer. The emitted values must be of the same type, i.e. Flow<Int>. However, along with the producer there should be the consumer to collect emitted values.

Step 2: Modifying Stream Data

Sequential data streams can be modified through intermediate operators like "map", "filter" , etc.

Here filter operator is fetching one by one from a list of integer values and filtering them to an even integer list. As flow is cold, intermediate operations for flow won't apply or start until we call the .collect{} to collect values.

Step 3: Creating a consumer

Consumers mainly act as UI to get fetched data and display it on the screen. To get data stream values in the stream, we use .collect.

Without calling .collect, flow won't emit computed values. In this example, we are collecting even numbers from an integer list from 1 to 5, i.e. 2 and 4. The producer will emit the next value if a consumer has collected(consumed) the previous value. For this example, produce will process and produce the value 4 once the consumer collects the value 2.

Buffering

Flows execute code in the building block, operator, and terminal operator sequentially. The total execution time is going to be the sum of the execution times of all the operators. To prevent this, we use a buffer. For our example, we can see the total delay of 2 seconds (1 sec from the producer block and 1 sec from the consumer block).

The buffer() operator creates a separate coroutine during execution for the flow applied. This coroutine will be used to execute the code in the producer and upstream operators.

If the consumer is slower than the producer, the buffer will be full at some point. By default it will suspend the producer coroutine until the consumer coroutine catches up.

Context Preservation

By default, code in builders, intermediate operators, and the collection is performed in the coroutine context that invoked the terminal operator. This property of flows is called context preservation.

This behaviour might look fine for fast-running, non-blocking code, but for some long-term operations, you might want to execute the code in a different Dispatcher, like Default or IO.

We can change the context by using flowOn()

Collection and emission are now happening concurrently in different coroutines. Remember that the flowOn operator only changes the coroutine context in the builder and operators applied upstream of it.

StateFlow

StateFlow is hot, which means if Activity or Fragment navigates or gone through a different configuration even if you are calling flow builder it will execute and returns the latest value to the collector of the state flow which is not the case for flow (which is cold by default).

StateFlow is a hot observable that stores the latest value. When created, it requires you to pass an initial state to it and any observer can check its current value or subscribe to it at any time to receive updates with the current value as it changes.

From the above example, we observe that StatFlow is very much similar to LiveData, but there are two differences

  • StatFlow requires an initial value
  • The method observe() from LiveData automatically unregisters the consumer when the view enters the STOPPED state. Flow collection is not stopped automatically, but this behaviour can be easily achieved with the repeatOnLifecycle extension.

We have normally three lifecycle-aware alternatives for handling Flow collecting in the background or stopped state

  • Flow<T>.asLiveData(): LiveData
  • Lifecycle.repeatOnLifeCycle(state)
  • Flow<T>.flowWithLifecycle(lifecycle, state)

asLiveData() converts flow type to live data for observable to unregister consumer when the view enters life cycle STOP state. However, we might not get the best solution as it combines Flow and LiveData functionalities.

repeatOnLifeCycle is a suspending function. You need to specify a Lifecycle.State as its input. A new coroutine will be launched each time the lifecycle reaches that state and will automatically be cancelled once the lifecycle goes below the specified state:

Note that the repeatOnLifecycle suspends the coroutine until the lifecycle reaches the DESTROYED state. To collect several flows, you can launch several coroutines like below:

If you only need to collect a single flow, flowWithLifecycle can be used:

The APIs repeatOnLifeCycle and flowWithLifecycle are the currently recommended approaches and are available in the androidx.lifecycle:lifecycle-runtime-ktx library.

Shared Flow

As we know that State Flow will update the latest when configuration changes for Activity or Fragments, but when we want to show the snack bar once only to indicate the value is being fetched from the server, if we use State Flow when a user rotates the screen, Activity or Fragments will be created resulting into value again will be fetched from server and snack bar will appear more than once, to avoid this we use Shared Flow.

It has parameters such as the number of old events to keep and replay for new subscribers and the extraBufferCapacity to provide cushion for fast emitters and slow subscribers and BufferOverflow strategy to either suspend the emitter or drop the oldest/newest emitted value.

Conclusions

In this article, we have tried to provide insights about the important topics of Flow, which is beneficial for reactive way programming in Kotlin. It will be better to implement it in our projects as we are migrating to JetPack Compose. Flow allows one to write some custom operators using delay instead of Thread. As Normal Flow is cold and State, Shared Flows are hot. We can utilise them at our convenience. A separate stream type is not needed to handle back pressure if we use Kotlin Flow. Flow by itself supports back pressure. Flows support Nullability. It can also handle exceptions.

Join us

Scalability, reliability, and maintainability are the three pillars that govern what we build at Halodoc Tech. We are actively looking for engineers at all levels and  if solving hard problems with challenging requirements is your forte, please reach out to us with your resumé at careers.india@halodoc.com.

About Halodoc

Halodoc is the number 1 all around Healthcare application in Indonesia. Our mission is to simplify and bring quality healthcare across Indonesia, from Sabang to Merauke. We connect 20,000+ doctors with patients in need through our Tele-consultation service. We partner with 3500+ pharmacies in 100+ cities to bring medicine to your doorstep. We've also partnered with Indonesia's largest lab provider to provide lab home services, and to top it off we have recently launched a premium appointment service that partners with 500+ hospitals that allow patients to book a doctor appointment inside our application. We are extremely fortunate to be trusted by our investors, such as the Bill & Melinda Gates Foundation, Singtel, UOB Ventures, Allianz, GoJek, Astra, Temasek many more. We recently closed our Series C round and In total have raised around USD$180 million for our mission. Our team works tirelessly to make sure that we create the best healthcare solution personalised for all of our patient's needs, and are continuously on a path to simplify healthcare for Indonesia.

Mayur Hebbar

Android Developer