Introduction to cross-platform development with Ionic

The following post is a 2-part article about cross-platform development. The first part is a brief introduction to the whole development format with its advantages and disadvantages, and a few popular alternatives. The second part is a tutorial for an expense tracker application written in Ionic (with Angular). The two parts are independently readable, but going through both gives a more exhaustive cross-platform experience. The tutorial requires basic Angular knowledge for easier understanding.

Part 1 - Introduction

Cross-platform development is the practice of developing software products or services for multiple platforms or software environments.

In recent years, this has been a topic that has been mentioned and considered by many developers. The promise of „build one codebase, run on any platform” sounds incredibly promising, both in terms of time management and expenses, so why would we code natively for every different platform?

Advantages, disadvantages

As mentioned previously, the main benefits of cross-platform development are the reduced development time and the significant cost efficiency. Other than these, there are a few other benefits of this form of development, including the ease of updating. Since there is only one application, all updates can be synchronized across all the platforms and due to this instant synchronization, deploying changes are way much easier.

Like any other promising technology, this new approach also has some drawbacks that cannot be ignored. Cross-platform applications cannot always integrate flawlessly with their target operating systems, because of the inconsistent communication between the native and non-native components. Compared to that, native code enjoys direct access to the host’s operating system and functionalities. In addition to the performance issues, designing the perfect user experience can be challenging with shared codebase, since it cannot always take advantage of the native-only features and methods.

At the beginning of the development, it is important to consider which aspects are essential for the targeted application. If it is a thick client application and flawless performance or extraordinary user interface are important parts of the application, it is better to go native. Other than that, cross-platform development may be the perfect choice for the project.

Alternatives

Since cross-platform development became so popular in the last few years, it is understandable that there are multiple frameworks dedicated to the approach. With no claim of being exhaustive, I would like to introduce a couple of popular platforms related to this topic.

Developed by Facebook, React Native is without a doubt the most popular JavaScript-to-native platform. For developers that are familiar with JavaScript (or even React), getting started is extremely easy, and the platform is well-tested, since Facebook and Instagram rely on it.

NativeScript is React Native’s biggest competitor. Both offer a similar cross-platform development experience. NativeScript lets the developer access 100% of the native APIs via JavaScript and reuse packages from NPM, CocoaPods, and Gradle.

Beloved by .Net developers, Xamarin uses a shared C# codebase and with Xamarin tools developers are able to write native Android, iOS and Windows apps with native user interfaces. It also provides access to the native APIs apart from faster development with Xamarin plugins and the NuGet package.

Ionic is arguably the most popular framework for hybrid application development. Hybrid apps run from within a native application and its own embedded browser (WKWebView on iOS, WebView on Android). Mainly it is because it allows developers to use the well-known and Angular framework (while React and Vue integration is also coming to the framework). It also comes with a powerful CLI which provides an amazingly simple way to create, code, test and deploy Ionic apps to any platform. It is built on top of Apache Cordova, which provides access to native API’s like camera, bluetooth, fingerprint authentication, GPS and so on (but in the future, Ionic is about to change default container to a new Native API Container called Capacitor that makes it easy to build web apps that run on iOS, Android, Electron, and on the web as PWAs, with full access to native functionality on each platform).

Part 2 – Getting started with Ionic

Getting started with Ionic is incredibly easy with the help of its impressive CLI.

First steps

First, if it is not done yet, install NodeJs for node package manager. Then install Cordova and Ionic with npm:

npm install -g cordova ionic  

Then create an Ionic project with the following:

ionic start  

After this command, enter the name chartapp for the project, and also choose starter template. Since we will create a simple one-page application, hit enter on blank.

After the project generation, the CLI will ask us if we want to install the Ionic Appflow and connect our app. Choose no (n), the Appflow SDK offers a CI/CD platform and other amazing tools, but we do not need them this time.

When the generation process is done, we can change directory to our freshly created project and run the app with the following:

cd chartapp  
ionic serve  

Firstly, it does not look like a mobile application, but in modern browsers we can easily access mobile device view (e.g. in Chrome DevTools Ctrl/Command+Shift+M on Windows/Mac).

