Welcome to the 21st century where 5G, hotspots and mobile internet is available everywhere. It’s one of the things considered to be simply there, so you could ask yourself:
Offline first apps - is this really needed?
That’s a perfect valid question to ask — but think about it for a second and you might find yourself in the below examples and will agree it make sense.
You are in a high speed train or in the subway on an early monday morning (I know you don’t want to be in this situation but…) and your public transport app is not showing the next connection because you don't have a signal.
You made it — finally the time has come for your well-deserved holiday but unfortunately you do not have a stable internet connection or it’s way too expensive. No way to quickly check the map to get to that pizza place you checked out before.
70.000 other people are with you in a stadium watching the game of the year. And with them at least 70.000 other phones in a relatively small spot. Goodbye, mobile network...
Driving along the country side, hiking in a forest or simply going for walk. These are situations that might get you into the zero network zone. I know quite a few spots around here (in good old Germany…) that are like black holes, no mobile signal will ever come out.
No connection? No way!
In the end it comes down to what your user expects — most of them would like to get their content without any interruption. Processes started should offer a function to finish them or content should allow changes — offline or online should not change this.
Offline-first, it’s about states
Think about application states, authenticated vs. none authenticated. Writing data or reading data are all application states. Why not adding a “missing connection” as a normal state instead of an error?
Maybe you go a step further and simply say:
Offline is my default state, a connection is a nice addition
Some concepts shown here are for Flutter but the general idea works also for websites, have a look at Google Docs, a perfect example of an offline capable website. Google Maps also has a nice offline function, allowing you to download an area and use it offline.
Let’s talk code
One short section before we look at code — let’s break our effort down into three implementation steps:
- Offline mode
Local cache / DB and general data storage
- Online mode
- On/Off UI pattern
Offline mode — local data
Let us divide data for your app into static and dynamic resources, everything that will not change (or doesn’t directly influence your app’s behavior) is static — like a picture.
Everything you process, the user types in is dynamic — like a comment for a post.
Static assets can/should be added during build time. All static assets should go to your
pubspec.yaml file, the complete process is described here. Keep in mind that a static asset can be a picture, music or a text file (like JSON).
Dynamic resources can be handled in different ways — as always in life, it depends on your needs. If it’s about simple files (like pictures or text) have a look at the cache manager package. Do not waste time with reinventing the wheel ;)
The above super simple example will download the image only if the local cache is empty or expired. Sure, if the image is required, it should be a static resource but I would like to keep it simple for the example.
More complex data can be handled by SQLite, actually the above mentioned cache manager uses SQLite to keep track of the cached files. I will not explain how to use SQLite, have a look at the related repository. Nevertheless, I would provide one little snippet that helped me a lot, handling database upgrades for local database can be tricky sometimes.
The above code is a DB provider I use for some Flutter apps. Let’s quickly cover the database upgrade part:
- Requesting the database will create it one time due to the static property that is shared between all instances (see the get database property)
- If it’s the first time (_database == null), initDB() will be called
- initDB() makes sure to generate the database and upgrade it if required
- Creation and upgrade makes use of batch execution, all statements are run in one request
- Upgrades are stored in a Map<int,Future<Null> Function(Database, Batch)> this might look complicated but actually it’s super simple — later more
- During update all elements of the map will be added to the batch (in the right order) and executed
The map mentioned in step 5 can be seen as a book — the first element is the page number (as int) and the second element the content of the page. If you would start reading a book somewhere in the middle you most likely have no clue what’s going on. If you continue to read page by page from where you left (in our case the currentVersion) the story will make sense.
Our database get’s updates from the currentVersion up to the latest available, all executed in the right order in a single batch operation.
UPDATE — Some fresh new ideas
Since I wrote this article I checked out a lot of other local storage solutions. Have a look at the linked article, moor is a nice package to assist you with offline first storage.
Flutter package at a glance — local storage with Moor
Welcome to the first episode of posts called “Flutter packages at a glance”! This time it’s all about offline storage…
Round 2: Online mode, ready ?
There are a billion options on how you can do this, as pointed out earlier this depends on your needs. I can just hand over some advises learned over the years, all our mobile apps have to be offline first, all of them have a sync build in.
- If you can, use an already build sync service — do net reinvent the wheel
- Please do not use integers for primary keys, instead use UUID offered by this super simple package, otherwise you might find yourself in the unlucky case of creating integers in given ranges for given clients. UUID’s are always unique¹
- Keep the exchanged data as small as possible, implement a change flag or last change date. Setup a data contract for all your data — this contract should describe the minimum required for all data (ID, last change), only send out data newer as the last sync
- Store timestamps in UTC to don’t end up in the timezone hell. Consider saving them as plain numeric values, this works for any kind of backend
- Keep the mobile side of the API as dumb as possible, if you have a bug in the app, a fix might take 2 days before it reaches your customer. Fixing a server side bug should be much faster
- Think about your API architecture twice, over/under fetching data is an issue of REST, a possible solution would be GraphQL
¹ The probability to find a duplicate within 103 trillion version-4 UUIDs is one in a billion. From Wikipedia
Last round: Offline first UX
Make sure your content offers interaction, even if you end up with “Sorry, no network, try later”, you still get further as hiding parts of your app.
Ensure that the workflows are the same, offline or online, there should be no difference.
In case you can’t handle a request, please provide a meaningful error message. Messages like the ones below are useless:
- An error occurred
- Internal error
- No response
Try something like: “Sorry, you seem to be offline. The current request can’t be completed.” If you have a certain function that will require a connection in any case, check the current connection state and disable the button if you are offline.
Pro mode — leave the button active but inform the user after he clicked that this can’t be done right now.
Think about your empty states, showing a big white rectangle because a list view is empty doesn’t help you. Include a nice image and a short text that get’s the user started/informed. Asking a user to confirm an action that will require a network connection can also be a helpful tool in case you need to perform a longer running operation.
Please, never ever clear your cache before checking the connection state. In the worst case you will end up with an empty app in the middle of nowhere.
Offline first, done right, get’s you the best of two worlds. Your users can use the app at anytime and have no fear of loosing their data. Thanks to caches and local data you should be able to deliver content quickly and reliable. Sure, it is an extra step of work for you but it might also be the extra feature you can offer but others not.
How about you? Did you ever made an offline-first app? I would love to see some examples or any experience in the comments :)
Thanks for reading.