Apps are meant to be online, but shouldn’t leave the user stranded when offline.

So you’re building your next big app and everything is great, except when your users are offline, in an area with spotty reception or just on slow connections in general. Suddenly their profile, messages, or whatever in-app content is loaded from your server’s API, is dead-slow, timing out or flat-out failing to load. This is a horrible user experience, and makes your app completely useless when offline.

In the post we will look at caching your Ionic app’s observables to reduce latency, improve the user experience and make your app still work while offline.

If you just want to try it out, you can clone the example source code here and try it out instantly with ionic serve : https://github.com/westphalen/ionic-offline-example

Installation

Start by firing up a new Ionic project. If you haven’t done this before, make sure to head over to IonicFramework.com and read how to get started.

ionic start myOfflineApp

You will need two packages to make the magic happen, the first being @ionic/storage which is needed to store data between app sessions and the second is ionic-cache-observable , the package that does all the magic:

npm install --save @ionic/storage ionic-cache-observable

Now we need to register the modules in our AppModule , eg. src/app/app.module.ts :

//.........

@NgModule({

declarations: [

...

],

imports: [

...

// Add the cache module

CacheModule,

// We will use the HttpClient for our API provider

HttpClientModule,

// Add the storage module

IonicStorageModule.forRoot(),

IonicModule.forRoot(MyApp),

...

],

//.........

Load data from an API

Now, if you already have a provider for your API data, skip this section. Otherwise, let’s create one with data from JSONPlaceholder.

ionic g provider Placeholder

This will create your new provider in src/providers/placeholder/placeholder.ts . Open it up and let’s add some API data:

import { Injectable } from '@angular/common';

import { HttpClient } from '@angular/common/http'; // Remember to import RxJs dependencies.

import { Observable } from 'rxjs/Observable';

import 'rxjs/add/observable/defer';

import 'rxjs/add/operator/delay'; // Interface for the data returned by the JSONPlaceholder API

export interface Placeholder {

userId: number;

id: number;

title: string;

body: string;

} @Injectable()

export class PlaceholderProvider { constructor(private http: HttpClient) {} public random(): Observable<Placeholder> {

// Observable.defer makes sure we generate a

// random number each time the Observable is triggered.

return Observable.defer(() => {

const randIndex = Math.ceil(Math.rand() * 10); // 0-10 const url = 'https://jsonplaceholder.typicode.com/posts/'

+ randIndex; return this.http.get<Placeholder>(url).delay(1000);

});

} }

Okay, we have defined an interface that describes the Placeholder data we expect from our API. Next is our random function, that requests random data from the API, simulating the data that is constantly changing in our app. Additionally we enforce a 1000 ms delay to simulate a slow connection — so we can better see what’s happening.

Caching data in your views

Onto the page where we want to display our data, for example src/pages/home/home.ts :

//....................

export class HomePage { public data$: Observable<Placeholder>; constructor(placeholderProvider: PlaceholderProvider,

cacheService: CacheService) {

// The data we want to display on the page.

// This could be a user's profile or his list of todo items.

const dataObservable = placeholderProvider.random(); // We register a cache instance, using a unique name

// so the CacheService will know what data to display next time.

cacheService.register('home', dataObservable)

.subscribe((cache) => {

this.data = cache.get$;

});

}

// ......................

In our page component we inject the PlaceholderProvider for our API data and the CacheService from the package we installed earlier. We then register the API data observable with the CacheService using the identifier 'home' to be able to recognise the data between sessions. The register observable provides us with the cache object, which represents our Placeholder data from now on. In this simple scenario we will just assign the cache’s get$ onto the page’s data$ variable. The cache will automatically pull data from the dataObservable we provided, which we can display in home.html :

<ion-content>

<div *ngIf="data$ | async as data">

<h1>{{ data.title }}</h1>

<p>{{ data.body }}</p>

</div>

</ion-content>

The async pipe will automatically update the view when data$ is updated.

Now, fire up the app using ionic serve , or run it on your device. When you first open the page you should notice about a seconds delay before a random title and body appears from our API. Now pop the page, refresh or reopen the app and see the magic happen. When you navigate to the page for the second time, data will more or less instantly appear, and even though the data is supposed to be random, we’re showing exactly the same content as before. The API is never called, so this data can even be showed while offline! But of course, we don’t want to be showing outdated data all the time. Let’s look at how to refresh the cache.

Refreshing cached data

Let’s make a simple refresh function using the cool Ionic Refresher component. Go back to your page component, eg. home.ts :

//....................

export class HomePage { public data$: Observable<Placeholder>; private cache: Cache<Placeholder>; constructor(placeholderProvider: PlaceholderProvider,

cacheService: CacheService) {

const dataObservable = placeholderProvider.random(); cacheService.register('home', dataObservable)

.subscribe((cache) => {

// Save the cache object.

this.cache = cache; this.data = this.cache.get$;

});

} onRefresh(refresher: Refresher): void {

// Check if the cache has registered.

if (this.cache) {

this.cache.refresh().subscribe(() => {

// Refresh completed, complete the refresher animation.

refresher.complete();

}, (err) => {

// Something went wrong!

// Log the error and cancel refresher animation.

console.error('Refresh failed!', err); refresher.cancel();

});

} else {

// Cache is not registered yet, so cancel refresher animation.

refresher.cancel();

}

}

// ......................

Two things have changed here. In the constructor, we are now setting the cache object on our page class, so we can access it from the second change, our new onRefresh function, which takes a Refresher argument from the Ionic Refresher component. In this function we simply call refresh on the cache object, and then update the UI accordingly through refresher . You don’t need to worry about the refreshed data, as it is automatically pushed to data$ .

Implement the Ionic Refresher component in your page template, eg. home.html :

<ion-content>

<ion-refresher (ionRefresh)="onRefresh($event)">

<ion-refresher-content></ion-refresher-content>

</ion-refresher> <div *ngIf="data$ | async as data">

<h1>{{ data.title }}</h1>

<p>{{ data.body }}</p>

</div>

</ion-content>

All we’ve done here is the addition of ion-refresher that calls our onRefresh function.

Now fire up your app. You should still see the cached data from before. Do a pull-to-refresh and wait about a second and you should see the data change! The cache object calls your registered observable in the background and pushes the new data as soon as it arrives. Refresh your app, and the latest content still loads until you pull-to-refresh again. Ok, but users shouldn’t have to pull-to-refresh all the time. Let’s look at automatically refreshing the content.

Refresh automatically

You can keep the useful pull-to-refresh functionality and still provide refreshed data automatically. We just need to change a few things in our page component, eg home.ts :

//....................

constructor(private placeholderProvider: PlaceholderProvider,

private cacheService: CacheService) {

// I have moved to ionViewWillEnter.

} ionViewWillEnter() {

const dataObservable = placeholderProvider.random(); cacheService.register('home', dataObservable)

.subscribe((cache) => {

this.cache = cache; this.data = this.cache.get$; // Refresh the cache immediately.

this.cache.refresh().subscribe();

});

} // .....................

We have moved the cache registration from the constructor to the ionViewWillEnter method, which is called every time the user wants to open the page. This includes navigating between views, so even without closing or reloading your app, users will see refreshed data every time the view opens. Once the ionViewWillEnter method retrieves the cache object from cacheService.register , we call to cache.refresh() right away. There is no need to handle the subscription, as we just want to refresh the data silently.

Try it out! Every time you open the page, you will instantly see the old data, but about a second later the refreshed/random data replaces the cached data. And you can still pull-to-refresh! Don’t be scared to call cache.refresh() multiple time, as the refresh observable is shared. So your user can safely pull-to-refresh even while your page is already refreshing the data in the background.

Thanks for reading!

If this has helped you, please give a clap! Also, if you have any questions or feedback don’t hesitate to leave a comment.

Read more about the ionic-cache-observable package and its possibilities on GitHub: https://github.com/westphalen/ionic-cache-observable

Feel free to suggest new features or submit pull requests with improvements.