Build Offline-First Mobile Apps by Caching API Results in Realm
Rate this code example
It's easy to make a call to a REST API endpoint from your mobile app, but what happens when you lose network connectivity? What if you want to slice and dice that data after you've received it? How many times will your app have to fetch the same data (consuming data bandwidth and battery capacity each time)? How will your users react to a sluggish app that's forever fetching data over the internet?
By caching the data from API calls in Realm, the data is always available to your app. This leads to higher availability, faster response times, and reduced network and battery consumption.
Many APIs throttle your request rate or charge per request. That can lead to issues as your user base grows. A more scalable approach is to have your backend Realm app fetch the data from the API and store it in Atlas. Realm Sync then makes that data available locally on every user's mobile device—without the need for any additional API calls.
You choose a base currency and a list of other currencies you want to convert between.
When opened for the first time, RCurrency uses a REST API to retrieve exchange rates, and stores the data in Realm. From that point on, the app uses the data that's stored in Realm. Even if you force-close the app and reopen it, it uses the local data.
If the stored rates are older than today, the app will fetch the latest rates from the API and replace the Realm data.
The app supports pull-to-refresh to fetch and store the latest exchange rates from the API.
You can alter the amount of any currency, and the amounts for all other currencies are instantly recalculated.
One of the reasons I picked this API is that it doesn't require you to register and then manage access keys/tokens. It's not rocket science to handle that complexity, but I wanted this app to focus on when to fetch data, and what to do once you receive it.
The app uses a single endpoint (where you can replace
EURwith the currencies you want to convert between):
The endpoint responds with a JSON document:
Note that the exchange rate for each currency is only updated once every 24 hours. That's fine for our app that's helping you decide whether you can afford that baseball cap when you're on vacation. If you're a currency day-trader, then you should look elsewhere.
JSON is the language of APIs. That's great news as most modern programming languages (including Swift) make it super easy to convert between JSON strings and native objects.
Note that only the fields annotated with
@Persistedwill be stored in Realm.
There are two other top-level classes used by the app.
UserSymbolsis used to store the user's chosen base currency and the list of currencies they'd like to see exchange rates for:
An instance of
UserSymbolsis stored in Realm so that the user gets the same list whenever they open the app.
This flowchart shows how the exchange rate for a single currency (represented by the
symbolstring) is managed when the
CurrencyRowContainerViewis used to render data for that currency:
Note that the actual behavior is a little more subtle than the diagram suggests. SwiftUI ties the Realm data to the UI. If stage #2 finds the data in Realm, then it will immediately get displayed in the view (stage #8). The code will then make the extra checks and refresh the Realm data if needed. If and when the Realm data is updated, SwiftUI will automatically refresh the UI to render it.
Let's look at each of those steps in turn.
The view then filters those results to find one for the requested
The view checks whether
rateis set or not (i.e., whether a matching object was found in Realm). If
rateis set, then it's passed to
CurrencyRowDataViewto render the details (step #8). If
nil, then a placeholder "Loading Data..."
TextViewis rendered, and
loadDatais called to fetch the data using the API (step #4-3):
The API URL is formed by inserting the base currency (
baseSymbol) and the target currency (
symbol) into a template string.
loadDatathen sends the request to the API endpoint and handles the response:
Rateobjects stored in Realm are displayed in our SwiftUI views. Any data changes that impact the UI must be done on the main thread. When the API endpoint sends back results, our code receives them in a callback thread, and so we must use
DispatchQueueto run our closure in the main thread so that we can add the resulting
Rateobject to Realm:
Notice how simple it is to convert the JSON response into a Realm
Rateobject and store it in our local realm!
true, waits a second to allow SwiftUI to react to the change, and then sets it back to
refreshNeededis passed to each instance of
refreshDatafetches the data in exactly the same way as
loadDatain step #4-3:
The difference is that in this case, there may already be a
Rateobject in Realm for this currency pair, and so the results are handled differently...
Rateobject for this currency pair had been found in Realm, then we reference it with
existingRateis then updated with the API results:
loadDatachecks that the existing Realm
Rateobject applies to today. If not, then it will refresh the data (stage 4-7):
By now, the code to fetch the data from the API should be familiar:
loadDatacopies the new
dateand exchange rate (
result) to the stored Realm
The number shown in this view is part of a
TextField, which the user can overwrite:
When the user overwrites the number, the
onChangefunction is called which recalculates
baseValue(the value of the base currency that the user wants to convert):
baseValuewas passed in as a binding, the new value percolates up the view hierarchy, and all of the currency values are updated. As the exchange rates are held in Realm, all of the currency values are recalculated without needing to use the API:
REST APIs let your mobile apps act on a vast variety of cloud data. The downside is that APIs can't help you when you don't have access to the internet. They can also make your app seem sluggish, and your users may get frustrated when they have to wait for data to be downloaded.
A common solution is to use Realm to cache data from the API so that it's always available and can be accessed locally in an instant.
This article has shown you a typical data lifecycle that you can reuse in your own apps. You've also seen how easy it is to store the JSON results from an API call in your Realm database:
We've focussed on using a read-only API. Things get complicated very quickly when your app starts modifying data through the API. What should your app do when your device is offline?
- Don't allow users to do anything that requires an update?
- Allow local updates and maintain a list of changes that you iterate through when back online?
- Will some changes you accept from the user have to be backed out once back online and you discover conflicting changes from other users?
The API you're using may throttle access or charge per request. You can create a backend MongoDB Realm app to fetch the data from the API just once, and then use Realm Sync to handle the fan-out to all instances of your mobile app.
Build a Command Line Tool with Swift and MongoDBBuild a Command Line Tool with Swift and MongoDB
May 16, 2022