This is Part 3 of 3 in a series on Offline Connectivity.
Read Part 1: No Connection? Big Problem
Read Part 2: Offline functionality is a no brainer (and how Twitter dropped the ball)
Most developers hone their coding skills by learning how to build web apps. On the web, you’re always connected: you can submit data instantly and check out recent changes with a simple refresh. And if something goes horribly wrong, your user will be able to detect it right away.
Not so with mobile.
Despite the fact that it’s impossible for mobile apps to be constantly connected to the Internet, many developers design them as if they will be. Even when building in valuable offline support would be easy, dev teams often don’t even think about it.
Here are eight tips for building a smart mobile app:
1. Assume your app will usually be offline
For your app to be the best it can be, the wise thing to do is is build it with the assumption that it will be offline most of the time.
Think about it: just because your phone thinks its online, does that mean your request will succeed? What if your device is connected to WiFi, but the router isn’t? What if your server is down? What if you’re in that strange netherworld where the phone thinks its online, but you just got in the elevator (ie “softline”)? Etc.
Lots of things can go wrong. It’s better to think like you’re always offline, with an occasional chance to talk to your server. Stop talking about offline “mode.”
2. Work smart
Does your entire app need to work offline? Probably not. Make a list of all features and estimate a) how important that feature is, and b) how hard it’ll be to support it offline. To maximize your efficiency, pick the good stuff and skip the rest.
3. Do the bare minimum
If you want your app to keep user adds/edits (and you should!), but you don’t want to implement a full db table sync, you can simply push the add/edit command into the queue and let it process later when there’s a connection.
Basically, the json you were going to send to the server gets stored in the command before it can be sent. The benefit is that the user won’t lose data entered offline, and you’ll keep your logic to a minimum (full 2-way sync is significantly harder than 1-way, in either direction). It’s a bit “ugly,” but will keep the users happy. Its also a lot cheaper to implement.
4. Choose an offline sync method
There are two basic methods of syncing: state-based (SB), and command-based (CB). (For a really detailed explanation, check out our post on Data Synchronization.)
SB and CB are conceptually the same, except that SB requires you to recreate the list of commands yourself from the “state” of the data, while with CB, you explicitly record the commands as you go. CB also lets you do stuff that isn’t in the DB, like upload images.
SB queries the database to see what needs to be synced. This generally manifests as a “dirty” flag on the table in the form of an “updated” timestamp. The concept is simple, but the process can become a huge pain as your DB gets more complex. Especially with foreign key relations.
CB, on the other hand, has these operations explicitly added to it from your app. If you save an account entry, for example, the command to send it to the server is recorded in the CB queue. This means they’ll run the moment your app detects a friendly network condition. CB’s complexity grows more linearly with DB complexity than SB. (Touchlab wrote a CB command queue called Superbus —I recommend you try it! There’s a simplified v2 in the works.)
5. Understand the difference between temporary and permanent issues
Unlike most web app errors, mobile “headless syncing” errors aren’t immediately apparent to the user, which means they might not get fixed.
This means it’s critical to understand the difference between temporary and permanent issues. Temporary issues usually spring from having no internet connection, or a down server. (In Android, you also *maybe* have to worry about the SD card being removed.)
Pretty much everything else is a permanent issue, and your app needs to be equipped to deal with them. At a minimum, build in messages that will tell your user what’s up when something goes wrong. You can also give them a chance to resubmit. Whatever you do, don’t just “drop” the command. Nothing gets your app a 1 star review faster than losing your user’s precious data!
I’ve noticed that new developers tend to lean towards assuming an issue is temporary, but this is dangerous. (Read the Superbus docs.) If you’re not careful, you can wedge your app permanently, as the queue will try your command, “fail” temporarily, and shut down to wait for more favorable conditions — which will never come.
That, of course, assumes you’re using our queue process, or something similar. Even if you roll your own, though, think about it. You have to be VERY careful with error handling. What happens when there’s an error? Do you skip the update? Can you? Was it really a problem, or was the server down?
One example from Touchlab: we implemented an app with the first version of the queue, interpreting all IOException instances as network issues. This, of course, was wrong. There ended up being an image upload command that was trying to send an image that had been deleted.
(Whoops. Life would be much easier if there was ShittyNetworkException, but there isn’t.)
Additionally, some server APIs use http codes well, but many will return 200 regardless of what happened and push actual error conditions into custom fields. This makes identifying errors more difficult — you may “finish” the command when you really had a data problem.
6. Don’t wedge your app
This applies directly to people using the Superbus queue, but in theory it goes for everybody.
You need to discern what exceptions are temporary in nature from permanent, and you need to keep temporary sync events in a “pending” state. In CB, that means leaving them in the queue. In SB, that means leaving your “dirty” flag dirty.
Also, unless you have a really loose schema, you need to process things is some form of temporal order, or everybody will get upset (either your users, because something from five days ago just came in, or your data, because of foreign keys).
The big danger here is wedging your app. Permanently. If you keep thinking an exception is temporary, when its really something that’ll never go away, you’ll leave the app in a never ending state of incomplete sync. Nothing that comes after the poison event will ever process.
In Superbus, there’s a CommandPurgePolicy that you can implement to force something out of the queue. If something has been processing for a while, you can dump it. Sounds nice, but this is MUCH harder to do well than you think. How long before you dump it? Under what conditions? Also, nothing else processes while this is being decided.
The default policy in Superbus is TransientMethuselahCommandPurgePolicy — because I like funny class names, and I want to make sure you understand that if you don’t do this properly, the command will live forever.
7. Opt for multiple ID values
If you’re a sane person, you use numeric IDs on your tables. When I first started doing sync on mobile, I tried to keep one “id.” Like a concerned parent, all I can say is: don’t do this. You’ll regret it later. For two-way sync entities, have a “localId” and a “serverId.” It’s just a lot easier to manage. (Say you create a value, then go back in to edit it while your sync is processing. You’ll either crash or create a duplicate entity.)
8. Understand data storage vs Id storage
When you add/edit an entity, and create a command to push the data, you can either keep a reference to its local id, or create a json dump. There are arguments for each — and to be totally honest, I flip flop periodically.
Keeping the actual data means you can send things in their actual order. If you only push half of the commands before losing your connection, a remote client should have a reasonably coherent version of the “truth” because the remote data was added in order. You can also do update “slices,” which only update certain fields, rather than whole entities, which is very valuable if multiple users are making edits.
Id references, on the other hand, are generally easier to implement if it’s not super critical to have your updates process in order. You can “collapse” them, so multiple edits are contained in one “command.” And if you upgrade your db schema, the data in the app queue isn’t “stale.”
However — and this is critically important — if you’re using id storage and your tables reference each other, give your “create” commands a higher priority. This makes for a bit of a temporal issue, as it’ll run all of your creates before your updates — but if you run an update and set a foreign reference to an entity that doesn’t exist yet, you’ll be having a bad day. You can mostly avoid this problem by slotting all of your adds first, although there are times when that won’t work right in all cases. Think defensively about how things will process in the real world.
****Coming Soon, Touchlab’s First eBook!!
Looking for a great team of focused Android experts to bring your idea to life? We do one thing: make Android apps. Keeping that focus means we can do things the right way. Let’s chat about your project, get in touch.
Are you one of the best Android developers out there who likes solving problems, working on a small, agile team and building only the best solutions? Maybe we should talk, get in touch.