Build offline-first mobile apps without pain

Designing for intermittent connections is easier when the platform handles serialization, sync, and conflict resolution

Build offline-first mobile apps without pain
Thinkstock

It is a truth universally acknowledged that a user in possession of a smartphone must be in want of a better connection. Despite billions of dollars of infrastructure investment and relentless technological innovation, it doesn’t take much more than a short drive to notice an essential reality of the connected era: You can’t assume a network connection will be available every time you want it. As mobile developers, it’s a truth that’s convenient to ignore.

Offline states in apps can be confounding to handle, but the problem begins with a basic and incorrect assumption—that offline is, by default, an error state. That made sense when we built apps for desktop computers with dedicated ethernet uplinks. It doesn’t make sense when the closing of an elevator’s doors renders an app completely useless or when it’s reasonable to expect that your application will be used in places that lack a reliable cellular infrastructure.

We can’t blanket the world in coverage, so we have to offer an alternative. We have to think offline-first. We have to design apps to be useful offline. We have to build apps that take full advantage of the internet when it is available but understand that internet access is always temporary. We have to make smart design decisions involving offline states and make those offline states intelligible to users.

Plenty of work is being done to define the offline-first future. Realm, the company where I work, has been building a real-time platform for offline-first mobile apps for some time. Our mobile database and the Realm Mobile Platform make it easy to create intelligent, offline-first apps on nearly any mobile device. The folks at A List Apart have contributed enormously to the offline-first literature, especially for web apps. And the developer communities of the major mobile ecosystems have spent many hours offering up impressive open source solutions of their own.

What follows is a brief introduction to how you can build an offline-first mobile app. I’ll draw on some simple Swift sample code toward the end to show what a minimal offline-first app looks like, but the principles and problems offered here are relevant to anyone working in mobile app development.

Design for offline-first

Before you build the offline-first app you’ve always wanted, we have to revisit the design solutions that made sense for desktops with a very high likelihood of being online. If your app can handle offline and online states, we have questions to answer about what it can do and how we show the user what’s possible.

Define what is possible offline

Let’s take Twitter as an example. If you’re offline and you post a tweet, an offline-first Twitter client could take two paths. It could queue the tweet until it regains connectivity. Or it could refuse to let you tweet—even if it lets you queue other actions such as faves, as Tweetbot does.

Why would Tweetbot prevent you from tweeting offline? Maybe because by the time you get back online, your tweets might not be relevant anymore. Solving that problem would involve making a new UI for a list of tweets you haven’t yet posted, but which you might need to edit or delete before they go online. If you hearted a tweet, on the other hand, it’s unlikely you would undo it if confronted with more information—and a lot less problematic to simply indicate that it’s queued for posting.

You can’t make an offline app do everything an online app can, but you can make it useful.

Design away conflicts

Regardless of the strategy you use on the back end to reconcile changes, your app will face a point where you have two conflicting pieces of data. Maybe it’s because the server crashed or because you and another person made offline changes and now want to sync them. Anything could happen!

Thus, anticipate conflicts and strive to resolve them in a predictable way. Offer choices. And try to avoid conflicts in the first place.

Being predictable means that your users know what could happen. If a conflict can arise when users edit in two places at once when they’re offline, then they should be alerted to that when they’re offline.

Offering choices means not simply accepting the last write or concatenating changes or deleting the oldest copy. It means letting the user decide what’s appropriate.

Finally, the best solution is to never let conflicts develop in the first place. Maybe that means building your app in a way so that new and weird data from many sources doesn’t lead to a conflict, and instead displays exactly as you’d want it to. That might be hard to do in a writing app that goes online and offline, but a shared drawing app can be architected to add new paths to the drawing whenever they get synced.

Be explicit

It’s one thing to define what the user can do offline. A whole other problem involves making those decisions intelligible to your users. Failure to successfully communicate the state of your data and connectivity, or the availability of given features, is tantamount to failure in having built an offline-first app in the first place.

A shared note-taking app illustrates the problem. If you go offline but expect collaborators to continue editing in the app in your absence, it’s not enough to simply allow a user to continue to type until they’re happy. When they reconnect, they’ll be surprised by conflicts that have developed.

Instead, help your user make the right decision. If you see that your server connection has been severed because your app’s top bar changes color, you know what could be coming: merge conflicts! That might be fine most of the time, and your app’s UI can help remedy unexpected conflicts when you come back online. But if you lose connectivity when multiple people are editing your app, wouldn’t it be helpful to know that the risk of conflicts is much greater? “You lost connection, but others were editing. Continuing to edit could cause conflicts.” The user can continue but knows the risk.

It’s easy to write endlessly about design problems and solutions, but before we get too far from the tools we’ll have to use, it might be helpful to see what it’s like to build an offline-first mobile app.

Build an offline-first app with Realm

The architecture of a basic offline-first app isn’t fancy. You need a way to persist data in the app (using an on-device database), a protocol to communicate with a server (including serialization and deserialization code if necessary), and the server where the synced data will live so that it can be distributed to whoever has permission.

First, I’ll walk you through how to get started with the Realm Mobile Database inside an iOS app (though the code wouldn’t look much different in an Android app). Then I’ll present a strategy for serializing and deserializing code that you get from a server and store in your local Realm database. Finally, I’ll show you how to get it all working together in a collaborative to-do list app that syncs in real time.

