Native mobile apps act as the interface between the user and the users’ data and require uninterrupted connectivity. Native mobile apps with offline capability stores both the mobile app’s software and its data locally on the mobile device. Offline mobile apps allow the user to run the app, regardless of connectivity.
How many times have you opened a mobile app, filled out a lengthy form that runs across multiple screens, and passed through validation errors only to see a message stating, “No Internet connection” when you finally click Submit? Most mobile applications are simply clients that send and receive data in real-time and become unusable without an active Internet connection.
As per the ICT Facts and Figures 2016, only 40.9 per cent of the population in developing countries has a mobile broadband subscription. And those who have it often complain about fluctuations, especially in crowded places or while travelling. Even in metro cities, the continuity of the network is not perfect. With unreliable networks, limited bandwidth and high latency, it becomes really challenging for engineering teams to come up with solutions that enable users to use an app, regardless of network connectivity. An approach where you make a network request, wait for the response and then display it, is quite unappealing. Ideally, you should have the data already, which you can display immediately, with a separate mechanism to update it.
Adding the offline capability to apps
Here are a couple of questions that must be answered before taking the plunge. What are the key features of the app that render it unusable due to no/flaky network conditions? How many users are affected due to this? How does it affect the key metrics of your business? Answers to these will help reveal the areas where there is an opportunity to provide offline support. There may not be a need to take the complete app offline. Features that cannot work offline can be redesigned, disabled or even be hidden.
In order for the app to function in unsteady and offline environments, the data that the application needs must be persisted locally on the device for subsequent lookups.
There could be different strategies for caching and refreshing the data. Choosing the right one is important and depends on the type of data your app presents. For example, time-sensitive data like stock updates should be as recent as possible, while images or lists of locations can be updated less frequently.
Network first: Try to retrieve data from the server first, and if that is not possible because there is no network, retrieve data from the local cache (if available). The next successful network hit should update the local copy. This strategy is useful when you always want to show the latest and most updated information.
Local first: Retrieve data from the cache and display it immediately. At the same time, if the network is available, fetch data from the server in the background and update the view as well as the cache. This approach is very useful as it leads to minimum in-app latency and the user gets to see the data instantly.
Local only: Cache data for a specific period and fetch it only from the cache without contacting the server at all. The cache can be later updated via notifications or periodic service polls. Do take into account the risk of battery drain caused by aggressive polling.
Prefetch: In some cases, it makes sense to download lightweight content and cache it pre-emptively so that it is instantly available when requested later.
Usually, a combination of these methods is required for different types of data at different places in the app. For example, product details can be cached for long, using either the ‘local first’ or the ‘local only’ strategy, whereas product price can be fetched in real-time by always using the ‘network first’ strategy. The longer the data can be cached, the better it is. Just be conscious of the size and security of data being stored. All sensitive data should be encrypted and stored safely.
Although the clients can decide on, choose and implement their own caching policy, the complete benefit of caching can be realised only when complemented with some sort of server-side caching support as well. By sending headers like cache-control with appropriate values, the server can indicate which data to cache and for how long. Networking libraries usually have inbuilt support for such headers and cache responses automatically. The backend can also be designed to reduce the amount of data transfer by comparing the version numbers (or Etags) of resources and sending a representation only if the resource has changed.
Data once cached can be displayed in offline mode easily, but what about adding/editing data? You may want to allow users to register, post pictures and update profiles, even when offline.
Whenever the application does not have a network connection, the newly entered or edited data operations can be collected in a local queue and processed later. A user should be able to use the app normally and always see the updated data, even if it is unprocessed. The queue also helps maintain the correct sequence of actions, which could be crucial at times. Imagine how confusing a conversation would be if the messages arrived in a random order.
A key point is to always inform users about queued operations. It would be better to create a separate UI that displays a list of unprocessed changes, giving users an option to cancel or retry them. As the operation is performed, notify users about the respective success or failure outcomes. This will help build more trust in the app among users— an assurance that it won’t lose any data regardless of the connectivity state.
When the signal is restored, the collected changes must be synchronised with data in the backend. Synchronisation can be triggered in various ways—as soon as the network is available, on launching the application, once in a day, after every six hours, etc. It’s better to go with bundled data transfers to help conserve battery resources.
While the user was working offline, it is possible that changes were made by other users to the same data. Or, the same user can change his data from multiple sources/devices, which can lead to conflicts that are usually detected at the level of a row in a database. A row is in conflict if it is changed at more than one of the sources between synchronisations. It could be a unique key collision, in which a row with the same unique key is inserted from different sources, or an update collision when the same row is updated from different places. It could also be a delete conflict when updating a row that has been deleted already.
Let’s look at an example.
- Clients A and B synchronise with the server and pull version v1 of a resource. The resource can be represented as a row in the database.
- A updates the resource and synchronises with the server. There is no conflict. The version is updated to v2.
- Later, B updates the same resource (at v1) and synchronises. A write conflict occurs because it is an update on the older version and the resource has been updated to v2 already.
Sometimes, conflicts can be detected on the client side. In the example above, if before pushing the changes, B pulls the resource again, it can compare the versions and see there is a conflict. B can then choose to discard its local changes or apply some other resolution, and inform the user accordingly.
Just like the version control systems, these conflicts must be resolved before the synchronisation process gets completed. There can be different conflict resolution strategies and choosing the right one is important to prevent data inconsistency problems.
Server wins: In this approach, changes received during synchronisation are discarded, leaving data in the database unchanged.
Client wins: The conflict is ignored and changes received are accepted, overwriting the value in the database. One should be very careful when using this approach as it might lead to data inaccuracy. Incorrectly overwriting a change of location (from Delhi to Mumbai, for instance) will result in showing completely irrelevant recommendations to the user.
Last update wins: Data with the most recent modified timestamp wins.
Let the user decide: Leave the decision to app users. The users can view the conflicting values and decide which one to keep.
Custom algorithm: A custom algorithm defined on the basis of business rules can be used to resolve conflicts. For example, there could be a business rule that says, “Pick the largest of the two values or take the union of the conflicting data sets and merge them.”
You may encounter situations where data is complex. Depending on your implementation, you can choose different approaches and build a robust conflict resolution system.
Synchronising offline data makes the application more responsive and optimised. You can build your own system for managing data transfers or use frameworks like Android’s Sync Adapter that automate it for you.
Lack of connectivity is not an error condition. Instead of showing an error message, the app should be tailored to provide a seamless experience, always. After all, we all know a happy customer is the best customer!