Users expect to see real-time data while using apps and don't want to refresh the screen. This is achieved by using Reactive Programming. In this article, you’ll explore why Reactive Programming exists and how it enables apps to update with new data in real time.
In this series of articles, I am writing about Reactive Programming. This programming paradigm helps systems react to data changes. It does this using a declarative approach, making it easier to dynamically change presented information as soon as new data arrives. Reactive Programming approaches allow developers to think about what they want to show instead of how to show it.
In this article, I will first briefly examine Reactive Programming after discussing other alternatives. I'll explain their pitfalls before introducing the concept of Reactive Programming. In the following article, I'll illustrate how to leverage Reactive Programming to build your front-end application by showing a real-life implementation, leaving behind old approaches prone to showing stale data or different values for the same data on different screens.
Tokenpad is an application developed using Flutter. It allows users to aggregate their crypto portfolios (we support up to 13 chains and growing) and have all of their information available at a glance. This helps users become smarter crypto investors.
Users can also add their Decentralized Finances (DeFi) positions so they have the most up-to-date information about current yields. Additionally, Tokenpad will notify users about potential liquidations, display token price changes, and check token price history, among other features. Today, Tokenpad has thousands of daily users and handles hundreds of different cryptocurrency tokens, displaying the current status for each account and each user.
We are obsessed with meeting user needs and becoming a leading crypto aggregator app. We constantly validate market needs and receive and analyze user feedback and requests. We have new features in the pipeline, such as transaction history, profit and loss calculations, and investment suggestions.
Tokenpad can handle hundreds of tokens from hundreds of different wallets, chains, exchanges, or DeFis and store, aggregate, and display all that data to users in a friendly and understandable way on multiple screens using different views. Thanks to Reactive Programming, users can continue to use Tokenpad to access up-to-date information in the app while more information is being fetched and processed in the background. As a crypto investor, you would always have the latest data available to make your own choices.
Processing and displaying all that information requires time to:
As we know, users prefer to avoid staring at a circular loader while the system is fetching and processing, waiting for data to be displayed on their screen. According to Google, 53% of users abandon a mobile web application if it takes more than three seconds to load. Users want to use an app and see available data.
Our job as developers is to allow users to see the most current information as soon as it's ready to be shown, even if it means updating the screen they're viewing. Users shouldn't have to manually refresh the screen or interact with the app in any way to see updated information reflected on the screen. With Reactive Programming, the app should react to new data arriving.
Fetching and showing data in apps before Reactive Programming
Not long ago, when creating applications that fetched and displayed data on a screen, an application would follow these steps:
These steps seem logical and necessary. But what happens if the data changes during steps two through four?
There are two main scenarios of what could happen:
For scenario a:
We can re-execute steps two through four to update the information on the screen.
For scenario b:
I can think of two options:
b1 - The application regularly retrieves data in the background and updates the screen if data changes.
b2 - The app displays the original data on the screen until the user manually refreshes the data. Then, the app executes steps two through four so the user can see updated information.
Both situations are sub-optimal for users as they expect to see information updated immediately.
For option b1, the interval between data retrieval updates might be too long (let's say once an hour) for the user to wait for the latest information or too short (let's say once every five seconds), wasting resources and negatively affecting user experience (by increasing page load times or battery consumption).
For option b2, if we don’t update the information on the screen, the users won’t know that the information displayed is no longer accurate. They would need to refresh the screen whether data was updated or not. In that case, we are making the users do unnecessary work.
The Observer Pattern, a key concept in Reactive Programming, can solve this conundrum!
In summary, it consists of two kinds of software components:
By using the Observer Pattern, we are accomplishing a few goals:
In other words, the user is NOT in charge of requesting data; the app reacts to data changes as soon as it's notified.
The first time I used the Observer Pattern, I was surprised that I could split the responsibility between knowing when an event occurred and processing that same event. With this pattern, I can be sure that the app will only react when an event occurs rather than constantly polling to check if a reaction is necessary.
Using the Observer pattern, we are making our application more reactive, allowing it to correctly respond to data changes and updates everywhere.
Even though the Observer Pattern is a substantial improvement, its implementation can still be enhanced, especially when multiple steps and branches are involved in processing the obtained data.
Let’s consider a real-life example:
Tokenpad has a "Consolidated Tokens" screen, where we show aggregated information about all the tracked tokens in the user’s portfolios.
For that screen, we needed to obtain the portfolios from the database and fulfill the following requirements:
To achieve these operations using the Observer Pattern, we could have the following code:
class Token {
final String url;
final String code;
final double usdValue;
Token(this.url, this.code, this.usdValue);
}
class Portfolio {
final String chain;
final List<Token> tokens;
Portfolio(this.chain, this.tokens);
}
class TokenAndPercentage {
final Token token;
final double percentage;
TokenAndPercentage(this.token, this.percentage);
}
Here we define the models that support all the calculations and will allow us to show the data on the screen:
class PortfoliosSubscriber implements Subscriber<List<Portfolio>> {
@override
void processEvent({required List<Portfolio> event}) {
final filteredPortfolios = event.where(
(element) => element.chain == currentlyFilteredChain,
); // 1: Filter Portfolios by selected chain
final filteredTokens = filteredPortfolios
.map(
(Portfolio portfolio) =>
portfolio.tokens, // 2: Extract Tokens from Portfolios
)
.expand(
(List<Token> tokens) =>
tokens,
// 3: Flatten matrix of Token into List of Token
)
.toList();
final Map<String, List<Token>> groupedTokensByCode =
groupBy(filteredTokens);
// 4: Group all Tokens by Token code field
final List<Token> tokensWithAddedValue = calcTotalValueByTokenCode(
groupedTokensByCode.values,
); // 5: Add up all the usdValue of the Tokens for each code
final sortedTokensWithAddedValue = tokensWithAddedValue.sort(
(Token a, Token b) => a.usdValue.compareTo(
b.usdValue,
),
); // 6: Sort the Tokens by its added usdValue
final List<TokenAndPercentage> top4Tokens =
calculateTop4Tokens(tokensWithAddedValue);
// 7: Extract top 4 Token
// 8: Show the sortedTokensWithAddedValue in the screen
// 9: Show the top4Tokens in the screen
}
List<Token> calcTotalValueByTokenCode(Iterable<List<Token>> values) {
/// TODO: Add up all tokens values and return a
/// single representative of the Token with its usdValue
/// field having the calculated addition
}
Map<String, List<Token>> groupBy(List<Token> tokens) {
// TODO: Group all tokens by its code field
}
List<TokenAndPercentage> calculateTop4Tokens(
List<Token> tokensWithAddedValue) {
/// TODO: Calculate the percentage of each token
/// wrt the total and return the top4 and "Other"
}
}
In the code, we can see how to receive new events in the processEvent method and start to process, step by step, the received list of Portfolios to calculate the necessary information to be displayed in the Consolidated Tokens screen.
It is important to note that although the logic is divided into steps, all of it depends on the processEvent argument (the list of Portfolios). As a result, the logic piles up inside the processEvent block. We could use the Extract Method refactoring to move the logic out of that block. However, in the end, all the logic will still depend on the same processEvent argument. That code block will grow with every new feature that depends on it.
I like to define Reactive Programming as the Observer Pattern on steroids.
Reactive Programming allows us to have the benefit of the Observer Pattern's push-based approach while also having some extra benefits, such as:
In their book “Reactive Programming with RxJava," Tomasz Nurkiewicz and Ben Christensen define Reactive Programming as:
Reactive Programming is a general programming term that is focused on reacting to changes, such as data values or events. It can and often is done imperatively. A callback is an approach to reactive programming done imperatively. A spreadsheet is a great example of reactive programming: cells dependent on other cells automatically “react” when those other cells change[…]
Therefore, it is a programming approach - an abstraction on top of imperative systems - that allows us to program asynchronous and event-driven use cases without having to think like the computer itself and imperatively define the complex interactions of state, particularly across thread and network boundaries.
That definition mentions two key concepts: reacting to change (a piece of what we already saw in the Observer Pattern) and not having to think like the computer itself.
The main shift when using Reactive Programming is that instead of imperatively telling the computer how and when to do calculations (static data and operations), we have a "living" stream of data to which we subscribe. The code automatically reacts to its changes, updating all the calculations in a way that's closer to how we, as humans, think instead of us having to accommodate our thinking to the computers' way.
Let's try to see this in the code:
int a = 10;
int b = 15;
int sum = a + b; // sum = 25
b = 25; // sum stays as 25 instead of increasing to 35
This is an example of static code. The variable b is initialized as 15, and then the variable sum is calculated in terms of a and b. Subsequently, b is updated from 15 to 25. In a reactive world, the value of the sum would be updated to 35, but given that this code is not reactive (it's static), its value stays at 25.
To update the value to 35, we must explicitly call the sum = a + b again.
If we want to transform this code into Reactive, we need to reference a couple of concepts:
Keeping these two concepts in mind, this is how we update the static code to update the value of the sum reactively:
int a = 10;
Stream<int> b = Stream.fromIterable([15, 25, 35, 45]);
// 1: One way of creating a Stream
b.listen((int newBValue) { // 2: The subscription is created
int sum = a + newBValue;
});
This code adds a and b, but this time uses Streams.
The sum value is recalculated whenever the value of b is updated (The Stream emits). The subscription to a Stream is created by calling the Stream’s listen method, which is equivalent to calling addSubscriber in the Observer Pattern.
In this example Stream b will emit the following values in order: 15, 25, 35, and 45. We can be sure that the value of sum gets updated to 25, then 35, 45 and finally 55.
The sum is updated every time b emits without us having to explicitly reassign it in a separate call.
Building data-heavy applications requires developers to write code to retrieve, store, process, and show data. Having tools that make these steps easy to write, understand, and modify extends benefits for all involved, from the developer to the application users.
Reactive Programming is one tool for achieving these goals. It changes the way developers think about building user-facing applications, abstracting away the computer-oriented approach while including additional features that make it even more powerful.
Stay tuned for the next part, where I'll explore the world of Reactive Programming, including real-life challenges in Tokenpad and how Reactive Programming helped us.