A Dive into Combine, Merge, Zip and Transform Functions for Efficient Flow Management
Photo by christian buehner on Unsplash
In modern reactive programming, managing asynchronous data streams is a crucial skill. Kotlin’s Flow API provides a powerful toolkit for dealing with such streams, enabling developers to handle data emissions and transformations seamlessly. In this article, we’ll delve into three essential functions within the Flow API — combine
, merge
, and zip
. These functions allow developers to orchestrate multiple flows of data, enabling elegant solutions to complex scenarios. We’ll explore each function in detail, showcasing their usage, benefits, and real-world examples.
Introduction
Kotlin’s Flow API offers an elegant and efficient way to manage asynchronous data streams. When dealing with multiple data sources, it’s often necessary to synchronize and combine these streams to perform specific operations. The combine
, merge
, and zip
functions play a pivotal role in achieving this synchronization. Whether you’re handling notifications, updates, or any scenario that involves data streams, these functions can be your secret weapons.
Merging Streams with .merge()
The merge
function is a workhorse for interleaving data streams. It fuses multiple flows into a single flow, emitting values in the order they arrive from the source flows. Let’s explore the quickNotifications()
function —
Imagine you want to display quick notifications to users for activities like comments, likes, and posts. Using merge
, you can interleave these notifications efficiently, enhancing user engagement. The sequence of results will print as follows based on our artificial simulation.
- Likes with a slight delay (50ms).
- Comments with a moderate delay (200ms).
- New posts with a balanced delay (100ms).
As a result, users receive notifications that closely mimic real-time interactions, thanks to strategically implemented delays. The example illustrate how merge
maintains the order of notifications while ensuring a dynamic user experience.
.merge() diagrammatic representation
Code
private fun quickNotifications(){ viewModelScope.launch { val likesActivity: Flow<Like> = flowOf(*likesList.toTypedArray()).onEach { delay(50) } val commentsActivity: Flow<Comment> = flowOf(*commentsList.toTypedArray()).onEach { delay(200) } val postsActivity: Flow<Post> = flowOf(*postsList.toTypedArray()).onEach { delay(100) } merge(likesActivity, commentsActivity, postsActivity).collect { when(it) { is Comment -> { Log.i("Quick Notification", "${it.userName} commented on your post: ${it.commentText}") } is Like -> { Log.i("Quick Notification", "${it.userName} reacted with ${it.reactionName} to your post: ${it.postTitle}") } is Post -> { Log.i("Quick Notification", "${it.userName} added a new post: ${it.postTitle}") } } } } }
Example Data Input
You can find the data class and example data here.
Example Data Output
Alice reacted with 👍 to your post: Exploring the Cosmos Alice added a new post: Exploring the Cosmos Rahul reacted with ❤ to your post: Starry Night Photography Grace reacted with 👏 to your post: Astronomy Basics Alice commented on your post: This is amazing! Bob added a new post: Starry Night Photography Charlie reacted with 👍 to your post: Starry Night Photography Charlie reacted with ❤️ to your post: Astronomy Basics Charlie added a new post: Astronomy 2 Basics Bob reacted with 👏 to your post: Exploring the Cosmos Charlie commented on your post: Beautiful shot!
As we can see, the activities are emitted in the order of the delays — likes, posts, comments, and more. This is why the output shows these activities interleaved based on their delay values. The merge
function combines these activities in the order in which they are emitted, regardless of their individual delays.
Combining Data Streams with .combine()
.combine
is a function provided by Kotlin’s Flow API. It allows us to combine two or more flows into a single flow, emitting values whenever any of the source flows emit a new value. This is useful for scenarios where you want to react to changes in multiple data sources and perform some operation on the combined data. Let’s look at a real-world example —
Consider a social media app where users can like, comment, and post. The function summarizedNotifications()
combines these activities into concise notifications. The sequence of results will print as follows —
- Likes and comments are combined for each corresponding post.
- The combined data is then merged with the posts, forming comprehensive notifications.
The use of the delay
function simulates real-time interactions for a more authentic user experience. For instance, users liking a post followed by a few comments followed by more reactions. This simulation adds a layer of realism to your app’s notification system.
.combine() diagrammatic representation
Code
Job Offers
private fun summarizedNotifications() { viewModelScope.launch { val likesActivity: Flow<Like> = flowOf(*likesList.toTypedArray()).onEach { delay(50) } val commentsActivity: Flow<Comment> = flowOf(*commentsList.toTypedArray()).onEach { delay(200) } val postsActivity: Flow<Post> = flowOf(*postsList.toTypedArray()).onEach { delay(100) } likesActivity.combine(commentsActivity) { like, comment -> "${like.userName} reacted with ${like.reactionName} on ${like.postTitle}\n" + "and ${comment.userName} commented ${comment.commentText} on ${comment.postTitle}" }.combine(postsActivity) { activity, post -> Log.i("Notification", "$activity .. Also ${post.userName} added a new post '${post.postTitle}'") }.collect() } }
Example Data Input
You can find the data class and example data here.
Example Data Output
Rahul reacted with ❤ on Starry Night Photography and Alice commented This is amazing! on Exploring the Cosmos .. Also Alice added a new post 'Exploring the Cosmos' Rahul reacted with ❤ on Starry Night Photography and Alice commented This is amazing! on Exploring the Cosmos .. Also Bob added a new post 'Starry Night Photography' Grace reacted with 👏 on Astronomy Basics and Alice commented This is amazing! on Exploring the Cosmos .. Also Bob added a new post 'Starry Night Photography' Charlie reacted with 👍 on Starry Night Photography and Alice commented This is amazing! on Exploring the Cosmos .. Also Bob added a new post 'Starry Night Photography' Charlie reacted with 👍 on Starry Night Photography and Alice commented This is amazing! on Exploring the Cosmos .. Also Charlie added a new post 'Astronomy 2 Basics' ... ... ...
Based on the delays provided in the code, likes will be emitted every 50 milliseconds, comments every 200 milliseconds, and posts every 100 milliseconds. The delay values influence the order in which the activities are emitted and combined.
As you can see, the activities are emitted in the order of the delays — likes, posts, and then comments. This is why Alice’s first like (refer input data) doesn’t appear first in the output. The delay of 50 milliseconds for likes causes the subsequent activities to be emitted and combined. As a result, the combination that striked (Rahul’s like) after her like at 100ms is the one that is actually printed.
Tip💡 — Do try
.combine()
with all three flows in the same function call to see the difference.
combine(likes, comments, posts) {likes, comments, posts -> // log here }.collect()
Pairing Streams with .zip()
.zip
is a function provided by Kotlin’s Flow API. It combines values from multiple flows into pairs or tuples, emitting a new pair whenever all source flows have emitted a new value. The zip
function orchestrates multiple flows, pairing their emissions to trigger actions based on specific criteria. Take a look at the personalizedNotifications()
function for example —
Suppose you’re interested in notifying users when their likes and comments match for a given post. By zipping likes and comments, you can detect such instances and create personalized notifications. The zip
function ensures that the emissions are paired according to their chronological order, facilitating precise matches.
.zip() diagrammatic representation
Code
private fun personalizedNotifications() { viewModelScope.launch { val likesActivity: Flow<Like> = flowOf(*likesList.toTypedArray()) val commentsActivity: Flow<Comment> = flowOf(*commentsList.toTypedArray()) likesActivity.zip(commentsActivity) { like, comment -> if(like.userName == comment.userName && like.postTitle == comment.postTitle) { Log.i("Notification","${like.userName} reacted and commented on your post ${like.postTitle}") } }.collect() } }
As you can see, the .zip
operation compares items with the same index from different flows 1:1 and emits a combined value if the conditions are met.
It won’t log a notification till it finds a reaction and like from the same user as per our code block above.
Example Output
Alice reacted and commented on your post Exploring the Cosmos
If for example, we change the input data to have
Like("Alice", "Astronomy Basics", "👏") // instead of Like("Grace", "Astronomy Basics", "👏") // in the likes list
New Output
Alice reacted and commented on your post Exploring the Cosmos Alice reacted and commented on your post Astronomy Basics
Other Example Use Cases
The provided examples mirror social media interactions, but these operators have broader applicability —
- E-commerce — Tracking product orders, payment confirmations, and shipping updates using
.zip
or.combine
. - Real-time Collaborations — In collaborative apps, like document editing,
.merge
can synchronize changes from multiple users. - Multi-Source Data — Combining data from various sources (APIs, databases) using
.zip
or.combine
for unified processing.
Pro Tip 💡
Using .transform
before using any of the above—
The difference between using .transform
followed by .collect
and directly using .merge().collect()
or any of the above followed by .collect()
lies in the separation of concerns and the ability to encapsulate different processing logic.
When you use .transform
followed by .collect
, you have the advantage of encapsulating the transformation logic within the specific flow that emits the raw data (likes, comments, or posts). This makes your code more modular and easier to understand. It separates the transformation process from the collection process, making your code more maintainable and allowing you to focus on the specific transformation of each item.
On the other hand, directly using .merge().collect()
for transformation means you’re embedding the transformation logic within the collection process. While this approach might work for simpler cases, it can become less readable and harder to manage as the complexity of the transformation logic grows. It also makes it more challenging to reuse or modify the transformation logic independently.
Consider the following benefits of using .transform
followed by .collect
—
- Modularity — Each flow (
likesFlow
,commentsFlow
,postsFlow
) encapsulates its own transformation logic, making it easier to maintain and understand. - Reusability — If you need the same transformation logic elsewhere, you can reuse the flow with
.transform
without duplicating the logic. - Clarity — The separation of transformation and collection logic improves code readability and makes it clear what each part of the code is doing.
In the end, the approach you choose depends on the complexity of your use case and your preference for code organization. For simpler transformations, using .merge().collect()
directly might be suitable, while for more complex and modular scenarios, separating transformation using .transform
followed by .collect
is recommended.
Conclusion
In the realm of reactive programming, Kotlin’s Flow API offers a treasure trove of tools to manage asynchronous data streams effectively. The combine
, merge
, and zip
functions elevate your coding game by enabling synchronization, interleaving, and pairing of data streams. By understanding their nuances, one can build applications that deliver seamless and engaging user experiences.
Disclaimer
The code snippets provided in this article are intended for educational purposes only and may not fully represent production-ready code. The focus is on explaining the concepts and functionalities of the Flow API functions.
With the knowledge gained from this exploration, you’re well-equipped to wield the power of the Flow API’s combine
, merge
, and zip
functions in your projects.
Code and Closing Remarks
To make it easier here are two files NotificationsViewModel.kt
and Data.kt
that have all the previously mentioned code along with the sample data.
If you liked the content, please feel free to leave your valuable feedback or appreciation. I am always looking to learn and collaborate with fellow developers.
Follow me on Medium for more articles — Medium Profile
Connect with me on LinkedIn for collaboration — LinkedIn Profile
Happy Coding!
This article was previously published on proandroiddev.com