After these few easy steps, we can open our project in our favorite code editor, and everything is ready to create an awesome cross-platform application.

Expense tracker application

The main purpose of this tutorial is to get an insight into cross-platform development with Ionic while creating a simple mobile application that allows us to track our everyday expenses.

In this application the user will be able to enter the amount and choose the type (Taxes, Food, Transportation, Entertainment, Clothing or Other) of the spending. After tapping on the + button, the given input will appear on a spectacular doughnut chart. Tapping on the bin button deletes the content of the chart.

chartapp

Development

The generated project structure looks very similar to a typical Angular project. The src/index.html file is the main entry point for the app, though its purpose is to set up scripts, CSS includes, and bootstrap, or start running our app. As usually in Angular projects, most of our code goes to the files of the src folder. For a more in-depth view, the official project structure description can be sufficient.

Add the following attributes to the HomePage class in the src/app/home/home.page.ts file. These are variables for the chart for the expenses, the amount of money spent, and the id of the selected expense from the expenses array. The expenses array also contains the name, the already spent amount and the displayed color of every expense. We will use the inputFocused variable to see if the expense amount input field is in focus or not.

doughnutChart: any;  
moneySpent: number;  
selectedExpenseId: number;  
inputFocused: boolean = false;

expenses = [  
  { id: 1, name: 'Taxes', amount: 0, color: '#FFEB3B' },
  { id: 2, name: 'Food', amount: 0, color: '#E91E63' },
  { id: 3, name: 'Transportation', amount: 0, color: '#2196F3' },
  { id: 4, name: 'Entertainment', amount: 0, color: '#4CAF50' },
  { id: 5, name: 'Clothing', amount: 0, color: '#F57C00' },
  { id: 6, name: 'Other', amount: 0, color: '#BDBDBD' }
  ];
Chart (a small non-Ionic part)

Let’s start the development process with the creation of the doughnut chart. This part is not related to Ionic, but we will be working on the dataset of the chart in the following segments. Even though this part is not about Ionic components, it is interesting to see how fluently Angular libraries and directives work in Ionic.

In the project folder, install and save ChartJS (for the charting library) and ng2-charts (for the easy Chart.js integration in Angular) into the project with the following command:

npm install ng2-charts chart.js --save  

In the home.page.ts file import Chart from Chart.js:

import { Chart } from 'chart.js';  

After the import, we can create and configure a new Chart and display it on the related view in the HomePage class:

  createChart() {
    this.doughnutChart = new Chart('doughnutChart', {
      type: 'doughnut',
      data: {
        labels: this.expenses.map(e => e.name), // mapping the names from the expenses array
        datasets: [{
          data: this.expenses.map(e => e.amount),
          backgroundColor: this.expenses.map(e => e.color),
          borderWidth: 2
        }]
      },
      options: {
        legend: {
          labels: {
            usePointStyle: true // only for prettier labels
          }
        }
      }
    });
  }

Make your HomePage class implement OnInit, and call the createChart method in the ngOnInit function.

import { Component, OnInit } from '@angular/core';

// ...
export class HomePage implements OnInit {

// ...

  ngOnInit() {
    this.createChart();
  }

In the src/app/home/home.page.html file, change the content of the element to the canvas:

<ion-content>  
  <canvas id="doughnutChart" height="300" width="300"></canvas>
</ion-content>  

(You cannot see the chart with zero money spent, but temporarily changing the amounts in the expenses array can do the trick.) Also, you can change the content of the to any title, for example “Expenses”.

Adding data

Add the following methods to the HomePage class in the home.page.ts file. This method should be called every time the amounts in the expenses array change:

  refreshChartData(): void {
    this.doughnutChart.data.datasets[0].data = this.expenses.map(e => e.amount);
    this.doughnutChart.update();
  }

This method finds the chosen expense in the expenses array by id and adds the entered amount to it:

  addDataToChart(expenseid: number, spent: number) {
    let index = this.expenses.findIndex(item => item.id === expenseid);
    this.expenses[index].amount += spent;
    this.refreshChartData();
  }

This method does validation for the money spent and the selected expense, and if there is not any selected expense or the amount entered is wrong or missing, it presents a toast. A toast provides simple feedback about an operation in a small popup, and it shows the given message and disappears automatically after the given duration time (in ms):

