Build Offline Ionic Angular 5 Applications

Build Offline Ionic Angular 5 Applications

In mobile apps, we have come across many conditions in which, the rendered data (from the server) is the same most of the time but is repeatedly fetched from the server. This data fetching every time from the server is useless and a huge waste of data bandwidth. Another similar situation where we can’t even access the app is — when we call the sever with no network/internet then there will be NO_INTERNET error in the App.

In such situations, we can customize the Ionic App, and manage the application when there is no internet. To do that we have to first recognize the status of the Network/Internet connection, and then perform online or offline logic.

Why do you need offline Performance?

Why do we need offline application performance????.. The Answer is to improve the user experience and maintain the state of the application properly, even with network fluctuations. As in today’s world, everything is dependent on the Internet and most applications won’t work if there is no Internet available. In some cases, it can, while in other cases we strictly need the Internet to perform some action. So the complete offline performance is dependent on your business logic. e.g If you have an online text editor it can let you type and edit after the initial loading of the document without any internet, but, for saving and other syncing features it will require internet.

Any drawbacks of Offline Performance?

With many advantages, offline performance brings many challenges for the developers. As this brings unexpected things to happen like no proper syncing of offline database and online database, it will lead the application to the dead state. And one of the major points, User is unpredictable and can take any action — such an unhandled case can cause an inconsistent app state, so keep in mind while giving offline privileges to the user.

How do Ionic 5 Applications works offline??

?We will modify our Ionic application to have two major features, required for offline functionality:-

  1. Store the DB tables on the device's local storage:- We will store some of the users and necessary information in the local storage (LocalStorage API, or any browser database). It will help us to operate the application offline as we have that data stored in local storage. This will resolve some of the problems that are less dependent on the Server. If your application has a very small database, you can sync all data of the user to phone storage. However, if you have large data of individual users, you can store data only for that session. This logic can be different for different apps, and one has to design the business logic accordingly.
  2. Store API calls in local storage:- With data, we also store the APIs in local storage, we store APIs in such a way that it helps us recognize which API is completely served (i.e. response is received) and which one is left to be served. This helps us to make application flow more stable, as if there is no network available and we want to perform an HTTP POST request to our server then we store the API request in the local storage and will perform it later as soon as the network is available. e.g. Like syncing documents with an online database.

Implementing the Offline features in Ionic App?:

We will assume that you are already familiar with simple Ionic Angular apps, if not — do check our other blogs to learn about its setup. We will start with an already set up Ionic Angular application — and ionic commands will be used directly in the project root directory.

Network detection plugin

The below command will install the packages and library required for network detection in the working Ionic project.

$ ionic cordova plugin add cordova-plugin-network-information
$ npm install @ionic-native/network
        

?Using this command we will detect whether we have the network connection or not.?

Maintaining user data in the local storage?table

Maintaining users' tables in local storage can give an illusion to the application - that the app is working in offline mode. So while login/registering, we will save the user's data in local storage and use that user's data everywhere we need in the application. Be sure not to store secure data like cards?and?passwords?in the offline database.?

Before that, we have to install the?cordova-sqlite-storage?plugin &?@ionic/storage?in our project.


$ ionic cordova plugin add cordova-sqlite-storage

$ npm install --save @ionic/storage
        

The above command will install the storage plugin and package in the working project and now we have to Import?[Line 14, Line 19]?and Inject the SQLite and N/W information module?[Line 31, Line 39]??in?app.module.ts


import { NgModule, NO_ERRORS_SCHEMA, APP_INITIALIZER } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { AppComponent } from '@app/app.component';
import { AppRoutingModule } from '@app/app-routing.module';
import { AgmCoreModule } from '@agm/core';
import { environment } from '@env/environment';
import { IonicStorageModule } from '@ionic/storage';
import { InitUserProvider } from '@app/services/inituser/inituser.service';
import { APIService } from './services/api/api.service';
import { DataService } from './services/data/data.service';
import { OfflineService } from './services/offline-manager/offline.service';
import { Network } from '@ionic-native/network/ngx';
	
