Building a PWA for Android and iOS: Tutorial and live example ?

Building a PWA web app tutorial and live example

Mobile web apps (known as Progressive Web Apps or PWA) can be a cheaper and totally viable replacement to native apps in many domains. As it’s been proved elsewhere, native apps require a costly launch and maintenance cycle. Google is betting strong on PWAs by implementing Service Workers and although iOS is not reacting that quickly on this trend, there are so many things you can do for both environments already.

The goal was to deliver an application, in our case a web app to manage our timesheet solution, that works for all main browsers installed on top of Android and iOS. The application is currently used on multiple browser/devices, including but not limited to Chrome, Firefox and Opera running on iPhones, iPads on iOS 10.X and Android 5.X+ devices.

For TL;DR people out there, there is a live working example including all coding and many self explanatory comments. Check the Quick Links.

Why building an offline web app

There are immediate advantages from developing a web app vs a native app:

  • Unified development skillset.
  • Easier deployment and automatic updates.
  • Extensive collection of open source web libraries.
  • No dependency on stores nor operating system.

But we were also well aware of the existing constraints:

  • Market familiarity to look for apps on the two main app stores
  • Limitations to certain device capabilities (Web APIs are catching up though).

But our reason could not be simpler: We strongly believe in the future of web apps. Google is certainly going in that direction. And did you know Steve Jobs himself shared the same vision? A very powerful presentation on web apps from Patrick Kettner (Edge team @ Microsoft) helped us decide.

The teammates: The web app stack

We have shared this path with a few dependable friends: The rookie Service Worker and the somehow more veteran – and sometimes misunderstood AppCache.

IndexedDB came to the rescue to help facilitate conversations with any of the other two, and to set up the basis for a common ground (sic).

The game: How to make your app a PWA

1. Using IndexedDB for PWA

Although there are several libraries out there to help you use IndexedDB (Debie, jquery-indexeddb, pouchDB, db.js and more), performance and reliability are among our top priorities so we decided to leverage the standard IndexedDB API following the MDN documentation to have absolute control over the final delivery. This was quite a straightforward process and we could adjust the development to our specific needs.

Although the IndexedDB coding may be worth a second post to share our hits and misses, it works like a charm in both Android and iOS and it is not the goal for this article.

2. Setting it up like a native app

Both Android and iOS provide the mechanism to add an icon to the home screen, so that users can initiate the app with just one touch, like any other native app. Applications run from the home page will use the entire screen and will hide the browser navigation bar. This feature is well documented on Web fundamentals for Android and on tuts+ for iOS. This is an optional step but highly recommended for your web app to be initiated as any other mobile app. It is not however the purpose of our tutorial.

3. Working offline

Making the app work offline for Android is quite straight forward using Service Workers (HTTPS being the only requirement). AppCache setup is also simple but required a bit more caring. Working offline is also what used to make a big difference between a native and a PWA. Not anymore. This is the real deal and the plan is to explain below how we made it work for us. Keep reading. We provide a step by step guide for both scenarios.

3.1 Service Workers for PWA

Before you continue, you need to be familiar with how promises work. If that is not yet the case, a classic read would be this article from Mr. Service Worker (aka Jake Archibald). However,ap in this case, I found the MDN tutorial more self explanatory. Now, to start implementing a service worker solution, an option is to go to MDN again, but my personal recommendation is to have a look at Matt Gaunt’s introduction to Service Workers.

Here’s a quick summary of the steps we followed for our implementation.

1. Register the service worker

The browser needs to support serviceWorker, so make sure to add the check. This code is to be implemented as starting point of your application (index.html in our example).

https://gist.github.com/beebole/26632ca09b5c85da5b4d9b570e490bdc

2. Enable offline usage at page load

In order to work offline, you need to cache the files that the application needs to run. You need to create an independent file (ex:sw.js) containing the service worker logic. This file needs to be placed at the lowest folder including all the elements that you want to use offline (i.e. if you have files within multiple folders that you want to cache, the sw file needs to be placed on the root folder including all those upper folders).

By default, the SW will be installed but will only become active the next time the user loads the app. It is possible however to precache all pages from the application while installing the Service worker (even before it becomes active).

Simply capture both the install and activate events for the service worker and add the predefined list of elements to the cache.

This step needs to be performed on the SW code itself.

Declare a variable to specify the list of resources to be cached:

https://gist.github.com/beebole/c8f60bfe6f56f5a400050586dc70ef2b

Define handler for the install event and skip the waiting:

https://gist.github.com/beebole/878764ac495a1c4521c50c849101fc71

Cache list of predefined resources during activate event, using the cache API:

https://gist.github.com/beebole/7181904aaeabd9b9f2a83d9b07008159

Finally, make sure the current becomes the active service worker:

https://gist.github.com/beebole/e138db94e6c12e4a34d3a8ee747421d8

