F. Hémery
Janvier 2017
Dans les TP précédents nous avons utilisé le langage java pour écrire des applications à destination de machines équipées du système android. Le travail doit être adapté si la machine cible n'est pas équipée du système android.
Quand c'est possible, on écrira une application web responsive, une application web qui va pouvoir être consultée quel que soit le client. Dans ce cas l'application est affichée par le navigateur de la cible.
Il existe un dernier type de développement à destination de cibles hétérogènes (android, ios, ...), qui l'on nomme applications hybrides. Les applications hybrides sont écrites en utilisant le langage du web (html, css, javascript). Elles sont compilées en spécifiant le système cible (android, ios, ...). Cette étape permet d'améliorer l'efficacité de l'application, de s'approcher du thème spécifique de chaque cible et permet surtout l'utilisation des capteurs et outils qui sont disponibles sur la cible.
Dans la suite du TP nous allons utiliser un framework javascript qui permet de développer des applications à destination des smartphones et tablettes à partir des langages du web.
Le framework ionic propose des objets graphiques adaptés aux cibles visées. Il utilise un autre framework javascript angular js.
Le framework ionic peut être récupéré à partir du site ici.
Plus simplement, pour créer une application utilisant le framework ionic, on utilise un interpréteur de commandes qui permet, entre autres possibilités, de créer un projet de base. Par exemple la commande suivante permet de créer un projet.
ionic start tpIonic blank --v2
start
demande de créer un nouveau projertpIonic
le nom du projet et le répertoire dans lequel sera stocké le projetblank
crée un projet avec une seule pagetabs
crée un projet avec 3 classeurs (tabs)sidemenu
crée un projet avec un menu swipabletutorial
crée un projet pour suivre le tutoriel proposé ici.La commande va créer un répertoire tpIonic
et récupèrer les librairies nécessaires pour le développement.
Il est possible de tester l'application avec la commande :
cd tpIonic
puis
ionic serve
La commande compile le projet et le charge dans un serveur web de développement. Vous pouvez visualiser le résultat dans votre navigateur à l'adresse suivante : http://localhost:8100/
Le point de départ se trouve dans le fichier src/index.html
et plus particulièrement la balise
<ion-app> </ion-app>
Le style de l'application
<link href="build/main.css" rel="stylesheet">
est le résultat d'une compilation des différents fichiers de style déclarés dans le projet.
Le code javascript se trouve dans le fichier
<script src="build/main.js"></script>
résultat de la compilation du code javascript contenu dans le répertoire src
et des librairies tiers utilisées.
La librairie cordova
<script src="cordova.js"></script>
permet d'exécuter l'application sur le smartphone cible.
Le point d'entrée du code javascript se trouve dans le fichier
src/app/app.module.ts
Il s'agit d'un code qui suit la syntaxe typescript (ES6/ES7) qui est transformé en javascript ES5 utilisé par les navigateurs actuels. L'application est composée de modules.
@NgModule({
declarations: [MyApp,HelloIonicPage, ItemDetailsPage, ListPage],
imports: [IonicModule.forRoot(MyApp)],
bootstrap: [IonicApp],
entryComponents: [MyApp,HelloIonicPage,ItemDetailsPage,ListPage],
providers: []
})
export class AppModule {}
Un module est une partie autonome dans l'application. Chaque application désigne un module racine. Un module déclare des composants (component), qui représentent l'interface utilisateur.
@Component({
templateUrl: 'app.html'
})
export class MyApp {
rootPage = HomePage;
constructor(platform: Platform) {
platform.ready().then(() => {
StatusBar.styleDefault();
Splashscreen.hide();
});
}
}
Un composant est une classe javascript (ES6) avec l'annotation @Component
qui permet de préciser le fichier html
(app.html
) qui décrit l'interface.
<ion-nav [root]="rootPage"></ion-nav>
Dans la page app.html
, on utilise la balise ionic <ion-nav>
qui est un conteneur. L'instruction [root]="rootPage"
est interprétée par le framework angular js, elle affecte à la directive root
l'expression qui se trouve à l'intérieur des gullemets (ici la variable rootPage
). Si on examine le code du composant app.component.ts
, la variable rootPage
vaut HomePage
(rootPage = HomePage;
). Le conteneur va afficher le composant HomePage
(dans le fichier src/page/home.ts
).
L'interpréteur de commandes ionic permet d'ajouter de nouvelles pages. Nous allons l'utiliser pour ajouter une page qui affiche une liste.
En tapant la commande
ionic g page Courses
dans le répertoire racine du projet, on ajoute le répertoire courses
dans le répertoire pages
. Le répertoire courses
contient 3 fichiers :
courses.html
l'interface utilisateurcourses.scss
le style de la pagecourses.ts
le code javascript de la pageModifier le fichier courses.ts
avec le code suivant :
import { Component } from '@angular/core';
import { NavController} from 'ionic-angular';
@Component({
selector: 'page-courses',
templateUrl: 'courses.html'
})
export class CoursesPage {
courses: any;
constructor(public navCtrl: NavController) {
this.courses = [
'Pain',
'lait',
'Fromage',
'Saucisse',
'Pommes',
'Bananes',
'Vin',
'Chocolat',
'Avocat',
'Moutarde',
'Gateaux au beurre',
'Mouchoirs'
];
}
}
Modifier le fichier courses.html avec le code suivant:
<ion-header>
<ion-navbar>
<ion-title>Courses</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item *ngFor="let aliment of courses">{{aliment}}</ion-item>
</ion-list>
</ion-content>
La navigation se fera à l'aide d'onglets. Modifier le fichier home.ts
pour déclarer le premier onglet (courses) en utilisant le code suivant:
import { NavController } from 'ionic-angular';
import {CoursesPage} from "../courses/courses";
@Component({
selector: 'page-home',
templateUrl: 'home.html'
})
export class HomePage {
tabCoursesRoot: any = CoursesPage;
constructor(public navCtrl: NavController) {
}
}
Nous allons modifier le fichier home.html
avec le code suivant:
<ion-tabs>
<ion-tab [root]="tabCoursesRoot" tabTitle="Courses" tabIcon="basket"></ion-tab>
</ion-tabs>
Il faut ensuite déclarer le nouveau composant dans le module (fichier app.module.ts
), en utilisant le code suivant:
import {NgModule, ErrorHandler} from "@angular/core";
import {IonicApp, IonicModule, IonicErrorHandler} from "ionic-angular";
import {MyApp} from "./app.component";
import {HomePage} from "../pages/home/home";
import {CoursesPage} from "../pages/courses/courses";
@NgModule({
declarations: [
MyApp,
HomePage,
CoursesPage,
],
imports: [
IonicModule.forRoot(MyApp)
],
bootstrap: [IonicApp],
entryComponents: [
MyApp,
HomePage,
CoursesPage
],
providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}]
})
export class AppModule {
}
La page http://localhost:8100/
dans le navigateur doit afficher la liste des courses.
Nous allons refaire la même opération pour gérer une liste de notes que l'on pourra modifier.
ionic g page Notes
Modifier le fichier notes.ts
avec le code suivant:
import { Component } from '@angular/core';
import { NavController, AlertController } from 'ionic-angular';
@Component({
selector: 'page-notes',
templateUrl: 'notes.html'
})
export class NotesPage {
notes: any = [];
constructor(public navCtrl: NavController, public alertCtrl: AlertController) {
}
addNote(){
let prompt = this.alertCtrl.create({
title: 'Ajoute Note',
inputs: [{
name: 'title'
}],
buttons: [
{
text: 'Annule'
},
{
text: 'Ajoute',
handler: data => {
this.notes.push(data);
}
}
]
});
prompt.present();
}
editNote(note){
let prompt = this.alertCtrl.create({
title: 'Edite Note',
inputs: [{
name: 'title'
}],
buttons: [
{
text: 'Annule'
},
{
text: 'Sauve',
handler: data => {
let index = this.notes.indexOf(note);
if(index > -1){
this.notes[index] = data;
}
}
}
]
});
prompt.present();
}
deleteNote(note){
let index = this.notes.indexOf(note);
if(index > -1){
this.notes.splice(index, 1);
}
}
}
Modifier le fichier notes.html
avec le code suivant:
<ion-header>
<ion-navbar>
<ion-title>Notes</ion-title>
<ion-buttons end>
<button ion-button icon-only (click)="addNote()"><ion-icon name="add"></ion-icon></button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item-sliding *ngFor="let note of notes">
<ion-item>
{{note.title}}
</ion-item>
<ion-item-options>
<button ion-button icon-only (click)="editNote(note)" light>
<ion-icon name="paper"></ion-icon>
</button>
<button ion-button icon-only (click)="deleteNote(note)" danger>
<ion-icon name="trash"></ion-icon>
</button>
</ion-item-options>
</ion-item-sliding>
</ion-list>
</ion-content>
Modifier les fichiers home.ts
, home.html
(icon paper
) et app.component.ts
pour intégrer cette nouvelle page.
Nous avons maintenant une application avec plusieurs pages. Il a été possible de tester son fonctionnement à partir d'un navigateur, nous allons maintenant la tester sur une cible.
Il faut que notre développement est accès à l'environnement de développement android (SDK). Pour cela il faut donner une valeur à la variable d'environnement ANDROID_HOME
à l'aide de la commande suivante :
export ANDROID_HOME=/usr/local/Android/Sdk
Ajouter une nouvelle architecture cible, par exemple, android
, taper la commande suivante:
ionic platform add android
Gestion du proxy et de l'API des machines de TP
Dans l'étape qui suit, la commande gradle
qui permet de construire l'application pour la cible android à besoin d'un accès sur le web. Vous devez ajouter le fichier gradle.properties
dans le répertoire platforms/android
systemProp.http.proxyHost=cache-etu.univ-artois.fr
systemProp.https.proxyPort=3128
org.gradle.jvmargs=-Xmx1536m
systemProp.https.proxyHost=cache-etu.univ-artois.fr
systemProp.http.proxyPort=3128
Vous devez modifier les fichiers platforms/android/AndroidManifest.xml
, platforms/android/project.properties
et platforms/android/CordovaLib/project.properties
. Dans les trois fichiers indiqués, il faut remplacer la référence à l'API 25 par une référence à l'API 24 qui est actuellement installée sur les machines de TP.
Pour platforms/android/project.properties
et platforms/android/CordovaLib/project.properties
:
...
target=android-24
et pour platforms/android/AndroidManifest.xml
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="24" />
Lancer l'émulateur avec la commande:
ionic emulate android
Nous avons pu réaliser une application qui s'exécute sur un smartphone qui utilise le système android, on aurait pu faire la même chose sur un smartphone iOS , Windows Phone ou encore BlackBerry OS.
Dans cette section, nous allons créer un service pour nous permettre de récupérer des données en local ou encore sur le réseau. Les données sont stockées dans le fichier src/assets/data/donnees.json
, vous pouvez récupérer le contenu de ce fichier ici.
Nous allons créer un service (une classe qui n'a pas d'interface graphique) qui aura pour tâche d'accéder aux données stockées sur la cible (par exemple les préférences de l'utilisateur). L'interpréteur ionic permet la création d'un service
ionic g provider iBookData
Stockage des données en local
Nous allons utiliser un fichier de données que nous allons lire à la demande. Le fichier se trouve sur moodle donnees.json
. Créez un répertoire data
dans le répertoire assets
et placez le fichier de données dans le répertoire data
.
Modification du fichier i-book-data
Pour récupérer les données dynamiquement, nous allons créer une méthode dans le fichier providers/i-book-data.ts
. Voici le code à ajouter pour la méthode getLocalData()
:
getLocalData():Observable<any> {
return this.http.get("assets/data/donnees.json").map(res => res.json().Books);
}
Modification du fichier app-module.ts
il faut ajouter IBookData
dans la liste des providers
// ...
providers: [{provide: ErrorHandler, useClass: IonicErrorHandler},
IBookData]
// ...
livres
.La commande suivante (déjà utilisée) permet d'ajouter une nouvelle page
ionic g page livres
modifier le fichier les fichiers home.ts
home.html
et app.component.ts
pour pouvoir accéder à la page livres.
export class HomePage {
tabCoursesRoot: any = CoursesPage;
tabNotesRoot: any = NotesPage;
tabLivresRoot: any = LivresPage;
}
<ion-tabs>
<ion-tab [root]="tabCoursesRoot" tabTitle="Courses" tabIcon="basket"></ion-tab>
<ion-tab [root]="tabNotesRoot" tabTitle="Notes" tabIcon="paper"></ion-tab>
<ion-tab [root]="tabLivresRoot" tabTitle="Livres" tabIcon="book"></ion-tab>
</ion-tabs>
puis modifier la page livres.ts pour pouvoir récupérer les données
@Component({
selector: 'page-livres',
templateUrl: 'livres.html'
})
export class LivresPage {
livres: any;
constructor(public navCtrl: NavController, public navParams: NavParams, private iBookData: IBookData) {
iBookData.getLocalData().subscribe(data => {
this.livres = data;
});
}
ionViewDidLoad() {
console.log('ionViewDidLoad LivresPage');
}
}
et le fichier livres.html
<ion-header>
<ion-navbar>
<ion-title>livres</ion-title>
</ion-navbar>
</ion-header>
<ion-content padding>
<ion-list no-lines>
<button detail-none ion-item *ngFor="let livre of livres; let i = index;">
<ion-avatar item-left>
<img [src]="livre.Image">
</ion-avatar>
<h2>{{livre.Title}}</h2>
<p>{{livre.SubTitle}}</p>
</button>
</ion-list>
</ion-content>
Nous allons reprendre la page notes qui permettait de gérer une liste de notes sans pouvoir la conserver d'une exécution à l'autre. Pour cela il faut importer un nouveau module Storage
proposé par le framework ionic. Ce module choisit une solution de stockage local en fonction de la cible sur laquelle l'application s'exécute. Si l'application est exécutée dans un navigateur, les données seront stockées par lui en local. Si l'application est exécutée sur un smartphone, c'est le SGBD SQLite
qui sera utilisé. Le choix se fait sans que le programmeur est quoi que ce soit à prévoir.
ionic g provider storage-data
Création d'un nouveau type de données Note
Nous allons définir la structure des données qui va permettre de stocker les notes. Dans un répertoire entities
nous allons créer un fichier Note.js
qui va définir la structure. Ici la structure de données se résume à une seule propriété.
export interface Note {
description: string;
}
Modification du fichier storage-data.js
en utilisant le code suivant :
import {Injectable} from "@angular/core";
import {Storage} from "@ionic/storage";
import {Observable} from "rxjs";
import {Note} from "../entities/Note";
@Injectable()
export class StorageData {
constructor(public storage: Storage) {
console.log('Hello Storage Provider');
}
getNotes(): Observable<any[]> {
return Observable.fromPromise(this.storage.get('notes'))
.map(data => {
console.log(data);
if (data !== null)
return this.mapNotes(data);
else {
let notes: Note[] = [];
return notes;
}
});
}
mapNotes = (r: string): Note[] => {
return JSON.parse(r).map(this.toNote);
}
toNote = (r: any): Note => {
let note = <Note>({
description: r.description,
});
return note;
}
save(note): Observable<Note[]> {
let newNote = JSON.stringify(note);
return Observable.fromPromise(this.storage.set('notes', newNote))
.map(this.mapNotes);
}
}
Pour créer la nouvelle page, taper la commande suivante :
ionic g page notesPlus
Après avoir modifier les fichiers home.ts
et home.html
:
Modifier le code de la classe notes-plus.ts
.
import {Component, OnInit} from "@angular/core";
import {NavController, AlertController} from "ionic-angular";
import {StorageData} from "../../providers/storageData";
import {Note} from "../../entities/Note";
@Component({
selector: 'page-notes-plus',
templateUrl: 'notes-plus.html'
})
export class NotesPlusPage implements OnInit {
notes: Note[] = [];
constructor(public navCtrl: NavController, public alertCtrl: AlertController, private storageData: StorageData) {
}
addNote() {
let prompt = this.alertCtrl.create({
title: 'Ajoute Note',
inputs: [{
name: 'description',
placeholder: 'Description'
}],
buttons: [
{
text: 'Annule'
},
{
text: 'Ajoute',
handler: data => {
console.log(data);
this.notes.push(data);
this.storageData.save(this.notes);
}
}
]
});
prompt.present();
}
editNote(note) {
let prompt = this.alertCtrl.create({
title: 'Edite Note',
inputs: [{
name: 'description',
placeholder: 'Description'
}],
buttons: [
{
text: 'Annule'
},
{
text: 'Sauve',
handler: data => {
let index = this.notes.indexOf(note);
if (index > -1) {
this.notes[index] = data;
this.storageData.save(this.notes);
}
}
}
]
});
prompt.present();
}
deleteNote(note) {
let index = this.notes.indexOf(note);
if (index > -1) {
this.notes.splice(index, 1);
this.storageData.save(this.notes);
}
}
ngOnInit(): void {
this.storageData.getNotes().subscribe((data) => {
this.notes = data;
if (this.notes == null) {
this.notes = [];
}
});
}
ionViewDidLoad() {
console.log('ionViewDidLoad NotesPlusPage');
}
}
Modifier le fichier notes-plus.html
.
<ion-header>
<ion-navbar>
<ion-title>NotesPlus</ion-title>
<ion-buttons end>
<button ion-button icon-only (click)="addNote()">
<ion-icon name="add"></ion-icon>
</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content padding>
<ion-list>
<ion-item-sliding *ngFor="let note of notes">
<ion-item>
{{note.description}}
</ion-item>
<ion-item-options>
<button ion-button icon-only (click)="editNote(note)" light>
<ion-icon name="paper"></ion-icon>
</button>
<button ion-button icon-only (click)="deleteNote(note)" danger>
<ion-icon name="trash"></ion-icon>
</button>
</ion-item-options>
</ion-item-sliding>
</ion-list>
</ion-content>
app/app.module.ts
Modifier le fichier app/app.module.ts
avec le code suivant :
import {NgModule, ErrorHandler} from "@angular/core";
import {IonicApp, IonicModule, IonicErrorHandler} from "ionic-angular";
import {IonicStorageModule} from "@ionic/storage";
import {MyApp} from "./app.component";
import {HomePage} from "../pages/home/home";
import {CoursesPage} from "../pages/courses/courses";
import {NotesPage} from "../pages/notes/notes";
import {LivresPage} from "../pages/livres/livres";
import {IBookData} from "../providers/i-book-data";
import {NotesPlusPage} from "../pages/notes-plus/notes-plus";
import {StorageData} from "../providers/storageData";
@NgModule({
declarations: [
MyApp,
HomePage,
CoursesPage,
NotesPage,
NotesPlusPage,
LivresPage,
],
imports: [
IonicModule.forRoot(MyApp),
IonicStorageModule.forRoot()
],
bootstrap: [IonicApp],
entryComponents: [
MyApp,
HomePage,
CoursesPage,
NotesPage,
NotesPlusPage,
LivresPage,
],
providers: [{provide: ErrorHandler, useClass: IonicErrorHandler},
Storage,
StorageData,
IBookData]
})
export class AppModule {
}
Il est parfois nécessaire de récupérer des données sur le web, pour cela l'application va lancer une requête vers un service qui est à l'écoute. Dans notre exemple nous allons utiliser le site it-book qui met à la disposition de ses utilisateurs une api REST permettant d'intérroger sa base de données.
i-book-data.ts
avec le code suivant : import {Injectable} from "@angular/core";
import {Http, Response} from "@angular/http";
import "rxjs/add/operator/map";
import {Observable} from "rxjs";
@Injectable()
export class IBookData {
_page: number;
_requete: string;
constructor(public http: Http) {
console.log('Hello IBookData Provider');
this._requete = encodeURI("javascript json");
this._page = 1;
}
getLocalData(): Observable<any> {
let url = "assets/data/donnees.json";
console.log("URL = " +url);
return this.http.get("assets/data/donnees.json").map(res => res.json().Books);
}
getRemoteData(): Observable<any> {
let url = "http://it-ebooks-api.info/v1/search/"+this._requete+"/page/" + this._page;
console.log("URL = "+url);
return this.http.get(url).map(res => {
// console.log(res);
let data = [];
try {
data = res.json().Books;
if (data === undefined)
data = [];
} catch (e) {
console.log("Erreur connexion : " + e.toString());
}
return data;
}).catch(this.handleError);
}
private handleError (error: Response | any) {
let errMsg: string;
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error("Message d'erreur: " + errMsg);
return Observable.throw(errMsg);
}
incPage() {
this._page++;
}
decPage() {
if (this._page > 1)
this._page--;
}
setRequete(r: string) {
this._requete = encodeURI(r);
this._page = 1;
}
}
Pour créer la nouvelle page, taper la commande suivante :
ionic g page livresPlus
Après avoir modifier les fichiers home.ts
et home.html
:
Modifier le code de la classe livres-plus.ts
.
import {Component, OnInit} from "@angular/core";
import {NavController, NavParams, AlertController} from "ionic-angular";
import {IBookData} from "../../providers/i-book-data";
@Component({
selector: 'page-livres-plus',
templateUrl: 'livres-plus.html'
})
export class LivresPlusPage implements OnInit{
livres: any;
constructor(public navCtrl: NavController, public navParams: NavParams, public alertCtrl: AlertController, private iBookData: IBookData) {
}
doInfinite(infiniteScroll) {
this.iBookData.incPage();
this.iBookData.getRemoteData().subscribe(data => {
console.log("livres : ", data);
if (data.length > 0)
for (let livre of data)
this.livres.push(livre);
infiniteScroll.complete();
});
}
ngOnInit(): void {
this.iBookData.getRemoteData().subscribe(data => {
console.log(data);
this.livres = data;
});
}
ionViewDidLoad() {
console.log('ionViewDidLoad LivresPlusPage');
}
changeRequest() {
let prompt = this.alertCtrl.create({
title: 'Requête',
inputs: [{
name: 'requete'
}],
buttons: [
{
text: 'Annule'
},
{
text: 'Recherche',
handler: data => {
console.log(data);
this.iBookData.setRequete(data.requete);
this.ngOnInit();
}
}
]
});
prompt.present();
}
}
Modifier le code de la classe livres-plus.html
.
<ion-header>
<ion-navbar>
<ion-title>Livres Plus</ion-title>
<ion-buttons end>
<button ion-button icon-only (click)="changeRequest()"><ion-icon name="search"></ion-icon></button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content padding>
<ion-list no-lines>
<button detail-none ion-item *ngFor="let livre of livres; let i = index;">
<ion-avatar item-left>
<img [src]="livre.Image">
</ion-avatar>
<h2>{{livre.Title}}</h2>
<p>{{livre.SubTitle}}</p>
</button>
</ion-list>
<ion-infinite-scroll (ionInfinite)="doInfinite($event)">
<ion-infinite-scroll-content></ion-infinite-scroll-content>
</ion-infinite-scroll>
</ion-content>