Using ReactiveX to build reliable stateless mobile applications
Heetch is a ride sharing service to help people going out at night. Therefore it is essentially used through its mobile applications.
This means that how the entire service performs is largely experienced through how the mobile applications are behaving in the hands of users. This behavior is essentially reflecting how state is managed by the mobile application, so let’s see how it can be handled in a more efficient way.
To get started, it is critical to never display an invalid state, especially in a ride-sharing application. Showing to a passenger that he is still free to order a ride while there is already a driver coming to pick him up can lead to confusion and loss of time.
Clients and Backend
Traditionally, there is the client state and the backend state. All the work is about keeping them in sync, which becomes tedious as the product grows. We observed this first-hand in the early days of Heetch.
But what if, instead, this approach could be changed? What about letting the backend entirely drive the state and let the mobile clients be stateless?
By doing so, the software that takes decisions about the state is running where ownership is clearly defined, the backend. Mobile applications then simply become a receiver that is monitoring the backend state. This gives control back to where you can actually patch the code within minutes.
Eventually, some user initiated actions (such as requesting a ride) can still impact this state. But, with such approach, the state changes will go through the backend first and then propagate back to the client. For this reason, the article will only focus on the receiving part.
Unfortunately, acting as a mere receiver has its tradeoffs. Ride sharing users use their phones throughout cities and suburbs, meaning they may operate under poor network conditions. This should not affect the reliability of the client (and by extension how Heetch is perceived).
Those issues must be dealt with and that is where ReactiveX and a proper design helps. But before diving in, let’s have a look at what our state is composed of so that we can have a clear example to work with.
State is the information that allows us to know what screen to display to our users and how to populate these screens. Here are the most commonly screens of our iOS app:
To know what to show the user, two different values need to be synchronized with the backend:
driver_mode: true|false // a boolean which tells if the user is considered to be a driver or not. This is a peculiarity of Heetch’s business model, where users can be drivers and drivers can be users.ride status: pending|survey|approaching|riding|… // an enumeration, representing the user’s current ride status.
Finding which screen needs to be displayed can easily be done by looking at combinations of those two values. For the sake of simplicity, we’ll just stick to a few ones through this article examples.
This is why state is so important for any application. If such a table cannot be made without effort, it means that state is probably scattered into UI components.
Synchronizing with the backend
The most simple way to get information from the backend is to fetch it using HTTP.
This is a pull based interface, where the client decides when it needs to request a value update. We can take the app start-up as a basic example: the client knows nothing about the current state, so all it can do is to fetch a value from the backend.
The state can evolve independently of user initiated actions. For example when a passenger cancels a ride, a change in the driver’s state will automatically occur.
In this situation, we need to be able to push information from the backend to the mobile apps.
This a push based interface and it will be used by the backend every time the state changes.
So the mobile application can use two separate streams to receive updates:
So far, by explicitly considering the state, the application is already easier to reason about. But this doesn’t mean much if the receiver isn’t able to “receive” properly in the first place due to some connectivity problems.
A common network issue
Communicating through the mobile network has its flaws. Which is especially true with mobile applications that are expected to be used in the streets rather than at home, within WiFi range.
A problematic issue the application faced in its very early days was that a race condition occurred between the two communication channels, HTTP and pushed messages:
What is happening here?
- client asks backend for its current driver mode value. ➀
- backend, upon reception of the request, notices the driver mode is true and prepares to answer ➁
- while the answer is being sent back to the user, an external event changes the driver mode to false ➂ (an example of this would be auto-deactivation of driver mode after an extended inactivity period)
- using the push-channel, the client is notified that his driver mode has changed to false ➃
- the HTTP response finally comes back to the client ➄, but after the pushed message ➃. That’s the race condition.
“Driver” screens are now shown to the user, because the latest information received ➄ said driver mode was true, while in reality the client is in passenger mode (because driver mode is false with ➂ and ➃).
Dealing with outdated state values
A naive solution to this problem is to add versioning to each state components. There are much better approaches but that’s outside the scope of this article.
So, instead of driver mode being a mere boolean, it can be a structure that holds both the boolean and an integer representing its version. The backend is responsible for incrementing it every time it changes its driver mode value.
Therefore, the local driver mode can be compared to the just received driver mode. If the received one has a version number lower than the local one, it can be ignored and the race condition had been solved.
Now that we’ve defined our example, it’s time to dive into the implementation.
Going in with ReactiveX
ReactiveX is a library for composing asynchronous and event-based programs by using observable sequences.
Reactive programming has seen a lot of traction in the front side development world for the last years.
Developed at first by Erik Meijer at Microsoft, ReactiveX draws from functional paradigm. It helps making state evolution explicit, by representing it as a stream of values. Those are referred to as Observables.
What is an Observable
An observable is an object you subscribe to using an observer. From there, the observer will receive three types of events:
- Next an element in the stream of values.
- Error attached with an error, implies end of emission. Can only be emitted once per subscription.
- Completed similar to Error, will only appear once and implies end of emission.
Operators can be applied to transform an observable or combine multiple ones. Let’s take the scan operator: it applies a transformation on emitted values using an accumulator. This makes it behave exactly like reduce but emits values on each step rather than accumulating into a final one.
Applying it to a ride-sharing application
In a ride-sharing application, ride statuses are being observed. A user or a driver has a “box” that contains either a ride, a ride request or just nothing. Observing it means that the application continuously receives photos of that box and react to them accordingly.
Our objective here is to encapsulate a mix of HTTP responses and push messages into a single stream of state values, easily consumable by the UI layer of our application.
Requirements are the following:
- always has a seed value on startup
- as neither driver mode or ride status are cached, these needs to be fetched to present the right screens to the application user
- can be updated through asynchronous push messages
- allows the backend to remotely act on the state, in case of events not generated by the application user
- outdated values are discarded
- unordered updates should not affect the state
Observables and HTTP requests
As the pulling interface is implemented through HTTP requests, the application must decide when to fire those. In ReactiveX vocabulary this referred to as a cold observable.
It’s called like this because it is inactive until subscription. Therefore, every subscription will trigger associated side-effects, in our case a HTTP request. But more on that later.
Now those payloads needs to be parsed:
Now, the pull interface had just been defined, let’s have a look at the push one.
Observables and push messages
Whatever happens, asynchronous updates, in this case, push messages are received. This is independent from subscription, this is referred to as a hot observable. It called like so because it’s “always ready”.
A word about cold and hot observables: they are not different at the type level, they are both encapsulated within the same class, Observable. They only differ in how they react to subscription, (i.e. how they are implemented).
A hot observable will not be affected by subscription and observers will only witness events as they occurs over time.
A cold observable on the other hand will start generating its values after a subscription occurs. Different observers in this case will see a similar chain of events, independently of the time when they decided to subscribe.
Let’s transform those messages into driver modes:
Combining those values into the final, usable state
Merge is used to create a single stream out of multiple ones then the Scan operator is used to compare versions, picking the most recent ones for the resulting stream:
For the sake of completeness, here is a Swift class that wraps all the behavior defined previously. It will be used throughout the communication layer and serve as a building block for the observable used by the UI layer.
Now let’s create some instances of RemoteResource, one for each value affecting application state:
The arguments of the constructor, push and pull, echo the design choices discussed earlier; design choices which consists in interacting with the backend through push-based and pull-based interfaces.
Preparing the observable for UI layer
Now that a way to represent state updates exists, with a dedicated HTTP endpoint and push messages, let’s combine them into a single state that can be streamed and be consumed by the UI Layer of the application.
combineLatest will emit a new value every time one of its source observables emits a next event. In the following diagram, when driverMode goes from false,v8 to true,v9 the previous rideStatus is carried over if none was emitted, so the resulting observable always has both values.
Eliminating superfluous HTTP requests
As the pull based observable is a cold observable, it means HTTP requests are fired on subscription. This is fine until different components of the application want to subscribe, which leads to making more requests than necessary.
Luckily, ReactiveX gives us an operator to prevent this without having to deal with it directly in the implementation: shareReplay.
It will make sures of two things:
- when subscription counts goes from 1 to 0, it will unsubscribe from the source observable, meaning it will subscribe again when going from 0 to 1, which will effectively fire the HTTP request from the pull based observable
- but on further subscriptions, e.g. 1 to 2, the previous value will be emitted again, without executing the HTTP request again, effectively preventing overhead
To sum up, pull-based and push-based updates have been combined into a single observable, discarding outdated values received in the wrong order (due to poor network). And all the while, ReactiveX and its operators prevent complexity to creep in.
What about reliability?
First, by dealing with state explicitly, the codebase is much easier to debug. It allows us to get a much clearer understanding of its behavior, as we can deal with direct representation of the state (and thus our business), rather than diving head first into the UI layer.
Next, handling state with ReactiveX, by allowing to think in terms of observables is quite game-changing. Rather than designing around a state that changes over time, it enables to view state as a signal, allowing to distinguish dependencies between those signals (or observables in proper jargon).
By using such an approach, we’ve seen a great improvement in terms of reliability of our service, and our code is definitively simpler to reason about, on both Android and iOS since ReactiveX is available in many languages.