  async addData() {
    if (this.moneySpent > 0 && this.selectedExpenseId) {
      this.addDataToChart(this.selectedExpenseId, this.moneySpent);

      this.moneySpent = null;
      this.selectedExpenseId = null;
    } else {
      const toast = await this.toastController.create({
        message: 'Wrong or missing data!',
        duration: 1500
      });
      toast.present();
    }
  }

To make the toast work, we need to import ToastController from ‘@ionic/angular’. ToastController is a component used to create Toast components:

import { ToastController } from '@ionic/angular';  

And then add it to the constructor:

constructor(public toastController: ToastController) { }  

Add the following elements to the ion-content under the chart in the home.page.html file. The Ionic input component is a wrapper to the HTML input element with some custom styling and additional functionality. This input is bound to the moneySpent attribute. The event given in ionBlur/ionFocus is emitted when the input loses/acquires focus, and for aesthetic reasons, the floating action buttons will not appear when the focus is on the input field:

  <ion-item>
    <ion-label position="stacked">Amount of money spent:</ion-label>
    <ion-input type="number" [(ngModel)]="moneySpent" placeholder="Enter amount" 
    (ionFocus)="inputFocused = true" (ionBlur)="inputFocused = false"></ion-input>
  </ion-item>

Selects are form controls to select an option, or options, from a set of options, similar to a native select element. When a user taps on the select, a dialog appears with all of the options in a list. In this select the user can choose an expense from the expenses array:

  <ion-item>
    <ion-label>Type of expense</ion-label>
    <ion-select [(ngModel)]="selectedExpenseId" placeholder="Select type">
      <ion-select-option *ngFor="let e of expenses" [value]="e.id">{{e.name}}</ion-select-option>
    </ion-select>
  </ion-item>

A floating action button (FAB) is a circular button that triggers action in the app’s UI. FABs should be placed in a fixed position that does not scroll with the content. FABs mostly contain an icon and Ionic provides a huge set of IonIcons to use in FABs (and anywhere else).

  <ion-fab vertical="bottom" horizontal="end" slot="fixed" *ngIf="!inputFocused">
    <ion-fab-button (click)="addData()">
      <ion-icon name="add"></ion-icon>
    </ion-fab-button>
  </ion-fab>

After creating the previous methods and adding the elements to the the home page should look and work like this:

Deleting data

In this section, the data deleting functionality is implemented. Pressing the delete button will reset the displayed expenses to zero. For example, this could be used monthly to restart the expense tracking.

Add the 2 following methods to HomePage class in home.page.ts. The first method iterates through the expenses array and sets the amounts to zero.

  deleteData(): void {
    for (let index = 0; index < this.expenses.length; index++) {
      this.expenses[index].amount = 0;
    }
    this.refreshChartData();
  }

This method presents an alert and asks if the user wants to delete the data. An alert is a dialog that presents users with information or collects information from the user using inputs. As it can be seen, there are two buttons presented on the alert: No button for canceling the deletion, and Yes button for calling the deleteData() method.

  async presentDeleteAlert() {
    const alert = await this.alertController.create({
      header: 'Delete',
      message: 'Are you sure you want to delete all your expenses?',
      buttons: [
        {
          text: 'No',
          role: 'cancel',
          cssClass: 'secondary'
        }, {
          text: 'Yes',
          handler: () => {
            this.deleteData();
          }
        }
      ]
    });
    await alert.present();
  }

To make alerts work, similarly to the toast, we need to add AlertController to the imported elements from ‘@ionic/angular’:

import { ToastController, AlertController } from '@ionic/angular';  

Also add it to the constructor:

constructor(public toastController: ToastController, public alertController: AlertController) { }  

Add the floating action button for deleting data to the in home.page.html:

