offline-lighthouse

(Photo by Jon Flobrant on Unsplash)

It’s a hard truth every software developer faces at some point: consistent Internet access is never guaranteed. WiFi is available everywhere nowadays, but service can be spotty or overloaded with connection requests (such as at large events or conferences). And, you can’t stop your users from accessing your app while their connection is poor or non-existent. So as a developer, what do you do? Embrace it. Tame any concerns about building offline experiences by learning the fundamentals of Offline First.

Offline First is the software engineering principle that assumes that apps can and will be used without a network connection at some point. This is especially important if you are designing for a mobile audience, who may go offline multiple times per day. Building Offline First apps increase the utility of your app and lead to better user satisfaction.

Offline First Mindset

Here are some of the things you can do to adopt an Offline First mindset.

Cache early, cache often

As the old saying goes, “the early bird caches the worm.” 😉

Bad jokes aside, cache your content early and often while connections are stable to save yourself headaches when network connections are more intermittent.

Caching content improves app performance since the now-local information is retrieved faster. Apps can be opened and closed while keeping their state that way as well. Check out some specific strategies for caching in the section on Service Workers below.

Identify the Non-Negotiable

On the flip side, some features are non-negotiable: you simply have to be connected to the internet to use them, such as location services.

That’s OK! Identify what features need an internet connection, then create messages or alerts informing those features’ needs to users. Users will appreciate it because you’ll take the guesswork out of understanding your app’s limits when offline.

Be Ready for Conflict

Conflicting information can be a big problem. It may not be enough to simply merge and override all changes except the last time a user was online. Users should have a clear understanding of what happens when conflict is unavoidable. Once they know their options, they can choose what works for them.

There are several solutions for handling conflicting information. It is up to you if you want the user to pick and choose which version to keep or to institute a “last write wins” rule. No matter what you choose, the app should handle this gracefully.

Know When to Apply Updates

Update too frequently and you may miss out on the benefits of being offline first. Update too slowly and you may create more conflicts than necessary and frustrate users.

Knowing what to update along with when to update is an important consideration as well. Uploading only the most important data to your servers saves on storage and the time needed to upload.

Expect the Unexpected

Users are unpredictable. While you should test for a wide variety of situations you cannot account for everything. Track what users actually do with continued testing and surveying when users are offline.

One personal example is my wife borrowing e-books from the library to read on her Kindle. Since borrowed books “expire” after a set rental period, she keeps her Kindle offline as long as possible to ensure that if she needs more time to finish a book, she can do so without losing access to it.

I do not know if the Kindle App’s developers anticipated people hanging onto books like that, but it is one example of people remaining offline as long as it takes to fit their use cases. The idea that everyone wants to constantly be online is one that should be tested against your own assumptions.

Those are the concepts you need to be aware of. To put these concepts to work you’ll need the right tools. Fortunately, there are a number of existing tools that make it easier for developers to program with Offline First in mind.

Tools

The Network Information API

The Network Information API is a critical tool for developers who want to ensure a smooth experience for users. Developers can use this API to create paths that open an app in online or offline mode from the moment a user opens the app. This cuts down on the errors a user sees when an app tries to find a connection but fails to open at all.

The Network Information API even distinguishes between WiFi and Cellular networks, allowing for fine-grained control over how content is served. The API also detects changes that allow apps to continue to function as a user moves from one network connection to another (like going in and out of subway tunnels).

As an example, when a user is on a slower connection, they get an alert that performance may be degraded:

if (navigator.connection.effectiveType != '4g') {
  console.log('slow connection!');
  // show modal dialog warning user that video will be loaded at lower resolution
}
else {
  console.log('ready to go!');
  // immediate load video file
}

Another option is Capacitor’s Network API. It extends the Network Information API to provide even more useful features for web and mobile apps, such as monitoring the network for status changes, which your app can then react to.

import { Plugins } from '@capacitor/core';

const { Network } = Plugins;

let handler = Network.addListener('networkStatusChange', (status) => {
  console.log("Network status changed", status);
});

// Get the current network status
let status = await Network.getStatus();

// Example output:
{
  "connected": true,
  "connectionType": "wifi"
}

Monitoring the network with these two APIs is a crucial early step in making your offline experiences as smooth as possible.

Service Workers

Service Workers let you control network requests, cache those requests, and then allow users to access cached content when offline.

With the Cache API, it is simple to add resources with the built-in add method. For example:

const resource = caches.add('/styles.css');

or you can use addAll to add multiple files, which uses a promise to ensure all the resources are cached and rejects the request if one is not loaded. For example:

const resources = caches.addAll('/styles.css', '/fonts.woff', 'otherdata/'); 

An event listener can fire after the app is installed which creates a cache and loads its contents ready to be used once installation is complete:

self.addEventListener('install', function(event) {
  event.waitUntil(caches.open('static-files')
    .then(function(cache) {
       return caches.addAll([
         '/css/styles.css',
         '/static/images/',
         '/fonts/fonts.woff'
       ]);
    })
  );
 }
);

From there you can create additional functions that check for updates to the cached contents, delete out of date files, or add additional files as needed.

