Persisting Data in Local Storage
We have our core functionality working now, but as soon as we refresh the application we lose all of our data. This lesson is going to be about making sure that data sticks around.
There are different ways you can go about persisting data and state. In some cases, your application might use an external database (especially if this is an application where data is shared by users like a chat application or social network). However, in this case, the data is going to be stored completely locally on the device itself.
To achieve this, we are going to use Ionic’s IonicStorageModule
. We have already talked about this in a previous module, but the general idea is that Ionic provides us with a simple key/value storage system, and underneath the hood it can use various storage mechanisms to store that data. In the browser, it can store the data in the browsers local storage but, as we have discussed before, that can be unreliable for permanent data storage. If we intend to build this application natively for iOS and Android, then we can also make use of the native SQLite database for more reliable storage of data, and we can configure the IonicStorageModule
to use that automatically if it is available.
Install Ionic Storage and the SQLite Plugin
To use the IonicStorageModule
we will need to install the following package:
npm install @ionic/storage-angular
If we want to use an SQLite database when we run this application natively, then we will also need to install the following package:
npm install localforage-cordovasqlitedriver
Then, we can configure the storage mechanisms we want to use in our root module.
Add the following import to the root
AppModule
inapp.component.ts
:
IonicStorageModule.forRoot({
driverOrder: [
// eslint-disable-next-line no-underscore-dangle
CordovaSQLiteDriver._driver,
Drivers.IndexedDB,
Drivers.LocalStorage,
],
}),
For reference, these are the imports you will need to add to the top of your file:
import { Drivers } from '@ionic/storage';
import { IonicStorageModule } from '@ionic/storage-angular';
import * as CordovaSQLiteDriver from 'localforage-cordovasqlitedriver';
What we have added to our AppModule
gives us the ability to use the Ionic Storage API, and through the driverOrder
allows us to specify our order of preference for storage mechanisms. We are saying we want to use the native SQLite database if it is available, but if it is not we will fall back to IndexedDB
and then to Local Storage
. If we are running in the browser, not natively on a device, then the IonicStorageModule
will fall back to using either IndexedDB
or LocalStorage
depending on what is available.
Creating a storage service
We have the ability to use Ionic’s Storage API, now we are going to create a service to interact with it. Technically, we could just interact with storage through any service we want to store data in, but by creating a service specifically for storage we can make it easier to use in our application and all of our other services can rely on the storage service, instead of using the Ionic Storage API directly.
Create a file at
src/app/shared/data-access/storage.service.ts
and add the following:
import { Injectable } from '@angular/core';
import { Storage } from '@ionic/storage-angular';
import { from, Observable } from 'rxjs';
import { map, shareReplay, switchMap, take, tap } from 'rxjs/operators';
import { Checklist } from '../interfaces/checklist';
import { ChecklistItem } from '../interfaces/checklist-item';
@Injectable({
providedIn: 'root',
})
export class StorageService {
#checklistHasLoaded = false;
#checklistItemsHasLoaded = false;
storage$ = from(this.ionicStorage.create()).pipe(shareReplay(1));
loadChecklists$ = this.storage$;
loadChecklistItems$ = this.storage$;
constructor(private ionicStorage: Storage) {}
saveChecklists(checklists: Checklist[]) {}
saveChecklistItems(checklistItems: ChecklistItem[]) {}
}
This service is a little complex, so I have left the methods unimplemented for now, and we will tackle them one at a time. First, let’s talk about the stream we have set up for storage$
:
private storage$ = from(this.ionicStorage.create()).pipe(shareReplay(1));
In order to use Ionic Storage, we need to inject it, and call its create
method. This returns a Promise that will resolve with an instance of the storage object that we can interact with to set/get data. Without using observable streams we might do something like this:
private storage: Storage;
async init(){
this.storage = await this.ionicStorage.create();
}
Which requires a little more code, but the main annoying thing about this approach is that when we try to use this.storage
we need to somehow check that the init
method has finished running. We also need to make sure that we call init
from somewhere in the application.
What we do instead is create a stream from this Promise by using the from
creation operator:
private storage$ = from(this.ionicStorage.create())
And now if we want to use storage we can subscribe to this stream to get access to it. But… with the code above this would re-run this.ionicStorage.create()
and create a new instance of storage every time we subscribe to storage$
. This is not what we want, so we do this instead:
private storage$ = from(this.ionicStorage.create()).pipe(shareReplay(1));
By piping on shareReplay(1)
it will run this.ionicStorage.create()
the first time some code subscribes to this.storage$
, but every time after that it will just share the existing result with new subscribers, rather than re-running the observable creation logic. The first subscriber triggers the storage to be created, and any subscribers after that just instantly get a reference to that same storage.
Now let’s implement the rest.
Modify the
load*
streams to reflect the following: