dilluns, 19 de juny del 2017

Ionic 2, using LoadingController

Sometimes our app it has that some realize tasks what need much time or we don't know the amount of time needed for receive data of the server, or for upload/download an image, file, etc... Normally these task works in background for not keep busy the main thread as this would block the app. The app is running normally but the user can have the sensation that somethings don't works correctly, the screen now is blank, it is waiting for data but the user does not know. For solve it and say to the user that the app is working and waiting data or waiting for the current task finished we can use the LoadingController for show a dialog box with a message to let the user know who the app is working normally.

Here you can read more about these controller. We can customize according our needs, we might dismiss it after a period of time but in my case I don't know when will finish the task.

For that I divide the process in three parts. First declare a variable of type Loading, second I create a function where creates the dialog  and assign them to the Loading variable and show the dialog. And finally when the promise finishes your task, I dismiss the dialog.


First import the components

import { NavController, NavParams, LoadingController, Loading } from 'ionic-angular';

We declare the variable that contains the dialog

loading: Loading;

The function to create and present the dialog

presentLoading () { this.loading = this.loadingCtrl.create({ content: "Waiting data ..." }); this.loading.present(); }


And use it

constructor(public navCtrl: NavController, public navParams: NavParams, public httpService: HttpService, public loadingCtrl: LoadingController) { this.source = navParams.get("src"); this.options = navParams.get("ops"); this.presentLoading(); this.httpService.getData(this.source, this.ops) .then((result:any) => { this.data = result; this.loading.dismiss(); }); }

Obviously is necessary to consider the possible exceptions, the catch clauses where  dismiss the loading dialog and show a message with the problem.

And that's all

dilluns, 12 de juny del 2017

Ionic 2. Checking permissions

Following with what was discussed in the last entry in Android since Marshmallow is necessary to check at run time the permissions cataloged how to dangerous and read or write in the external storage is one of them. In the case at hand, for restore the backup, as the app writes in the sandbox it isn't necessary to check the permissions. 

For check the permissions, we use the diagnostic plugin and we will use the same plugin for check permissions for access to location, sensors, phone, contacts, etc... but in this entry I only will write over the access to the external storage.

Well, first we install the plugin

   ionic plugin add cordova.plugins.diagnostic


And

   npm install --save @ionic-native/diagnostic


And we create a service to manage all the permissions

   ionic g provider PermissionsService

And we edit ../providers/permissions-service.ts and write

import { Injectable } from '@angular/core'; import { Platform } from 'ionic-angular'; import 'rxjs/add/operator/map'; import { Diagnostic } from '@ionic-native/diagnostic'; /* Generated class for the PermissionsService provider. See https://angular.io/docs/ts/latest/guide/dependency-injection.html for more info on providers and Angular 2 DI. */ @Injectable() export class PermissionsService { constructor(public platform: Platform, public diagnostic: Diagnostic) { }
isAndroid() { return this.platform.is('android'); }
checkWritePermission(): Promise<boolean> return new Promise (resolve => if (this.isAndroid) { this.diagnostic.isExternalStorageAuthorized() .then(authorized => { if(authorized) { resolve(true); } else { this.diagnostic.requestExternalStorageAuthorization() .then(authorization => { resolve (authorization == this.diagnostic.permissionStatus.GRANTED); }); } }); } }); } }

And we use it

createBackup () { if (this.permissions.isAndroid()) {< this.permissions.checkWritePermission().then(permission => { this.backup(); }, error => { console.log(error); }); } }

And the result looks like this



dilluns, 5 de juny del 2017

Ionic 2. Backup stories part II

Well, in the previous entry I explain how to install the plugin and how decide according the system the origin of data and the destination. Every OS has a different file system and we must adapt us at your characteristics. In the two cases the origin it's similar and use  applicationStorageDirectory property for access both of them. The difference is in the directory that contains the database, Library/LocalDatabase/ in iOS and databases/ in Android.

In the case of the destination it's where the differences are more big. In Android I think the best place is the root of the external storage, either in the root or in a directory for the app in the root. The access for this location is easy and the user just have to plug your device and has already access to the files and directories. 

iOS is another thing. Reading the File System Programming Guide from Apple Developers here we can see that recommends Documents/ in the Data Container inside the sandbox for store the user-generated content. The contents of this directory is accessible with iTunes and iCloud when making a backup or through file sharing but not  as in Android with a file manager when plugs the device in a computer.

Having said this already we can implement our methods for make a backup and restore it. For the destination path we use two different properties externalRootDirectory for Android and documentsDirectory for iOS, you can see this in the previous entry. 

