Offline support: “Try again, later”, no more.
I have the privilege of living in a country where 4G network and strong Wifi is almost everywhere — at home, at work, even at the basement of my friend’s apartment.
And somehow, I still manage to get this:
Maybe it’s because my Pixel phone is playing with me? Uh… no.
The internet connection is the most unstable thing that I have ever used. While in 95% of the time it works well, and I’m successfully able to stream my favorite music without any problem and always, always when I’m standing in an elevator and trying to send a message — it fails on me.
We, as developers live in an environment where a strong connection isn’t an issue, but the fact is — it is. Even more — just as in Murphy’s law — it will hurt your users exactly when they most need your App to work — and work fast.
Being an Android user and observing this “Try again” happening in many of my installed apps. I struggled to do something about it, at least in my app.
Our precious App
Finally, after starting my own startup KolGene, I got my chance. In startups, as most of you know, you start by building your first MVP and testing your assumptions. The process is so crucial and hard, with so many things that could go wrong, that losing even a single customer because of an offline issue is totally unacceptable.
Every customer we lose costs us a lot of money.
If there were leaving because the experience of using the application was bad — well, it’s not even an option.
Our app usage is pretty simple: A clinician creates a request for genetic tests on mobile app, the relevant laboratories receive a message, submit offers, clinician receives those offers and chooses the best offer based on their needs.
When we discussed various UX solutions, we decided on the following: No loading bar at all — even if it’s very beautiful.
The app should work smoothly without putting the user in a “waiting” state.
So basically what we want to achieve, is that internet connectivity shouldn’t matter — the app will always work.
And the result was:
When the user is in offline mode, he submits the request and … it’s submitted.
The only small reminder about being offline is small icon of “syncing” status at the top right corner. Once he comes online, the app will post his request to the server regardless if it is in background or foreground.
Same goes for every other network request-except for registration and sign In.
So how did we do it?
First we started by completely separating our view, logic and persistence model. As Yigit Boyar says:
Act locally, sync globally
It means that your model should be persistent and will be updated from the outside world. The data from model should propagate asynchronously using callbacks/events to the presenter and later to the view. Remember - the view is dumb and only reflects what we have in our model. No loading dialogs. Nothing. The view reacts to user and passes the interaction result through the presenter to the model and later, receives the next state to show.
For local storage we use SQLite. On top of it we decided to wrap it in a Content Provider because of its ContentObserver capability for events.
ContentProvider is a nice abstraction for Data access and manipulation.
Why not RxJava? Well, it’s completely different topic. In short — for a startup, when you need to move as fast as possible and the project changes hands every couple months — we decided to leave it as simple as possible.
Besides, I love ContentProvider, and there are a lot of additional capabilities: auto-initialization , running in a separate process and a custom search interface.
For background sync jobs, we choose to use GCMNetworkManager. If you’re not familiar with it — it’s enables scheduling tasks/periodic tasks to be executed when certain specific conditions met, like internet connection for example and it lives really well with Doze mode.
So the architecture looks like this:
The flow: Create Order and Sync it.
Step 1: Presenter creates new order and sends it for insert via ContentResolver to Content Provider.
Step 2: Content Provider inserts into the local database and notifies all observables that there is a new order created with status “pending”.
Step 3: Our background service that was registered to observe changes in Order table by URI — gets notified and starts the specific service for this task.
Step 4: The service obtains the data from DB and tries to sync it over the network. When network request succeeds— the order is updated with status “synced” via ContentResolver.
Step 5: If the request fails, it will schedule GCMNetworkManager one-time task with
.setRequiredNetwork(Task.NETWORK_STATE_CONNECTED) and order id.
When the criteria is met (the device is connected to the internet and no doze mode), GCMNetworkManager calls onRunTask() and the app will try to sync our order once again. If it fails again — it will reschedule it.
Once the order is synced, a background service or GCMNetworkManager will update the status of local order via ContentResolver with the status “synced”.
Of course this type of architecture is not bulletproof. It requires that you handle all possible edge cases, for example what if you schedule a task to update an existing order on the server, but it was canceled/changed by an admin on the server side? What if they change the same property? What should happen if the first update was made by the user, or by an admin?
Some of them we solved immediately. Some of them we left in production (they are really rare). The different approaches that we took to solve these isseus, I will share in one of my next articles.
And there is definitely some room for improvements in our codebase as Fred says:
Even the best planning is not so omniscient as to get it right the first time.
— Fred Brooks
But we continue to struggle to improve it and make usage of our KolGene App delightful and full of joy to the users.