Even if the user goes offline after loading the page, they will be able to navigate through the entire application (as long as elements are properly defined in the urlsToCache array).

3. Retrieve resources from the cache
Add a listener to all fetch actions from the browser. The SW has the ability to capture the request before sending the response to the browser:

https://gist.github.com/beebole/2843b5fbb9ea219add6fcafd39eebc0d

Please note that self in this case does not correspond to window, which does not exist in the service worker context, but to the service worker instance, which is independent from the browser window. Next, use the event to send the response to the browser:

https://gist.github.com/beebole/0788485dd7d6051610137c089aa4e908

At this stage, we can send the requested object from the cache, if it is already there:

https://gist.github.com/beebole/11f2e23214a81f83da1588a0b7d28d5b

Or we can request it from the network otherwise:

https://gist.github.com/beebole/19e788956465a65b8864889387ef241f

4. Cache all resources

It is optional to add some logic that will cache any other resource accessed by the user while navigating through the app. You even have the option to skip the previous step and only cache resources as the user accesses them.

Within the fetch handled above, if the resource is not already in the cache, we may want to save it there for a later use. For that, we first capture the response from the prior promise:

https://gist.github.com/beebole/5958d95abdc7d7ad0c214f5d600fd10c

And then use the cache API to save the response:
https://gist.github.com/beebole/71c96387da81e36b6602269d0ae09621

name_your_cache_container being the name of the cache that you can see from the Web Console when debugging your app. You can use any string there.

5. Manage updates

If there is a change in the sw.js file, the next time the user loads the app the new SW will be identified and loaded on a waiting state, until the app closes again so the older SW version can be released. By default also, any change to any cached page will not be detected by the app as it goes cache-first for performance reasons.

As result, any change to any web page that you want to be detected by the app needs to be accompanied by a change to the SW file (sw.js) and it will not be captured by the browser until the user refreshes the app.

It is possible however to force this refresh when any change to the SW is detected. (Some authors prefer to display a message informing the user of the new updates and let them decide whether to refresh or not).

In order to automatically refresh, make sure the service worker is updated whenever a new version is available. Include this check in your web page (index.html in our example), right after the SW registration process:

https://gist.github.com/beebole/1285f8f32e0de8d21e5ec482629caf22

To test this feature, you will have to deploy this app to your own server, make a change to the html page AND the sw.js file, deploy and reload.

3.2 AppCache management

At this point, you may have already read Jake’s dissertation about the AppCache. Additionally, if you read the AppCache MDN documentation, you might be discouraged by the first paragraph:
AppCache for PWA, web app

HOWEVER, and this is a big however, if you want to deploy a web application with offline capabilities on iOS, AppCache is so far your only option, and as we learned, sometimes painfully, it works just fine.

Below are the required steps we followed to make it work.

1. Define list of items to be cached

This is a mandatory step with AppCache and it can be defined on an independent file called appcache manifest. You can name it the way you want but a common naming convention is to use: offline.appcache.

Mandatory line to start your file with:

CACHE MANIFEST

Add a line to maintain the version. This will help the browser to identify when the cache files need to be refreshed. You can enter any text, but the recommendation is to keep a numeric counter to keep track of your versioning internally.

Use ‘#’for comments within the appcache manifest file.

# BeeBole Mobile AppCache v251

Define the list of files to be cached. You can use absolute or relative paths here. The list needs to be defined by CACHE:

CACHE:
.
index.html
mobile.js
mobile.css

If all your resources can be retrieved from the cache while offline, the following line applies. This can be modified if specific resources need to be white listed and retrieved from the network even when offline (sic):

NETWORK:
*

Finally, it is possible to add a fallback resource in case any resource is not available from the cache while offline. Most typical usage is to display an offline image to indicate that there is no access to the network resource. It works by pairs, indicating a fallback resource for each URI. To use the same fallback for all resources, you can use ‘/’.

FALLBACK:
/ offline.png

And this is how it looks all together:

CACHE MANIFEST
# BeeBole Mobile AppCache v251
CACHE:
.
index.html
mobile.js
mobile.css
NETWORK:
*
FALLBACK:
/ offline.png

2. Call the appcache manifest from the web app.
Simply include the following code within your html page, with the proper manifest file location:

https://gist.github.com/beebole/4d07f4d4efbfd614db9e187396f7b221

The problem here is that the offline.appcache file needs to be available on page load time, which may not always be the case. There is a trick (as proposed by Patrick Kettner) to load the appcache dynamically, by injecting it in a hidden iframe.

3. Make sure the last version is used 

Until this point, you are making sure that the appCache is refreshed whenever a change exists in the manifest file (remember that it is required for the manifest to be modified – increasing the version for instance – each time you modify any file within the application, or else the AppCache will keep feeding from the cached resources.).

The application will use the previously cached resources until the page refreshes. You can force this refresh by detecting the change on the AppCache status.