Realm Mobile Database

It’s easy to get started with Realm. You install the Realm Mobile Database, then define your schema by making classes. Because Realm is an object database, it’s really as simple as making classes, instantiating some objects, and passing those objects into a write block to persist them to disk. No serialization or ORM is required, plus it’s faster than Apple’s Core Data.

Here’s the core of our model and the most basic possible to-do list app (which you’d have to recompile every time you wanted to make a new task):

import RealmSwift
class Task: Object {
   dynamic var name =""
}

class TaskList: Object {
   let tasks = List<Task>()
}

let myTask = Task()
myTask.task ="Finish writing offline-first article for InfoWorld"
let myTaskList = TaskList()
myTaskList.tasks.append(myTask)
let realm = Realm()
try! realm.write{
            realm.add([myTask, myTaskList])
}

From there, it doesn’t take much to build out a more fully functional app around a TableViewController:

import UIKit
import RealmSwift
class TaskListTableViewController: UITableViewController {
   var realm = try! Realm()
   var taskList = TaskList()
   override func viewDidLoad() {
       super.viewDidLoad()
       print(Realm.Configuration.defaultConfiguration.fileURL!)
       // Here, you could replace self.taskList with a previously saved TaskList object
       try! realm.write {
           realm.add(self.taskList)
       }
       // add navbar +

       navigationItem.setRightBarButton(UIBarButtonItem.init(barButtonSystemItem: UIBarButtonSystemItem.add, target: self, action: #selector(displayTaskAlert)), animated: false)
   }

   func displayTaskAlert() {
       // make and display an alert that’ll take a name and make a task.
       let alert = UIAlertController(title: “Make a task”, message: “What do you want to call it?”, preferredStyle: UIAlertControllerStyle.alert)
       alert.addTextField(configurationHandler: nil)
       alert.addAction(UIAlertAction(title: “Cancel”, style: UIAlertActionStyle.cancel, handler: nil))
       alert.addAction(UIAlertAction(title: “Create Task”, style: UIAlertActionStyle.default, handler: { (action) in
           let task = Task()
           task.name = (alert.textFields?[0].text)!
           try! self.realm.write {
               self.realm.add(task)
               self.taskList.tasks.append(task)
           }
           self.tableView.reloadData()
       }))
       self.present(alert, animated: true, completion: nil)
   }
   override func didReceiveMemoryWarning() {
       super.didReceiveMemoryWarning()
   }
   override func numberOfSections(in tableView: UITableView) -> Int {
       return 1
   }
   override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
       return self.taskList.tasks.count
   }
   override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
       let cell = tableView.dequeueReusableCell(withIdentifier: “reuseIdentifier”, for: indexPath)
       cell.textLabel?.text = self.taskList.tasks[indexPath.row].name
       return cell
   }
}

That’s all it takes to get started! You can get much cleverer with Realm’s collection and object notifications, so you can intelligently reload the tableView when an object is added or deleted, but for now we have persistence—the foundation of an offline-first app.

Serialization and deserialization

An offline-first app isn’t much of an offline-first app unless it can also go online, and getting data to and from Realm can be a bit tricky.

First of all, matching your client schema as closely to your server’s schema is crucial. Given how most back-end databases work, that will likely involve adding a primary key field to your Realm class, as Realm objects don’t by default have a primary key.

Once you have your schema matched up well, you need a way to deserialize data coming from the server into Realm and to serialize data into JSON to send back to the server. The easiest method to do it is to pick your favorite model mapping library and let it do the heavy lifting. Swift has Argo, Decodable, ObjectMapper, and Mapper. Now when you get a response from your server, you simply let the model mapper decode it into a native RealmObject.

Still, it’s not that great a solution. You still have to write a ton of networking code to get JSON to and from your server safely in the first place, and your model mapper code is going to need rewriting and debugging anytime your schema changes. There ought to be a better way, and we think the Realm Mobile Platform is exactly that.

Working with the Realm Mobile Platform

The Realm Mobile Platform (RMP) gives you real-time sync so that you can focus on building a mobile app, not fighting to get the server and app to talk. You simply take your Realm model above, add RMP’s user authentication, and let RMP take care of synchronizing data between the server and your app’s realms. Then you simply continue to work with native Swift objects.

To get started, download and install the Realm Mobile Platform MacOS bundle, which lets you get a Realm Object Server instance going on your Mac really quickly. Then we’ll add a few items to our to-do list app to make it connect to the Realm Object Server.

Once you’ve finished following the installation instructions above, you should have the server running and an admin user at http://127.0.0.1:9080. Remember those credentials, and we’ll return to our Swift code.

Before we write any more code, we need to make two tiny changes to the project. First, we need to go to our app’s target editor in Xcode, and in the Capabilities tab, enable the Keychain Sharing switch.

Then, we’ll need to allow non-TLS network requests. Go to the project’s Info.plist file and add the following inside the <dict> tags:

<key>NSAppTransportSecurity</key>
<dict>
   <key>NSAllowsArbitraryLoads</key>
   <true/>
</dict>

1 2 Page 1
Page 1 of 2