I have been working on the Fedora Android App, as my GSoC project. One of our primary goals is to make the Fedora app usable in offline mode. In the lifetime of the application, we make several API calls and load several assets across several origins. Caching API responses (which were in JSON) was straightforward, however, images were entirely a different challenge.
We display a few images in our header carousel. We fetch the images from the articles posted under the “New in Fedora” section in the Fedora Magazine. We want to cache the images and make them available, even if the user is offline.
- We have to support Android 4.4 (Chrome 33). Its compatibility with modern ES6 is similar to IE 11 levels.
- We have to cache images from external services which do not support CORS.
In progressive web apps, Caching is implemented almost invariably using Service Workers. However, Cordova does not support service workers.
Fetch in “no-cors” mode and CacheStorage API
The new Fetch and CacheStorage API takes advantage of the underlying browser caching mechanism and saves us from re-implementing a lot of cache management code. However, it is not available on Android 4.4.
File system Caching
Another approach I considered was to directly download the files to the disk and serve them. It is a feasible solution, but it introduces yet another headache for file management. Cache files can be removed by the OS at any time. I worked on a prototype but soon abandoned it as the code for downloading and retrieving became complex with so many moving parts. Had we been storing a rather huge amount of data, this would have been the recommended approach.
The Choosen Solution
IndexedDB to the rescue! Prior to Cache API, Service Worker based solutions relied upon storing content in the IndexedDB. It’s a great method to store data for the browser which can be queried just like a database. It also can store binary blobs.
We directly don’t depend on IndexedDB API as it is hard as hell (even the MDN Docs warn about it). We rely on Ionic Storage to expose IndexedDB as a key-value store.
Here is how it works:
- We fetch the URL of the images we need to display.
- If the image URLs are already cached we return the data URI from the cache.
- If the URL is not cached, we download the URL and store the value in the cache.
- We compare the old list of URLs with the new list of image URLs. We delete the data we do not require.
This gives us a very fine-grained control of what to cache and when to cache. Our caching logic is implemented in a CarouselProvider, a plain Ionic provider.
We begin with a model that will represent an image. This is the type that will be passed to the components when they request for carousel images. It contains the image data/link and the associated metadata.
The loadFile function of the CarouselProvider loads a data URI into a given Image object if it is available. It returns the unmodified Image object if the image is not cached. This allows us to only download images that are not already cached, hence saving data of the users.
To work-around CORS issue while using live-reload during development, we rewrite the URL to use our Ionic proxy. CORS issue is not present while running as a Cordova App.
Our component first loads the cached images using
loadCachedPosts and then requests updated images using
You can see the full code here:
- CarouselProvider: https://pagure.io/fork/amitosh/Fedora-app/blob/mag-offline-improvement/f/src/providers/carousel/carousel.ts
- HomePage (the component that uses CarouselProvider): https://pagure.io/fork/amitosh/Fedora-app/blob/mag-offline-improvement/f/src/pages/home/home.ts