The idea for the implementation is easy, first we check if file exist in the destination path, if it exist we remove it because the plugin cannot rewrite it. After of remove the file or if the file doesn't exist we copy it . If the operation have success shows a success message and if it is rejected shows an error message.

The code for the backup

backup() { let path: string; let destPath: string; let dirMessage: string;
if (this.iOSDir.length > 1) { destPath = this.iOSDir; path = this.file.applicationStorageDirectory + 'Library/LocalDatabase/'; this.translate.get('IOSDOCUMENTSDIRECTORY') .subscribe(value => { dirMessage = value; }); } else { destPath = this.AndroidDir;< path = this.file.applicationStorageDirectory + 'databases/'; this.translate.get('ANDROIDDOCUMENTSDIRECTORY') .subscribe(value => { dirMessage = value; }); } //check if file exist, if it exist remove it and copy new file. //If the file doesn't exist copy it this.file.checkFile(destPath, this.fileName) .then(success => {
//if file exist first remove it this.file.removeFile(destPath, this.fileName) .then(success => { //Copy new instance of the file this.file.copyFile(path, this.fileName, destPath, this.fileName).then(success => { //translations this.translate.get('BACKUPSUCCESS') .subscribe(value => { this.alertMes = value; this.alertMessage(this.alertMes + dirMessage); });
}, error => { this.translate.get('BACKUPUNSUCCESS') .subscribe(value => { this.alertMes = value; this.alertMessage(this.alertMes); }); }); }, error => { this.translate.get('BACKUPUNSUCCESS') .subscribe(value => { this.alertMes = value; this.alertMessage(this.alertMes); }); }); }, error => { //first copy of file this.file.copyFile(path, this.fileName, destPath, this.fileName).then(success => { this.translate.get('BACKUPSUCCESS') .subscribe(value => { this.alertMes = value; this.alertMessage(this.alertMes + dirMessage); }); }, error => { this.translate.get('BACKUPUNSUCCESS') .subscribe(value => { this.alertMes = value; this.alertMessage(this.alertMes); }) }); }); }

And for restore

restoreBackup() { let path: string; let destPath: string; let dirMessage: string; if (this.iOSDir.length > 1) { path = this.iOSDir; destPath = this.file.applicationStorageDirectory + 'Library/LocalDatabase/'; } else { path = this.AndroidDir;
destPath = this.file.applicationStorageDirectory + 'databases/'; } //check if backup file exist this.file.checkFile(path, this.fileName).then(success => { //check if file exist, if it exist remove it and //copy new file. If the file doesn't exist copy it this.file.checkFile(destPath, this.fileName) .then(success => { //if file exist first remove it this.file.removeFile(destPath, this.fileName) .then(success => { //Copy new instance of the file this.file.copyFile(path, this.fileName, destPath, this.fileName).then(success => { //Close and reopen database this.dbService.closeDatabase().then(success => { this.dbService.openDatabase(); this.translate.get('RESTOREBACKUPSUCCESS') .subscribe(value => { this.alertMes = value; this.alertMessage(this.alertMes); }); }); }, error => { this.translate.get('RESTOREBACKUPUNSUCCESS') .subscribe(value => { this.alertMes = value; this.alertMessage(this.alertMes); }); }); }, error => { this.translate.get('RESTOREBACKUPUNSUCCESS') .subscribe(value => { this.alertMes = value; this.alertMessage(this.alertMes); }); }); }, error => { this.translate.get('RESTOREBACKUPUNSUCCESS') .subscribe(value => { this.alertMes = value; this.alertMessage(this.alertMes); }); }); }, error => { this.translate.get('BACKUPUNOTEXIST') .subscribe(value => { this.alertMes = value; this.alertMessage(this.alertMes + path); }); }); }

Basically for restore we do the same changing the origin and the destination of the file. The last operation when the file are copied is close the database and reopen it for what the changes are efectives in the app.

And that would be all for iOS and Android before to Marshmallow. Since Marshmallow it is necessary to check the permissions and this I leave it for the next entry.

dilluns, 29 de maig del 2017

Ionic 2. Backup stories part I

When I started this blog, the idea was to practice my English with the hope to improve it, I'm self taught and I think that it is a good way to do it. Normally I write about what I'm doing, and this time is how to do a backup in my app developed with Ionic 2. The point is all of my apps don't use any server to store data, they not ask for special permissions, only the internet access for serve ads and in this case in Android, permission for write in the external storage for save the backup. But this is the second question of this story, so let's start at the beginning.

My app stores the data in a database SQLite and it to do in two tables. The amount of data is not very large and I don't need any more. My first idea was to create a method  that makes a query of all the data and store it in a file in JSON format. Create the backup is really easy and restore it not too complicated. I looked in the Ionic documentation and it provides a File API in Ionic Native for read and write files. Here you can find more info about this.  Well, it seems that I already have all of I need so let's install it.

From a terminal inside the project directory

   ionic plugin add cordova-plugin-file


And
   npm install --save @ionic-native/file




And add the plugin in the app.module.ts. First import it and then we put it on the providers section

import { File } from '@ionic-native/file';

providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}, DbService, File, AdMob]