@NgModule({
  declarations: [
	AppComponent,
  ],
  entryComponents: [],
  schemas: [NO_ERRORS_SCHEMA],
  imports: [
	    BrowserModule,
	    IonicModule.forRoot(),
	    IonicStorageModule.forRoot({ 
          name: environment.IONIC_STORAGE 
        }), // Here you can use your DB name
	    AppRoutingModule,
  ],
  providers: [
	    SplashScreen,
	    APIService,
	    OfflineService,
	    DataService,
	    Network,
	    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
	    InitUserProvider,
	    { provide: APP_INITIALIZER, 
          useFactory: initUserProviderFactory, 
          deps: [InitUserProvider], multi: true 
        },
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

export function initUserProviderFactory(provider: InitUserProvider) {
	return () => provider.load();
}
	        

In?app.module.ts, we have declared an?APP_INITIALIZER?which basically runs the?load()?function (shown below in code) defined in the?InitUserProvider?service file, during the initial load of the application (Before loading any components). This?load()?function will check if there is an existing user in the local storage (if any then set it as the?loggedInUser). It will be dependent on network status, if the network exists — then the User will be fetched from the server. And if there is no network, then localUser will be saved as?loggedInUser.


async load() {
  if (this.networkStatus) {
    const onlineUser = await this.api.getUserFromServer(this.userid)
    this.api.setLoggedInUser(onlineUser)
  }
  else {
    const localUser = await this.data.getStoredUserData();
    this.api.setLoggedInUser(localUser);
  }
}
        

And during registering or login flow we can set the user in the local storage using the?setUserData()?function defined in?DataService?file?(create data service file using the ionic generate service *path/filename*). You can add the below line in your existing login/register logic.


await this.data.setUserData(userData);
        

In the above line,?userData?is the user's data fetched from the server or any database. You must be wondering that we are fetching the data from the server then why is offline data saved here? Only at first login, we have to fetch the data from the server after that we simply use the local storage data, every time.


import { Injectable } from '@angular/core'
import { Storage } from '@ionic/storage';
	
@Injectable({
   providedIn: 'root'
})
export class DataService {
	
  constructor(private storage: Storage) { }
	
  async setAppSettings(settings) {
	 await this.storage.set('settings', settings);
  }
  async setUserData(userData) {
	 await this.storage.set('user', userData);
  }
  getStoredUserData() {
	 return this.storage.get('user');
  }
  async getAppSettings() {
	 return this.storage.get('settings');
  }
};
        

Here in?DataService, we use the?set?and?get?methods of the storage module. This will store the data in?key: value?pairs in the local storage. We are done with storing the data and now we can get the Data when the app is offline. To check whether the app is offline or online, we will use the Network module. Below is the code for?load() function that will run when the app is initialized:-


import { Network } from '@ionic-native/network/ngx'
import { AlertController, Platform } from '@ionic/angular';
	
export class InitUserProvider {

  disconnectSubscription: any;
  connectSubscription: any;
  networkStatus = true;
	
  constructor(private network: Network, private platform: Platform) {
	  this.platform.ready().then(() => {
	     this.checkInternetConnection();
	   });
  }
	
  checkInternetConnection() {
	  this.disconnectSubscription =  
        this.network.onDisconnect().subscribe(async () => {
	      this.networkStatus = false;
	    });
	    this.connectSubscription = this.network.onConnect()
        .subscribe(() => {
	      this.networkStatus = true;
	    }); 
   }
	

  async load() {
	    if (this.networkStatus) {
	    // We have the Internet, so we can fetch the users data from the server
	      const onlineUser = await this.api.getUserFromServer(this.userid);
	      this.api.setLoggedInUser(onlineUser);
	    } else {
	      const localUser = await this.data.getStoredUserData();
	      this.api.setLoggedInUser(localUser);
	    }
  }
};
        

Here we first set the Network listener using?checkInternetConnection method?[Line 16]?that will detect whether we have the network or not and on the basis of that we fetch the user's data from the server or from the local storage.

You can modify the above code according to your app structure, as this code will help you to work offline with the user table.

Maintaining API Storage?database

In this section, we will explore how can we store the API calls in the local storage. We will define a procedure that will help our application to work in an offline manner, this procedure will help us to save the API data(if there is no network/internet) and call it later when the network is available. Overall we have 2 flows:-

  1. The Application does not have the network/Internet:-?In this flow, we will save the HTTP request(API — path and data) in the local storage and then make the HTTP request when network/Internet is available. And once the HTTP request is served, then we will set a?completed?flag to true. This procedure will help us to store the API and later call that when the network is available.
  2. Application has the Network/Internet:-?This will be the normal flow in which we have the network/internet. We first save the HTTP request(API) in local storage and make the HTTP request and make its completed flag to true. Remember we always store the HTTP request in local storage anyway, network is available or not, this makes flow easier to code.

Complete flow in Action

In the above 2 flows, the initial steps are the same but further, they are divided into 2 parts that will be decided on the basis of the availability of the network. We will implement this logic in a separate service file i.e.?API-manager (to create the service file run the below command)


$ ionic generate service services/api-manager
        

Once the above command is executed the service file will be added to the project, Now we can start to code our main logic:-

No alt text provided for this image

Below is the code for the API-Manager (we will explain it later):-


import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core';
import { environment } from '@env/environment';
import { Network } from '@ionic-native/network/ngx';
import { AlertController, Platform } from '@ionic/angular';
import { Storage } from '@ionic/storage';
import { forkJoin, from, Observable, of } from 'rxjs';
import { finalize, switchMap } from 'rxjs/operators';
import { StoredRequest } from '../../models/request';


@Injectable({
  providedIn: 'root'
})
export class APIManager {


  disconnectSubscription: any;
  connectSubscription: any;
  networkAlert: HTMLIonAlertElement;
  networkStatus = true;
  requestSubscriber: any;
  mutex = true;


  constructor(
    private storage: Storage,
    private http: HttpClient,
    private network: Network,
    private platform: Platform,
    private alertCtrl: AlertController
    ) {
        this.platform.ready().then(() => {
          this.checkInternetConnection();
        });
   }
   
  async checkInternetConnection() {
    this.disconnectSubscription = 
    this.network.onDisconnect().subscribe(async () => {
      this.mutex = true;
      this.networkAlert = await this.createNetworkAlert(
        'No Internet', 
         false, 
        'Please Check you internet Connection and try again',
        {
        text: 'Ok.!, Checking',
        role: '',
        cssClass: 'secondary',
        handler: async () => {
          this.networkAlert.dismiss();
          this.networkAlert = null;
        }
      });
      this.networkAlert.present();
      this.networkStatus = false;
    });
    this.connectSubscription = this.network.onConnect().subscribe(() => {
      this.networkStatus = true;
      if(this.networkAlert) {
        this.networkAlert.dismiss();
        this.networkAlert = null;
      }
      if (this.mutex) {
        this.mutex = false;
        this.requestSubscriber = this.checkForUnCompleteAPI().subscribe();
      }
    });
  }


  checkForUnCompleteAPI(): Observable <any> {
    return from(this.storage.get(environment.REQ_STORAGE_KEY)).pipe(
      switchMap((storedOperations: any) => {
        let storedObj = JSON.parse(storedOperations);
        if (storedObj && storedObj.length > 0) {
          return this.sendRequests(storedObj).pipe(
            finalize(() => {
              this.completeAllRequests().then(async () => {
                this.requestSubscriber.unsubscribe();
                const stored = 
                    await this.storage.get(environment.REQ_STORAGE_KEY); 
                    // use the db name that you prefer
              });
            })
          );
        } else {
          return of(false);
        }
      })
    )
  }


  completeAllRequests(): Promise <any> {
    return new Promise((resolve, reject) => {
      this.storage.get(environment.REQ_STORAGE_KEY).then((StoredReqs) => {
        let storedObj = JSON.parse(StoredReqs);
        storedObj.forEach((request) => {
          request.completed = true;
        });
        this.storage.remove(environment.REQ_STORAGE_KEY);
        resolve(
            this.storage.set(
              environment.REQ_STORAGE_KEY, 
              JSON.stringify(storedObj)
            )
        );
      });
    })
  }


  storeCallAndRespond(method, url, header, data?): Promise <any> {
    return new Promise(async (resolve, reject) =>  {
      const action: StoredRequest = {
        url: url,
        type: method,
        data: data ? data : null,
        time: new Date().getTime(),
        completed: false,
        response: null,
        header: header,
        id: Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5)
      };
      
      const Store = await this.storeRequest(action);
      await this.repeatRequest(action).then((response) => {
        console.log('Response', response);
        resolve(response);
      })
    });
  }

  async repeatRequest(action) {
    return new Promise(async (resolve, reject) => {
        let response;
        if (!this.networkStatus) {
          // No Internet
          resolve(action.data);
        } else {
          // Internet is there
          if (action.type === 'GET') {
            response = await this.http.request(
                                          action.type, 
                                          action.url, 
                                          {
                                             headers: action.header
                                          })
                                          .toPromise();
          } else {
            response = await this.http.request(action.type, action.url, 
            {body: action.data, headers: action.header}).toPromise();
          }
          this.updateActionObject(action, response);
          resolve(response);
        }
    });
  }

  storeRequest(action) {
    this.storage.get(environment.REQ_STORAGE_KEY).
     then((storedOperations) => {
      let storedObj = JSON.parse(storedOperations);
      if (storedObj) {
        storedObj.push(action);
      } else {
        storedObj = [action];
      }
      return this.storage.set(
         environment.REQ_STORAGE_KEY, JSON.stringify(storedObj)
      );
    });
  }

  updateActionObject(action, response) {
    this.storage.get(environment.REQ_STORAGE_KEY)
    .then((storedOperations) => {
      let storedObj = JSON.parse(storedOperations);
      storedObj.forEach((call) => {
        if (call.id == action.id) {
          call.response = response;
          call.completed = true;
        }
      });
      this.storage.remove(environment.REQ_STORAGE_KEY);
      return this.storage.set(
           environment.REQ_STORAGE_KEY, JSON.stringify(storedObj)
      );
    });
  }

  sendRequests(operations: StoredRequest[]) {
    console.log('Req Called')
    let obs = [];
    let oneObs;
    for (let op of operations) {
      if (!op.completed) {
        if (op.type === 'GET') {
          oneObs = this.http.request(op.type, op.url, 
          {headers: op.header});
        } else {
          oneObs = this.http.request(op.type, op.url, {body: op.data,   
          headers: op.header});          
        }
        console.log('Array res', oneObs);
        obs.push(oneObs);
      }
    }
    return forkJoin(obs);
  }

  ngOnDestroy() {
    this.disconnectSubscription.unsubscribe();
    this.connectSubscription.unsubscribe();
  }

  async createNetworkAlert(header, backdropDismiss, message, 
  buttonOptions1, buttonOptions2?): Promise<HTMLIonAlertElement> {
    const alert = await this.alertCtrl.create({
      header,
      backdropDismiss,
      message,
      buttons: !buttonOptions2 ? [buttonOptions1] : [buttonOptions1, buttonOptions2]
    });
    return alert;
  }
};
        

In the above code, we store and make the HTTP request using the?storeCallAndRespond()?function?[Line 96]. In this function, we simply store the API Request in a particular format mentioned below:-?


const action: StoredRequest = {
  url: url,
  type: method,
  data: data ? data : null,
  time: new Date().getTime(),
  completed: false,
  response: null,
  header: header,
  id: Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0,   5)
};
        

This Action object contains the parameters that will be used in making HTTP requests like?URL,?method(type),?completed?flag,?response?(set the response when an HTTP request is served),?headers, and the?unique Id.

Now we can set the API request using?storeRequest() method that is defined in API-manager itself?:-


storeRequest(action) {
  this.storage.get(environment.REQ_STORAGE_KEY).
  then((storedOperation) => {
    let storedObj = JSON.parse(storedOperations);
    if (storedObj) {
     storedObj.push(action);
    } else {
     storedObj = [action];
    }
  return this.storage.set(environment.REQ_STORAGE_KEY, 
     JSON.stringify(storedObj));
  });
}
        

The above code will push the existing API request object (action) into the local storage DB. Till this point above two points have the same flow. Now onwards on the bases of network/internet availability, we will differentiate between the flow:-?(further logic is contained inside the?repeatRequest() method)


async repeatRequest(action) {
    return new Promise(async (resolve, reject) => {
      let response;

      if (!this.networkStatus) {    <-- No Internet
      // No Internet
         resolve(action.data);
       } else {                     <-- Internet is working
      // Internet is there
      if (action.type === 'GET') {
         response = await this.http.request(action.type, action.url,
         {headers: action.header}).toPromise();
      } else {
         response = await this.http.request(
            action.type,
            action.url,
            {body: action.data, headers: action.header}
         ).toPromise();}
         this.updateActionObject(action, response);
         resolve(response);
      }
   });
}
        

In this flow, we will first check whether the network/internet is available or not?[denoted in the above code using ←], and if we have the network — we will make the HTTP request using the?action?object and make that API stored Object’s(action)?completed?flag?true?and save the response in it.

And if the app doesn’t have the network then we will simply resolve the promise and continue the UI flow, This procedure will make no change in UI rendering and the end-user will not feel like there is no internet/network. After this, once the network is back the flow will call the?checkForUnCompleteAPI() method?[Line 1 in below Code]?that will serve the uncalled(i.e. completed: false) HTTP request.


checkForUnCompleteAPI(): Observable <any> {

  return from(this.storage.get(environment.REQ_STORAGE_KEY)).pipe(
    switchMap((storedOperations: any) => {
      let storedObj = JSON.parse(storedOperations);
      if (storedObj && storedObj.length > 0) {
        return this.sendRequests(storedObj).pipe(
          finalize(() => {
            this.completeAllRequests().then(async () => {
              this.requestSubscriber.unsubscribe();
              const stored = await 
               this.storage.get(environment.REQ_STORAGE_KEY); 
              // use the db name that you prefer
            });
          })
        );
      } else {
        return of(false);
      }
    })
  )
}
        

The above code will check for the API request array in local storage and if any, then passes it to the?sendRequests() method that will make the uncompleted HTTP request using the?action?object:-


sendRequests(operations: StoredRequest[]) {
  let obs = [];
  let oneObs;
  for (let op of operations) {
    if (!op.completed) {
     if (op.type === 'GET') {
       oneObs = this.http.request(op.type, op.url, {headers: op.header});
     } else {
       oneObs = this.http.request(op.type, op.url, {body: op.data, headers:
       op.header});
     }
     console.log('Array res', oneObs);
     obs.push(oneObs);
    }
  }
  return forkJoin(obs);
 }
          

The above code will check for the?completed?flag for each API request, and if the?completed?flag is?false?then make an HTTP request using the stored action object.

This above-defined code (i.e. API-manager service) will be helpful when our UI is dependent on the API call but we do not have the network, then we store the API call in local storage with all its parameter and once the internet comes back then we try to make that HTTP request at that time.?

How to modify HTTP requests in the existing?App?

Now we will look at how we add this HTTP API logic to the existing application which earlier has direct API calls:-?

Where you have API calls (i.e. HTTP calls) in your application, there instead of directly making that HTTP call we will pass the required parameters to the?storeCallAndRespond()?function like below:-


updateUser(id, userData): Promise <any> {
  return new Promise(async (resolve, reject) => {
  const response = await
    this.apiManager.storeCallAndRespond('PUT', `     
    `${this.url}/clients/${id}`, { Authorization: `JWT ${this.token}` },    
     userData);
    resolve(response);
  });
}
        

We will pass the required HTTP options to the?storeCallAndRespond() method?[Below Code]?defined under API-manager. There are 3 arguments that are passed to?storeCallAndRespond()?method :- 1) Request Method. 2) Request URL. 3) Request Header


storeCallAndRespond(method, url, header, data?): Promise <any> {
  return new Promise(async (resolve, reject) =>  {
   const action: StoredRequest = {
    url: url,
    type: method,
    data: data ? data : null,
    time: new Date().getTime(),
    completed: false,
    response: null,
    header: header,
    id: Math.random().toString(36).replace(/[^a-z]+/g,   '').substr(0, 5)
   };
   const Store = await this.storeRequest(action);
   this.repeatRequest(action).then((response) => {
     console.log('Response', response);
     resolve(response);
   })
  });
}
        

Conclusion

In this tutorial, we have learned how we can covert our Ionic 5 application into an offline working application. So now you are ready to fit this feature in your existing or new application with ease. If you want to know more about different features and methods to handle any of the technology, just go to?Enappd Blogs Section

Tinashe Mangoro

I make stuff using VSCode

1 年

fantastic! let me see if i can apply this to an ionic vue project

要查看或添加评论,请登录

社区洞察

其他会员也浏览了