Advanced PWA. What you want to know when building mobile app for business

Advanced PWA.

What I learned from building line of business mobile web apps

This article is intended to give you a broad technical overview of the main points you should consider when building a line of business PWA.

PWA is a great tech for building mobile app for front line workers or deployed employees

The first mover in PWA realm was the online press which realizes that people weren’t that eager to download an app from a store. Then e-commerce entered the field. Now, other business sectors are starting to see the advantage of building a PWA instead of developing a native app for 2 or 3 different platforms.

One case that appears to be great for PWA is mobile apps for front line workers and “not-in-front-of-a-desk” workers.
You can build a functional mobile app rapidly, with Web technologies only: HTML, CSS, Javascript.
And you do not need to go through an App Store registration process.

If you want to know more about why you should consider building a PWA, go read it.

You don’t need a Single Page Application.

You don’t need an over-bloated framework à la React, Angular, Vue…
  • Make it a Multi-Page Application. Why: because the service worker will give you a router for free!
    You can skip all the overhead of extra code for dealing with that plus maintaining a state.
    A service worker will let you set up any route without the need for a router.
  • You’ll keep the volume of code to send through the wire and parse on the client small. Which is good for the performance and the planet.
  • Line of business mobile apps are some garden variety of a CRUD app.
    • Your use case will be to create some objects, store them, edit them sometimes, and sync them to the server.
    • This means route of the like: someObject/new, someObject/<objectID>/edit, someObject/<objectID>/show, someObjects/list
    • Precache the pages with all the HTML markup you need, including forms.
    • Let the service worker do the heavy lifting of pre-rendering the list view (especially if you need to aggregate some data from other objectStore)
    • Usually, the delete part is done server side. When syncing your user’s app will get the objectID to remove.
  • You can also stream chunks of responses to build a complete page without needing the whole shebang from the server. I use it for internal docs.
    The header, nav, and footer are cached at install.
    When requesting the doc, the server will only respond with the HTML body. Which will be cached for subsequent use.
    The fetch response will then use a streamed response to compile together the different chunks and serve the page.

Your user will be offline. Often. Deal with it.

The numerous reasons for being offline: a lot of parts of land even in wealthy countries have very poor Internet connectivity/ mobile network. Your user may be in the train, the metro, aboard a plane, in the middle of the sea, may not have enough battery to turn off airplane mode. Just no 3G/4G.

  • Make your app offline by default.
    On the plus side, it helps with reasoning about objects in the same way you do in a server side app. Just don’t forget to add the sync layer :)
  • When calling fetch(), if you’re offline, the fetch call will fail immediately. You’ll want to deal with this separately from a failed response — response.ok == false.
try{
    const response = await fetch(url, init)
    return await response.json();
} catch (fetchError){
    // see https://github.com/github/fetch/issues/201#issuecomment-308213104 for fetch fail
    console.log('You're offline, baby!')
    throw new Error('transmission_failed')
}

Use workbox.

Period. Just do it.

The library gives you lots of well thoughts methods. Let’s you organize your logic for routing, caching, …

Managing synchronization with a remote server is not always easy.

Background sync event is not always available (iOS) nor reliable (sometimes the sync event will not fire, or use up all 3 attempts but not reach the server).

  • Implement a back-up method: a button, auto-sync on service worker startup, or don’t use background sync event at all :)
  • workbox backgroundSync plugin is not always what you need. Especially if you already have stored your object and photos.
  • Use a “state” property on the object that should be synced. “sync:pending”, “sync:failed”, “sync:done”
  • At first approximation, optimistic locking logic on distant server (version number) works pretty well.
  • You’ll run across cases :
    1. when uploading a photo, the distant server will time out. Not much to do, except increase the timeout value on server, and let the user know that there’s still some object to sync
    2. The distant server will save the synced object but the response will not reach the user’s mobile app.
      My way of dealing with that:
// pseudo code
try { 
    <DB handler>::insert('myDB', myData);
} catch (UniqueConstraintViolationException $e) {
   // Return some meaningful state so that you can update syncState on your user’s side. 
    retun ['errorContent' => [ 
      'myObject' => [
        'uuid' => $myObject->uuid(),
        'msg' => 'Duplicate myObject'
      ]]];
}

You’ll need indexedDB to store your data.

  • indexedDB is asynchronous :
    • It can be accessed on the service worker side. Great!
    • Use a wrapper to promisify it. iDB is good.
  • It’s perfect for a large amount of data like photos.

Think carefully when creating an object Store

  • How are you indexing it? What’s the primary key? What kind of query will you need to run against it?
  • indexedDB is a bastard for any mildly sophisticated operation —like searching with more than 1 criteria or sorting.
    If you need that, consider either storing a complete object or use Dexie.

iOS exceptionalism

Be aware of some quirk by iOS Safari.

  • One that took me quite a few hours to figure out: FormData is not available in service worker for Safari iOS (was in Dec 2019. Did not re-check lately). You’ll need it when uploading photos to the distant server.
  • iOS won’t let you store blob in indexedDB. Convert to ArrayBuffer. A method to promisify it :
function blobToArrayBuffer(blob)
{
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.addEventListener('loadend', (e) => {
            resolve(reader.result);
        });
    reader.addEventListener('error', reject);
    reader.readAsArrayBuffer(blob);
    });
}

Take extra good care of the UI

  • Your carefully designed buttons will be too small. Always.
    The standard is 40px minimum wide. Will be too small.
    Make it at least 60px. The best is to double it.
  • Front line workers —think construction workers, truckers, home meal deliveries, care workers, etc.— may not be very familiar with mobile devices. Try to make their life easier by simplifying the UI.
  • The main concern for all workers is the ease with which they can do the thing they are supposed to do on the mobile app. Remember, this is not their main job.
    Usually, this has to do with reporting, metadata producing for the main database,…
    This is the kind of task that may be interrupting or annoying. Be extra careful how the UI works.
    Test the accessibility. Go outside and observe some of your users using the app. You’ll uncover a metric tone of insights. And it’s fun. Really.
    You get to discover all sorts of crazy trades!
  • Any kind of user on the go is mainly concerned about: battery drainage.
    No amount of sophistication — think, iPhone bearer— will alleviate this concern.
    Be very considerate about your app calling home.
    I usually put a timestamp somewhere for the last time a sync operation did run, and don’t let a new one happen before at least 5 minutes have passed.
  • Instrument your app. The downside of mobile PWA is that you may not have any error hitting the server. One that I love but is expensive: TrackJS. Another one: Rollbar is free for 5000 hits/month, I’ve heard that Sentry is good too.

Other annoying bits

Mobile phone constructors have a nasty habit of not giving the user any way to manage how heavy should be the photo taken.
It’s not uncommon for a photo to be around 9-10Mo heavy.
Good luck uploading that beast on a flaky connection.
And resizing client-side is just… a computer-intensive task :/ See battery drainage above.

Now, go build some amazing PWAs!