Well, it is ready to use.

But reading the documentation of the plugin I see which it have methods to check, delete and copy files. And I have thought SQLite stores the data in a file so for make a backup I only need copy this file and store it in another directory that finally is the same that should I do with the result of the query. Ok, it's already decided. I will copy the file in a directory accessible to the user.

Now the first step is find the file.  In my database service, I have wrote

openDatabase() { return this.db.openDatabase({ name: 'eDomestic.db', location: 'default', createFromLocation: 1 }) .catch(error => console.error('Error opening database', error)); }


But where is location: default. Well, in Android it is in /data/data/your-app/databases/ or using the plugin File.applicationStorageDirectory + 'databases/'. In iOS it is in /var/mobile/Applications/<UUID>/Library/LocalDatabase/  or using the plugin File.applicationStorageDirectory + 'Library/LocalDatabase/.

First we need to know what system we are using Android or iOS, for that

if (/(android)/i.test(navigator.userAgent)) { this.AndroidDir = this.file.externalRootDirectory; } else if (/(ipod|iphone|ipad)/i.test(navigator.userAgent)) { this.iOSDir = this.file.documentsDirectory; }

AndroidDir and iOSDir contains the destination for the backup. For the origin path

if (this.iOSDir.length > 1) { destPath = this.iOSDir; path = this.file.applicationStorageDirectory + 'Library/LocalDatabase/'; this.translate.get('IOSDOCUMENTSDIRECTORY') .subscribe(value => { dirMessage = value; }); } else { destPath = this.AndroidDir; path = this.file.applicationStorageDirectory + 'databases/'; this.translate.get('ANDROIDDOCUMENTSDIRECTORY') .subscribe(value => { dirMessage = value; }); }

Here, depending of what OS we are using we select a destination and origin or another. And now we can use the plugin for copy the file. But where save the file and why?

In the next entry I will continue explain the as and the why I do it like that.

dilluns, 22 de maig del 2017

Ionic 2. Adding a popover menu


Maybe you want add new features on your app, if this is not an action which it affect only one thing such as realize a backup or restore it, print a summary, etc... for this, a popover is a good option to contain these actions. 

A popover is a dialog that typically appears above other content on the screen and it usually it is positioned at top of the current page, and displays actions that can be carried out. As almost everything in Ionic it is really easy, another thing is as implement these actions. Here you can find more info.

A possible popover could look like to this


Well,  for implement a popover, Ionic provides a PopoverController that we use it for creating, present and dismiss these dialog.

For that we need a template and a controller, I separate it in two files a popover.html for the template and a popover.ts for the controller and I save it in the same directory which contains the page on it appears. It is my way of doing it, but not the unique.

The template can be like this 

<ion-list> <ion-list-header color="primary">Options</ion-list-header> <button ion-item (tap)="backup()">Backup</button> <button ion-item (tap)="restore()">Restore backup</button> </ion-list>

As you see it's just a list

The popover controller looks like

import { Component } from '@angular/core'; import { ViewController, NavParams } from 'ionic-angular'; @Component({ templateUrl: 'popover.html' }) export class PopoverHomePage constructor (public viewCtrl: ViewController, public navParams: NavParams) { } backup() { let data = this.navParams.get('data'); save(data); this.viewCtrl.dismiss(); } restoreBackup() { let data = this.navParams.get('data'); restore(data); this.viewCtrl.dismiss(); } }

And now inside the template where the popover will be showed, in the toolbar section we put a button and at the method which will present the popover we passed him the $event. That variable, contains the original browser event object.

<ion-header> <ion-toolbar color="primary"> <ion-title>eDomestic</ion-title> <ion-buttons right> <button ion-button icon-only color="royal" (tap)="optionsPopover($event)"> <ion-icon name="more"></ion-icon> </button> </ion-buttons> </ion-toolbar> </ion-header>


Inside the controller we put the method which respond to the click button event.

//Show popover menu optionsPopover(event) { let popover = this.popoverCtrl.create(PopoverHomePage, {page: this.pageContent}) popover.present({ ev: event }); }