  <ion-fab vertical="bottom" horizontal="start" slot="fixed" *ngIf="!inputFocused">
    <ion-fab-button color="danger" (click)="presentDeleteAlert()">
      <ion-icon name="trash"></ion-icon>
    </ion-fab-button>
  </ion-fab>
Saving the data

At this stage of the tutorial we have a completely working application, but every time we close the application, we lose the data since our app does not store the expenses. For this purpose, Ionic provides an easy way to store key/value pairs and JSON objects. Ionic Storage uses a variety of storage engines underneath, picking the best one available depending on the platform.

In the terminal install the cordova-sqlite-storage plugin:

ionic cordova plugin add cordova-sqlite-storage  

Then install the package:

npm install --save @ionic/storage  

Next, add it to the imports list in your NgModule declaration in src/app/app.module.ts:

import { IonicStorageModule } from '@ionic/storage';  
  // ...
  imports: [
    BrowserModule, 
    IonicModule.forRoot(), 
    AppRoutingModule, 
    IonicStorageModule.forRoot()
  ],
  // ...

After the previous steps, you can inject Storage into the HomePage:

import { Storage } from '@ionic/storage';  

In the HomePage class add Storage to the constructor, and create a storageKey attribute that will serve as a key, while the expenses array will be the value:

  storageKey: string = 'expenses';

  constructor(public toastController: ToastController, public alertController: AlertController, public storage: Storage) { }

Finally, create the methods for saving and loading the expenses array in the HomePage class:

  saveData() {
    this.storage.set(this.storageKey, JSON.stringify(this.expenses));
  }

  loadData() {
    this.storage.get(this.storageKey).then((val) => {
      if (val) this.expenses = JSON.parse(val);
  this.createChart();
    });
  }

Change the method called in ngOnInit() from createData() to loadData(), and the expenses array will be loaded from Storage after initialization:

  ngOnInit() {
    this.loadData();
  }

Also add the saveData() method to the addData() and deleteData() methods to always save changes:

  async addData() {
    if (this.moneySpent > 0 && this.selectedExpenseId) {
      this.addDataToChart(this.selectedExpenseId, this.moneySpent);

      this.moneySpent = null;
      this.selectedExpenseId = null;
      this.saveData(); // saving added
    } else {
      const toast = await this.toastController.create({
        message: 'Wrong or missing data!',
        duration: 1500
      });
      toast.present();
    }
  }

  deleteData(): void {
    for (let index = 0; index < this.expenses.length; index++) {
      this.expenses[index].amount = 0;
    }
    this.refreshChartData();
    this.saveData(); // saving added
  }

Now you have a fully functioning expense tracker application with a fancy doughnut chart that displays all the entered spendings compared to each other.

Deploying the app

At this point, we have a fully functioning application and it would be cool to generate a release build and maybe even publish the application to App Store or Play Store. I suggest using these simple official guides to help us through the whole process:

To install and set up the required development kits, the official guide provides instructions (the red notes depending on your operating system):
https://ionicframework.com/docs/v1/guide/installation.html

To the publishing process, this guide explains the process entirely (with key generation, apk signing, etc.):
https://ionicframework.com/docs/v1/guide/publishing.html

Summary

Conclusion

Choosing between native and cross-platform development can be tricky. With applications that require high performance, native applications always win, since they can be designed to use the resources of the device entirely, while hybrid apps are not optimized for one platform directly.

At the start of the project, the developers need to consider priorities and the chances of future features and changes, and make the big decision based on them.

Going with cross-platform development, I can completely recommend Ionic with its beautiful and smooth UI components, and the fact that it is based on Angular can be super beneficial, since most of front-end developers have some experience with it.

Other useful references

A more detailed comparison between cross-platform development tools: https://www.outsystems.com/blog/free-cross-platform-mobile-app-development-tools-compared.html

About the Ionic and Angular lifecycle: https://ionicframework.com/docs/lifecycle/angular

Chart.js samples for different chart types: https://www.chartjs.org/samples/latest/

Using the keyboard plugin from Cordova instead of detecting when the input field is in focus is also a good and maybe more sophisticated solution: https://github.com/ionic-team/cordova-plugin-ionic-keyboard

Ionic Studio for faster and smoother development: https://ionicframework.com/studio