Simply add a listener to reload the application if a new manifest has been identified. It is required for the manifest to be modified (increasing the version for instance) each time you modify any file within the application:
https://gist.github.com/beebole/fb926ad2a5405c955ca8df9aa714a85f

Our Beebole mobile web app: Live example

beebole-timesheet-pwa-web-appAs we stated at the beginning, it is possible to have a PWA working offline for both Android and iOS devices. You can see it for yourself: register for a 30 day free trial, no credit card required, and add our timesheet app to your homescreen.

To make this whole post clearer, check out https://beebole.github.io/mobile-app-demo/ where you can find all the above pieces working together.
Best way to test it is to see the app load, then go on airplane mode and navigate through the bunnies. You should see all bunnies even if you did not load them prior to going offline.

Commented sources are available at Github.

FAQs and tips

How do I make sure my Chrome console uses the last version of the files when developing and testing on localhost?

Although the article describes how to make sure the browser will update the last version of the app when deployed to any server, it is true that localhost sometimes behaves its own way when developing with Chrome. The best way to make sure your app is using the last files is to manually clean up the cache files, which you can do via Chrome Console – Application – Cache Storage. Right click on the name of your cache (FILES_CACHE in our example above) and delete. If there is no arrow to expand the Cache Storage level, just right click there and select Refresh.

Why is the browser saying that I am offline when I reload the page when on airplane mode?

AppCache is very sensitive about the pages that are cached, and not very good in communicating when there is something it does not like. As soon as there is anything that the browser is trying to load and it has not been properly defined in the cache manifest, the application will fail entirely and the browser will state that you need a connection. Triple check that you have included all files. A common mistake is to forget about the favicon.ico if you have one in your server.


So far, so good…Leave us a comment with your own questions or examples. Have you built your own mobile web app? Would you mind sharing your thoughts here with us?

Americas Customer Satisfaction Overlord at BeeBole and always a Developer. Scorpion hunter, skater and A/C lover. Former table tennis 'champion'.

Comments

Thanks for the post.
It should be possible to support single page application in offline mode right? Let’s say the application is developed in Angular we cache the whole application using manifest file. The SPA should work without any need for network. With HTML storage, we should be able to store previously retrieved data and use this data to display when the application is running in offline mode. Is this correct?

That is correct Jishnu,

As long as you capture every single page the application needs to work offline – the first time the user accesses the app – you are good to go. The user will be able to run the application when offline.

To cache data, we are using IndexedDB ourselves but you can also use localStorage.

In this post I am providing a very basic example – without the IndexedDB component – but the BeeBole mobile application is just that: A Single Page Application that works offline

Cheers!
Miguel

Maybe not the right place to ask.
But im wondering how do you navigate cms pages.

lets say you have a blog and products.
The urls are most of the time not defined by a natural patterns.

so how does a pwa routing send the urls to the right components.
– product
– news
– content

Hi Michael,

I would say the standard PWA behavior is to cache all static pages that your application need to run offline. If your pages are dynamically created, you can always add some logic in your Service Worker to cache pages as they are navigated by the user. This way, user will be able to at least get access to those pages in case the connection is lost.

Kind regards,
Miguel

The question was asked:
“How do I make sure my Chrome console uses the last version of the files when developing and testing on localhost?”

The answer was to remove cache and reload. I had the same problem with my github site. Doing so all the time was cumbersome and — left me confused as to why the latest code wasn’t ALWAYS loading up immediately. While developing, an older version of my code was kept showing up. Then I discovered my problem. I needed to remove the lingering COOKIES. Go to your secured site, click on the lock icon, and delete them cookies!

Hi Leslie,

Thanks for your comment. Deleting the cookies will work but will force you to login again if you are using an authorized app, which is our case. The quickest way (for me) to make sure the last pages are taken into account is to use the Application >> Clear storage option from the Chrome console menu. Then make sure that you select the box to clean up the cache storage and click on ‘Clear site data’

Quite informative article, it solved bundle of technical concepts.
However, a person should know the benefits and reasons to choose PWA rather than standard web or mobile application. I would suggest everyone to discover advantages of PWA and then build one for themselves.

Related posts

Beebole’s best picks: Top 10 apps for construction workers

Published: 2020/6/4 | Katie Stearns

Succeeding in the construction industry comes down to being smart, organized, quick and efficient. In addition to having a strong ...

Read more

16 project time tracking tools for enhanced project financial management

Updated: 2023/11/29 | Helen Poliquin

There is a considerable number of project time tracking tools on the market. Because there are so many options, we’ve ...

Read more

5 challenges of mobile workforce management that companies will face in 2019: We asked the experts

Published: 2017/11/29 | Magda A. Torres

The rise of the concept Mobile Workforce is a tremendous example of how technology is transforming our personal and professional ...

Read more