And in the controller we import PopoverController from ionic-angular. 

First parameter is the component and the second the data that we pass to the popover. Finally we add the popover component in the app.module.ts file and it is ready for work .

And that it's all our popover is ready to be showed.

dilluns, 15 de maig del 2017

Ionic 2. Nested lists

One of the things that surprised me about Ionic 2 it is the ease with which it creates lists, an array in the controller and a *ngFor directive in the template is sufficient to create a list. A little example can be

In the controller

months: string[] = ['January', 'February', 'March', 'April',
                    'May','June', 'July', 'August',
                    'September', 'October', 'November',
                    'December'];

An in the template

<ion-list inset> <button ion-item *ngFor="let month of months" (tap)="monthSelected(month)"> {{ month }} </button> </ion-list>

An the result looks like


Easy and quick. But if you need put a list inside another, the things change.  It's not that it's more difficult, but the things change, starting in the data model and finishing in the template.

This is the way in that I solve this problem, I don't know if it is the best way, but it is the one I used and it works.

First the problem, I need put inside of each month in the list another list with his correspondant expense. So the data that I need are the month name, the total expense of the month, the correspondent expense and his amount.

The result has to be something like this



For that in the controller I have an array with the expenses

expenses: string[] = ['Shopping basket', 'Clothes', 'Electronics', 'Phone', 'Electricity','Fuel', 'Maintenance', 'Transport', 'Hairdressing, ...', 'Health', 'Entertainment','Taxes', 'Rentals'];

And an interface like that

interface Result { month: string; totalMonth: number; amount: number[]; }

An array of Result to which I call result will be the one that will contain the data that we pass to the template. For loop in the result I use the template form of the *ngFor as follows

<template ngFor let-entry [ngForOf]="result"> <ion-list inset> <ion-item> <ion-label class="month">{{ entry.month }}</ion-label>
<ion-label class="expense">{{entry.totalMonth }}$</ion-label> </ion-item> <ion-list inset> <ion-item *ngFor="let expense of entry.amount; let i = index"> <ion-label class="entry">{{ expenses[i] }}</ion-label> <ion-label class="expenseentry">{{ expense }}$</ion-label> </ion-item> </ion-list> </ion-list> </template>

How say here ngForOf is a directive that instantiate a template once per item for an iterable.  For each iteration the entry variable contains an Result, in the first list, the month list, we take the name of the month and the total expense.
Next, we create another list, the expenses list. For that the Result contains an array with the amount of the expense but not the name. The name is in another array (the cause that it is in another array and not in the Result that's not the point) so we will use another value of ngForOf. It is the value index, this value corresponds to index of the current item in the iterable. For obtain the values of the amount array we use an *ngFor, the difference is the introduction of the value index that we use for obtain the name of the expense.

And that's all, our nested list is ready.

dilluns, 8 de maig del 2017

Ionic 2, using ActionSheetController

Recently I have found myself in a situation that has led me to choose between different options. I wanted edit an entry in the database, first I just wanted to modify two fields and use an AlertController for that. Then I thought that I could modify a third field and here appears the problem, this field only can be modified using a checkbox input and a checkbox input can't be mixed with a text input.
For solve this, my first idea was make a new page and use components <ion-select> and <ion-input> but I think it's more natural realize the operation in the same page so that I need  to use an AlertController. For that I have divided the task in two AlertController, one with two text inputs and one with the checkbox input. And for show the two options to user an ActionSheetControllerHere you can find more info.

The first step is import the ActionSheetController  from ionic-angular in our controller
   
   import { NavController, AlertSheetController } from 'ionic-angular'

Next we add the ActionSheetController how a parameter in the constructor
   
   constructor(public navCtrl: NavController, 
               public alertCtrl: AlertSheetController) {

   }   


And now we create a function to present the alert to the user

  showMenu(entry, expense) {
  
    let optionsMenu = this.actionSheetCtrl.create({
      title: '',
      buttons: [
        {
          text: 'Edit entry',
          role: 'edit',
          handler: () => {
            
             this.editEntry(entry, expense);
          }
        },
        {
          text: 'Move to',
          role: 'move',
          handler: () => {
            
             this.moveEntry(entry, expense);
            
          }
        },
        {
          text: 'Cancel',
          role: 'cancel',
          handler: () => {
            optionsMenu.dismiss();
          }
        }
      ]
    });

    optionsMenu.present();
  }

And two functions editEntry(entry, expense) and moveEntry(entry, expense) with his correspondant AlertController to realize the task. 

The result looks like