Offline Storage

How you save data locally is another important consideration. There are several solutions: localStorage, IndexedDB, and Ionic Offline Storage.

localStorage

localStorage contains built-in methods to create and store key -> value pairs. These are persisted offline and when a browser is closed. You can declare and retrieve data using setItem and getItem. The following code sets a ‘name’ value and returns it when called.

var offlineStore = window.localStorage;
offlineStore.setItem('name', 'John');
var name = localStorage.getItem('name');

Use the clear method to delete data.

 localStorage.clear('name');

IndexedDB

localStorage is great for storing strings, but what if you want to store more complex data like images or blobs? That’s where IndexedDB comes in. IndexedDB lets you create a new local database connection and map objects to the schema you provide. It requires more code than localStorage but you can take advantage of more features.

When creating an IndexedDB you create a “store” for your data and IndexedDB lets you go from there. Here’s an example of creating an IndexedDB of books and their authors:

// creates a new version of the database if needed
const dbName = "books_db";
let request = window.indexedDB.open(dbName, 1),db,tx,store, index;

request.onupgradeneeded = function() {
  let db = request.result,
  store = db.createObjectStore("booksStore", { keyPath: "isbn" }), 
  index = store.createIndex("title", "title");
} 

request.onsuccess = function() {
  // db where data goes
  db = request.result;

  // defines the transactions allowed
  tx = db.transaction("booksStore", "readwrite");

  // the store you create for the data that uses the 'isbn' key.
  store = tx.objectStore("booksStore");

  // an index you can use to search data
  index = store.index("title"); 

  // insert data
  store.put({isbn: 1234, title: "Moby Dick", author: "Herman Melville"});
  store.put({isbn: 4321, title: "Emma", author: "Jane Austen"}); 

  // retrieve data
  let book1 = store.get(1234);
  book1.onsuccess = function() {
    console.log(book1.result);
  } 

  // close the database connection once the transaction has completed
  tx.oncomplete = function() {
    db.close();
  };
};

You can learn more by reading the IndexedDB docs. There are other solutions like localForage which is a wrapper API for IndexedDB that aims to make it easier to work with.

Ionic’s Offline Storage solution

For more advanced, robust data storage needs, check out Ionic’s Offline Storage solution. It’s a cross-platform data storage system that works on iOS and Android and powered by SQLite (a SQL database engine). Since it provides a performance-optimized query engine and on-device data encryption (256-bit AES), it’s great for data-driven apps.

Since it’s based on the industry-standard SQL, it’s easy for developers to add to their project. For example, creating a new table then inserting initial data:

this.database.transaction((tx) => {
  tx.executeSql('CREATE TABLE IF NOT EXISTS software (name, company)');
  tx.executeSql('INSERT INTO software VALUES (?,?)', ['offline', "ionic"]);
  tx.executeSql('INSERT INTO software VALUES (?,?)', ['auth-connect', "ionic"]);
});

Querying data involves writing SELECT statements then looping through the data results:

this.database.transaction((tx) => {
  tx.executeSql("SELECT * from software", [], (tx, result) => {
    // Rows is an array of results. Use zero-based indexing to access
    // each element in the result set: item(0), item(1), etc. 
    for (let i = 0; i < result.rows.length; i++) {
      // { name: "offline-storage", company: "ionic", type: "native", version: "2.0" }
      console.log(result.rows.item(i));

      // ionic
      console.log(result.rows.item(i).company);
    }
  });
});

Learn more about building fast, data-driven apps here.

Availability Messages

It’s important to communicate with users when their offline experience changes. Your app should be proactive about letting users know when they are online or offline. Use network event listeners to detect changes in a user’s connection and send messages letting a user know that not all features may be available.

The Ionic Framework Toast Component is one tool that can help you set availability messages quickly and easily. Once your app detects that it is offline use a toast to notify the user:

// Display a toast message if the network connection goes offline
async function showOfflineToast() {  
  const toast = document.createElement('ion-toast');  

  // Display the message that the User is now offline. 
  toast.message = 'The latest chat messages will appear when you are back online.';

  // Display the toast message for 2 seconds
  toast.duration = 2000;
}

Start Building Offline Apps

Users will go offline at some point, due to intermittent network connectivity or their location (remote areas or thick-walled buildings for example). They may simply be concerned about limiting bandwidth usage to save on costs, preferring offline modes over the need for constant connection. Regardless of the reason, we must account for these scenarios.

If this seems challenging to get started with Offline First, don’t worry. Use the following checklist to ensure that you are on the right track when developing an Offline-First experience. Your users will thank you!

  1. Are you designing for an Offline-First experience?
  2. Are you checking for network connections using the Network API? What type of connections are you detecting?
  3. Have you configured a service worker to serve offline content?
  4. What features cannot work without an internet connection? How will you let users know what is or is not possible offline?
  5. What are your storage solutions for offline content? Will you rely on localStorage or a more robust system like IndexedDB?

What tips and tricks do you use when building offline apps? Let us know below.

Signup for the Ionic Newsletter to get the latest news and updates!