Débutant

Ionic 3 – Créez votre application mobile

Vous maîtrisez les technos du web et souhaitez développer une application mobile ? Ne cherchez plus ! Ionic est fait pour vous.

Ionic est un Framework permettant la création d’applications cross-platform rapidement et facilement en utilisant des technologies web (JavaScript, HTML, CSS). Il s’appuie sur Angular pour la partie web et sur Cordova pour la partie native.

Dans cet article, nous allons apprendre les bases de l’utilisation d’Ionic. Nous allons créer une application from scratch qui nous permettra de faire un tour des fonctionnalités de ce Framework. Vous trouverez sur Github tout le code présenté dans cet article.

Installation et mise en route du projet

Prérequis

  • Notions en TypeScript/JavaScript
  • Notions d’Angular (v2+)
  • Notions sur npm, installé avec Node.js

Installation

Tout d’abord, il nous faut installer Ionic avec Cordova et ce en utilisant npm => npm install -g cordova ionic

Création du projet

Ionic nous met à disposition une CLI (Commande Line Interface) pour, entre autres, la création d’un projet à partir d’un template contenant toutes les dépendances nécessaires pour démarrer => ionic start ionic3-movie-app sidemenu

Cette commande nous permet de créer le projet “ionic3-movie-app” en se basant sur le template sidemenu.

Pour lancer l’application, il suffit de se mettre dans le dossier et de lancer la commande ionic serve. Chrome DevTools fournit un outil nous permettant de simuler la taille des écrans sur mobile, ce qui est plus pratique pour tester notre application :

Il est aussi possible de déployer l’application sur un mobile / émulateur Android mais pour cela il faut installer le SDK et configurer le path. La démarche étant assez fastidieuse et au-delà du scope de ce tuto, vous trouverez toutes les infos nécessaires ici.

Une fois le SDK installé et votre téléphone branché en USB, voici les démarches à suivre pour lancer l’application sur ce dernier :

  • Ajouter Android comme plateforme : ionic plateform add android
  • Lancer le build : ionic build android
  • Lancer l’application : ionic cordova run android (la fonction débogage doit être activée)

Vous pouvez aussi créer des appareils virtuels grâce à l’AVD Manager et émuler l’application avec la commande ionic cordova emulate android.

Au moment où j’écris ces lignes, un nouvel outil est sorti, Ionic DevApp, qui permet de lancer et de débugger l’application sans avoir à installer des SDK complémentaires.

Pour ce tuto, un navigateur sera amplement suffisant.

Anatomie d’un projet Ionic

Ionic se base sur les components, ce qui veut dire que chaque page est vue comme un composant individuel avec son propre habillage et comportement. Cela facilite la maintenabilité du projet vu que tout le code se trouve au niveau du composant.

Voici la structure de notre projet :

/

À la racine de notre projet, nous avons des fichiers de configuration dont les plus importants sont :

  • config.xml : utilisé par Cordova ; contient la configuration essentielle au build de l’application pour les plateformes iOS et Android.
  • package.json : il s’agit du fichier de config de npm, contenant des informations sur le projet (nom, version, auteur, licence…) et ses dépendances en terme de packages npm.

src/

Le code du projet vit ici. C’est ici que nous allons créer des pages et des services. Ce dossier contient plusieurs sous-dossiers :

  • src/app : contient le composant racine (root component), point d’entrée de notre application où l’on définit notre root page, déclare nos modules, etc.
  • src/assets: contient tous les assets statiques, que ce soient des images ou des données en JSON.
  • src/pages : contient toutes les pages de l’application. Ionic CLI génèrera automatiquement les pages dans ce dossier
  • src/provider : contient les services (appelés ici provider). Ionic CLI génèrera automatiquement les providers dans ce dossier.
  • src/theme : contient les variables SCSS pour les couleurs de l’application, etc.

www/

C’est le dossier web qui contient le code compilé de notre application.

resources/

Contient les ressources pour les plateformes Android et iOS, notamment les images utilisées pour le splash screen et les icônes.

Maintenant que tout est installé et que la structure de projet d’Ionic n’a plus aucun secret pour nous, nous pouvons commencer à créer nos premières pages.

Création des pages

Notre application contiendra notre home page qui affichera nos films favoris, une page pour afficher la liste des films et une autre page pour le détail du film (image, description, note, trailer…). On peut choisir nos films préférés à partir de la liste et stocker le tout dans une base de données locale. On aura le résultat final suivant :

Ionic CLI nous permet de générer nos pages avec la commande ionic g page page-name.

Si vous avez déjà utilisé Angular CLI, vous remarquerez que la commande est assez similaire. Pour plus d’informations sur les possibilités de génération de fichiers, je vous invite à lire la doc d’Ionic CLI.
Nous allons donc générer nos fichiers pour chaque page :

ionic g page my-movies
ionic g page movie-list
ionic g page movie-detail

Pour chaque page, nous aurons 4 fichiers : Le template (.html), la feuille de style (.scss), le module (module.ts), et la page (.ts). Après cela il faut déclarer les modules dans le tableau des imports de app.module.ts

@NgModule({
  declarations: [MyApp],
  imports: [BrowserModule, IonicModule.forRoot(MyApp),  MyMoviesPageModule, MovieListPageModule, MovieDetailPageModule],
  bootstrap: [IonicApp],
  entryComponents: [MyApp],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: ErrorHandler, useClass: IonicErrorHandler }
  ]
})

Maintenant que nous avons créé nos pages, il faut leur mettre du contenu et naviguer vers ces pages. Mais avant cela il faut que je vous parle du système de navigation d’Ionic.

Navigation Stack

Principe

Pour naviguer entre les vues dans Angular 4, on utilise un système de routage. Dans Ionic, on utilise plutôt une pile de navigation (navigation stack), ce qui implique de mettre des vues dans la pile (push) et de les enlever (pop) comme décrit sur le schéma suivant :

C’est toujours la page se trouvant en haut de la pile qui sera active :

Ici on est sur la root page et l’on souhaite naviguer vers la Page 1 : on la push dans la stack et vu qu’elle se trouve maintenant en haut de la pile, elle sera donc affichée à l’utilisateur. Si l’on souhaite revenir en arrière, on pop la page de la stack et c’est la root page qui sera affichée.

Les vues sont créées lorsqu’elles sont ajoutées à la stack de navigation. Par défaut, tant qu’elles y sont, elles sont mises en cache et laissées dans le DOM. Elles seront détruites dès qu’elles sont supprimées de la stack.

En pratique

La navigation se fait avec le NavController dont les 3 méthodes principales sont :

  • push() : permet d’insérer la vue dans la stack. Si la page contient un ion-navbar, un bouton “Retour” sera automatiquement ajouté. On peut aussi passer des paramètres qui seront récupérés grâce au module NavParams.
  • pop() : permet de supprimer la vue de la stack et de naviguer vers la vue précédente.
  • popToRoot() : permet de supprimer toutes les vues de la stack et de naviguer vers la root page.

Il est souvent nécessaire d’exécuter des tâches au chargement de la page (rafraîchissement du contenu, etc.) ou lorsque l’on quitte la page active (désabonnement aux observables, stockage de données, etc.). C’est là qu’entrent en jeu les lifecycle events.

Lifecycle events

Les lifecycle events, comme leur nom l’indique, sont des événements nous informant de l’état d’une page Ionic durant tout son cycle de vie, de sa création à sa destruction. Certains ne sont lancés qu’une seule fois tandis que d’autres sont lancés à chaque fois que la page concernée est active.

  • ionViewDidLoad : se lance pour signaler que toutes les variables et dépendances sont prêtes à l’emploi, ce qui implique aussi que la page a été ajoutée en mémoire et qu’elle sera en cache. Cet événement n’est lancé qu’une seule fois. C’est ici que l’on met le code d’initialisation et de configuration de la page.
  • ionViewWillEnter : même si la page est complètement chargée, ce n’est pas encore la page active et elle n’est pas encore visible pour l’utilisateur, et Ionic exécute encore des tâches en background. Néanmoins, on peut manipuler les éléments de la page avant qu’elle ne soit affichée. À utiliser pour les actions que l’on souhaite réaliser à chaque fois que l’on affiche la page (mise à jour d’une table…).
  • ionViewDidEnter : cette fonction nous signale que tout s’est bien passé et que notre vue a été correctement affichée. Mais à quoi sert-elle et quand l’utiliser ? Eh bien, c’est l’endroit où l’on déclenche une fonctionnalité de l’application que nous souhaitons montrer à l’utilisateur à l’affichage de la page (animations, changelog de l’appli….).
  • ionViewWillLeave : se lance lorsque l’on est sur le point de quitter la page. Elle est toujours considérée comme étant la page active mais elle est sur le point d’être supprimée.
  • ionViewDidLeave : cet événement nous signale que la page n’est plus la page active. La fonction se lance après le ionViewDidEnter() de la page suivante. Mais que faire de cette fonction ? C’est un très bon endroit pour sauvegarder des données ou pour lancer des opérations en background qui n’ont pas besoin que la vue soit visible.
  • ionViewWillUnload : c’est la dernière fonction à se lancer dans le cycle de vie d’une page. Elle ne se lance qu’une seule fois, et seulement quand on navigue en arrière. Par exemple si l’on navigue de la Page 1 à la Page 2, l’événement ne se lancera pas pour la Page 1 car elle est dans le cache (souvenez-vous de la stack de navigation), par contre si l’on fait un retour en arrière de la Page 2 vers la Page 1, cet événement se lancera pour la Page 2, car cette page n’est plus en mémoire ; elle sera donc déchargée (unloaded).

Pour plus d’informations, je vous conseille de lire la doc sur le NavController à la rubrique “Lifecycle events”.

Appliquons ensemble les concepts que nous venons d’apprendre à notre projet.

Application V1.0

Création de la root page

Première chose à faire : déclarer notre root page (page de démarrage) dans app.component.ts. Dans notre cas, il s’agit de MyMoviesPage :

import { MyMoviesPage } from "../pages/my-movies/my-movies";
import { Component, ViewChild } from "@angular/core";
import { Nav, Platform } from "ionic-angular";
import { StatusBar } from "@ionic-native/status-bar";
import { SplashScreen } from "@ionic-native/splash-screen";

@Component({
  templateUrl: "app.html"
})
export class MyApp {
  @ViewChild(Nav) nav: Nav;

        rootPage  = MyMoviesPage;
}

Ensuite, mettons du contenu dans my-movies.html :

<ion-header>
  <ion-navbar color="primary">
    <button menuToggle ion-button icon-only>
      <ion-icon name="menu"></ion-icon>
    </button>
    <ion-title>Movie App</ion-title>
  </ion-navbar>

  <ion-toolbar color="secondary">
    <ion-title>My favorite movies</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-card>
    <ion-card-content>
      <p>You haven’t selected any movie</p>
      <p>Please select one by using the button below</p>
      <button ion-button icon-left full (click)="findMovie()">
        <ion-icon name="search"></ion-icon>
        On going movies
      </button>
    </ion-card-content>
  </ion-card>
</ion-content>

J’ai espacé le code HTML pour bien voir les différentes parties de la page :

  • Header contient un ion-navbar qui est notre barre de navigation avec un hamburger button pour afficher le menu latéral. Il a comme titre le nom (très original) de notre application “Movie App”, mais vu que notre home page contiendra nos films favoris, j’ai donc ajouté, en dessous, un ion-toolbar avec le nom de la page dessus. Vous avez sûrement remarqué les attributs color avec les valeurs “primary” et “secondary” ; ces valeurs sont configurables dans theme/variables.scss.
  • Content est notre body : c’est ici que nous mettrons notre contenu. J’ai choisi d’utiliser ion-card mais vous pouvez utiliser ce que vous souhaitez. Le bouton contient une icône avec du texte, et j’utilise de l’event binding pour faire appel à ma fonction.

Pour plus d’informations sur les composants Ionic, je vous invite à lire la doc qui est assez bien faite et possède pas mal d’exemples.

Navigation dans l’application

Voici à quoi ressemblera la stack de navigation de l’application :

MyMoviesPage sera la page de démarrage. De là on peut soit accéder au détail d’un film favori en naviguant vers MovieDetailPage, soit afficher la liste des films dans MovieListPage et ensuite voir le détail d’un film.

Nous allons développer, un à un, les cas de navigation.

MyMoviesPage → MovieListPage

Dans notre template, lors du clic sur le bouton, nous faisons appel à la méthode findMovie(). Implémentons cette méthode dans MyMoviesPage :

export class MyMoviesPage {
  constructor(public navCtrl: NavController, public navParams: NavParams) {}

  ionViewDidLoad() {
    console.log("ionViewDidLoad MyMoviesPage");
  }

  findMovie() {
    this.navCtrl.push(MovieListPage);
  }
}

Avec le NavController on push MovieListPage en haut de la pile. On est donc redirigé vers cette page, et un bouton de retour y est automatiquement ajouté :

Maintenant, il nous faut ajouter du contenu dans la page Movie List :

<ion-header>
  <ion-navbar color="primary">
       <button ion-button menuToggle>
      <ion-icon name="menu"></ion-icon>
    </button>
    <ion-title>Movie List</ion-title>
  </ion-navbar>
</ion-header>


<ion-content padding>
  <ion-grid *ngIf="movies">
    <ion-row>
      <ion-col col-4 *ngFor="let movie of movies">
        <img [src]="movie.poster_path" (click)="goToDetail(movie)" />
      </ion-col>
    </ion-row>
  </ion-grid>
</ion-content>

Créons la variable movies dans movie-liste.ts et mettons-y des valeurs en les copiant depuis le fichier movies.json.

import { Component } from "@angular/core";
import { IonicPage, NavController, NavParams } from "ionic-angular";

@IonicPage()
@Component({
  selector: "page-movie-list",
  templateUrl: "movie-list.html"
})
export class MovieListPage {
  movies: IMovie[] = [
    {
      vote_count: 666,
      id: 19404,
      video: false,
      vote_average: 9.1,
      title: "Dilwale Dulhania Le Jayenge",
      popularity: 50.154262,
      poster_path:
        "https://image.tmdb.org/t/p/w185/2gvbZMtV1Zsl7FedJa5ysbpBx2G.jpg",
      original_language: "hi",
      original_title: "Dilwale Dulhania Le Jayenge",
      genre_ids: [35, 18, 10749],
      backdrop_path: "/nl79FQ8xWZkhL3rDr1v2RFFR6J0.jpg",
      adult: false,
      overview:
        "Chaudhry Baldev Singh est un père de famille installé à Londres. Un jour, il reçoit une lettre d’Inde : son meilleur ami lui écrit, lui rappellant la promesse qu’il avait faite deux décennies auparavant de marier leurs enfants. Chaudhry décide alors de tenir sa promesse, mais donne toutefois un mois libre à sa fille tout avant qu’elle ne s’en aille en Inde se marier...",
      release_date: "1995-10-20"
    },
    {
      vote_count: 8482,
      id: 278,
      video: false,
      vote_average: 8.5,
      title: "Les Évadés",
      popularity: 76.107673,
      poster_path:
        "https://image.tmdb.org/t/p/w185/5cIUvCJQ2aNPXRCmXiOIuJJxIki.jpg",
      original_language: "en",
      original_title: "The Shawshank Redemption",
      genre_ids: [18, 80],
      backdrop_path: "/xBKGJQsAIeweesB79KC89FpBrVr.jpg",
      adult: false,
      overview:
        "En 1947, Andy Dufresne, un jeune banquier, est condamné à la prison à vie pour le meurtre de sa femme et de son amant. Ayant beau clamer son innocence, il est emprisonné à Shawshank, le pénitencier le plus sévère de l’Etat du Maine. Il y fait la rencontre de Red, un Noir désabusé, détenu depuis vingt ans. Commence alors une grande histoire d’amitié entre les deux hommes...",
      release_date: "1994-09-23"
    },
    {
      vote_count: 1099,
      id: 372058,
      video: false,
      vote_average: 8.5,
      title: "Your Name",
      popularity: 57.569033,
      poster_path:
        "https://image.tmdb.org/t/p/w185/xq1Ugd62d23K2knRUx6xxuALTZB.jpg",
      original_language: "ja",
      original_title: "君の名は。",
      genre_ids: [10749, 16, 18],
      backdrop_path: "/7OMAfDJikBxItZBIug0NJig5DHD.jpg",
      adult: false,
      overview:
        "Mitsuha est une lycéenne, la fille du maire d’une petite ville nichée entre les montagnes. Vivant avec sa petite sœur et sa grand-mère, c’est une demoiselle franche qui n’hésite pas à dire qu’elle n’a pas envie de participer aux rituels shinto, ou d’aider son père dans ses campagnes électorales. En fait, elle rêve de pouvoir quitter cette ville où elle s’ennuie, pour partir tenter sa chance à la capitale...",
      release_date: "2016-08-26"
    }
  ];

  constructor(public navCtrl: NavController, public navParams: NavParams) {}

  ionViewDidLoad() {
    console.log("ionViewDidLoad MovieListPage");
  }

  goToDetail(movie: IMovie) {
  }
}

Personnellement, j’aime bien typer mes variables. Du coup, j’ai créé une interface IMovie en me basant sur la réponse JSON, en utilisant l’outil en ligne json2ts qui simplifie la tâche.

Et voilà :

Vous avez dû remarquer la fonction goToDetail(movie: IMovie) (je sais que je ne peux rien vous cacher ^^). Nous allons donc l’implémenter et voir comment récupérer des données de la stack de navigation.

MovieListPage → MovieDetail

Nous pouvons passer des paramètres suplémentaires au NavController en plus de la page. Dans notre cas, nous allons lui passer notre objet movie :

export class MovieListPage {
  constructor(public navCtrl: NavController, public navParams: NavParams) { }

  goToDetail(movie: IMovie) {
    this.navCtrl.push(MovieDetailPage, movie);
  }
}

Mettons un peu d’html dans movie-detail.html :

<ion-header>
  <ion-navbar color="primary">
    <ion-title *ngIf="movie">{{movie.title}}</ion-title>
  </ion-navbar>
</ion-header>

<ion-content>
   <ion-card *ngIf="movie">
    <img [src]="movie.poster_path" />
    <ion-card-header>
      <h2>Synopsis
        <ion-badge color="primary">{{movie.vote_average}}</ion-badge>
      </h2>
    </ion-card-header>
    <ion-card-content>{{movie.overview}}</ion-card-content>
  </ion-card>
</ion-content>

Pour afficher l’objet dans MovieDetailPage, il nous faudra le récupérer grâce à NavParam. On définit la valeur de la variable movie lors du lifecycle event ionViewDidLoad :

export class MovieDetailPage {
  movie: IMovie;

  constructor(
    public navCtrl: NavController,
    public navParams: NavParams
  ) {}

  ionViewDidLoad() {
    this.movie = this.navParams.data;
  }
}

Et voilà la première version de l’application avec une navigation du début à la fin :

Dans MovieListPage, nous avons directement initialisé notre liste en utilisant des valeurs fixes, mais ce n’est pas la meilleure chose à faire. On serait plutôt tenté de faire des appels à une WebAPI en utilisant le HttpClient d’Angular. Afin de respecter le SRP, nous allons déporter ces appels dans une autre classe et créer ce qu’Ionic appelle un provider, à rapprocher d’un service dans Angular.

Création de notre premier provider

Ionic CLI nous permet de générer des providers assez simplement grâce à la commande ionic g provider movie-api. Cela génère le fichier src/app/providers/movie-api/movie-api.ts et met à jour AppModule en déclarant MovieApiProvider dans le tableau des providers, le mettant à disposition de tous les composants de ce module. Pour plus de détails sur les services/providers et sur l’utilisation du HttpClient, vous trouverez tout ce qu’il faut dans cet article. Je vous conseille de suivre pas à pas la déclaration du module http. Je passe directement à l’implémentation du provider :

import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Platform } from "ionic-angular";
import { Observable } from "rxjs/Rx";
import { IMovie } from "../../interface/IMovie";

@Injectable()
export class MovieApiProvider {
  private baseUrl: string = "../../assets/api/movies.json";

  movies: IMovie[];

    constructor(
    private readonly http: HttpClient,
    private readonly platform: Platform
  ) {
    console.log("Hello MovieApiProvider Provider");
  }

  getMovies(): Observable {
    return this.http.get(`${this.baseUrl}`);
  }
}

Ici, nous récupérons les données à partir d’un json, mais vous pouvez bien évidemment faire des appels REST.

Si vous testez l’application sur Android, vous aurez une erreur 404, car l’emplacement de movies.json ne sera pas le même. Pour remédier à cela, il faudra modifier le constructeur comme ceci :

  constructor(
    private readonly http: HttpClient,
    private readonly platform: Platform
  ) {
    console.log("Hello MovieApiProvider Provider");
    if (this.platform.is("cordova") && this.platform.is("android")) {
      this.baseUrl = "/android_asset/www/assets/api/movies.json";
    }
  }

Une fois la méthode getMovies() créée, nous allons l’appeler dans MovieListPage et supprimer l’initialisation de notre variable movies :

export class MovieListPage {
  movies  = new Array<Movie>();

  constructor(
    public navCtrl: NavController,
    public navParams: NavParams,
    private movieApiProvider: MovieApiProvider
  ) {}

  ionViewDidLoad() {
    this.movieApiProvider.getMovies().subscribe(data =>{
      this.movies = data;
    })
  }

  goToDetail(movie: IMovie) {
    this.navCtrl.push(MovieDetailPage, movie);
  }
}

Nous avons bien évidemment utilisé le lifecycle event ionViewDidLoad pour initialiser notre variable movies à la création de la page. Vous devriez maintenant avoir une liste de films bien plus fournie que précédemment.

Gestion des favoris

Le but est de choisir des films favoris à partir de la page de détail, et de les afficher sur la page de démarrage. Commençons par modifier movie-detail.html afin d’y afficher un bouton de favoris, que nous allons mettre sur la navbar à droite :

<ion-header>
  <ion-navbar color="primary">
    <ion-title *ngIf="movie">{{movie.title}}</ion-title>
    <ion-buttons end>
      <button ion-button incon-only style="font-size: 1.7em" (click)=toggleFavorite()>
        <ion-icon [name]="isFavorite ? 'star':'star-outline'"></ion-icon>
      </button>
    </ion-buttons>
  </ion-navbar>
</ion-header>

<-- suite de la page -->

Implémentons maintenant la méthode toggleFavorite() :

export class MovieDetailPage {
  movie: IMovie;
  isFavorite: boolean = false;

  constructor(
    public navCtrl: NavController,
    public navParams: NavParams
  ) {}

  ionViewDidLoad() {
    this.movie = this.navParams.data;
  }

  toggleFavorite() {
    if (this.isFavorite) {
      this.isFavorite = false;
      // TODO persist data
    } else {
      this.isFavorite = true;
      // TODO persist data
    }
  }
}

C’est une fonction qui permet d’inverser le booléen. Maintenant, le but est de stocker le choix de l’utilisateur. Pour cela nous allons utiliser Ionic Storage.

Ionic storage

Le storage est un moyen facile de stocker des paires clé / valeur et des objets JSON. Storage utilise une variété de moteurs de stockage sous-jacents, en choisissant le meilleur disponible en fonction de la plate-forme.

Lorsqu’il est exécuté dans un contexte d’application native, Storage priorise l’utilisation de SQLite si (et seulement si) le plugin est présent dans l’application, car il s’agit d’une base de données stable et qui ne sera pas vidée en cas d’espace disque insuffisant sur le device.

Lorsqu’il s’exécute sur le Web ou sous la forme d’une application Web progressive, Storage tente d’utiliser IndexedDB, WebSQL et localstorage, dans cet ordre.

Installation

Pour cela, nous allons nous baser sur la doc. Nous allons installer le plugin SQLite :
ionic cordova plugin add cordova-sqlite-storage

Ensuite nous allons ajouter IonicStorageModule.forRoot() à la liste d’imports de app.module.ts :

@NgModule({
  declarations: [MyApp],
  imports: [
    BrowserModule,
    IonicModule.forRoot(MyApp),
    IonicStorageModule.forRoot(),
    MyMoviesPageModule,
    MovieListPageModule,
    MovieDetailPageModule,
    HttpClientModule
  ],
  bootstrap: [IonicApp],
  entryComponents: [MyApp],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: ErrorHandler, useClass: IonicErrorHandler },
    MovieApiProvider
  ]
})
export class AppModule {}

Maintenant que l’installation est finie, nous allons créer un provider pour y centraliser toutes les opérations de CRUD.

Provider pour les opérations de CRUD

Sur le même principe que pour movie-api.ts, nous allons créer un provider favorite-movie.ts où l’on écrira toute la logique liée au stockage des films :

import { Injectable } from "@angular/core";
import { Storage } from "@ionic/storage";
import { IMovie } from "../../interface/IMovie";

const MOVIE_KEY = "movie_";

@Injectable()
export class FavoriteMovieProvider {
  constructor(private storage: Storage) {
    console.log("Hello UserPreferencesProvider Provider");
  }

  addFavoriteMovie(movie: IMovie) {
    this.storage.set(this.getMovieKey(movie), JSON.stringify(movie));
  }

  removeFavoriteMovie(movie: IMovie) {
    this.storage.remove(this.getMovieKey(movie));
  }

  isFavortieMovie(movie: IMovie) {
    return this.storage.get(this.getMovieKey(movie));
  }

  toogleFavoriteMovie(movie: IMovie) {
    this.isFavortieMovie(movie).then(
      isFavorite =>
        isFavorite
          ? this.removeFavoriteMovie(movie)
          : this.addFavoriteMovie(movie)
    );
  }

  getMovieKey(movie: IMovie) {
    return MOVIE_KEY + movie.id.toString();
  }

  getFavoriteMovies(): Promise<IMovie[]> {
    return new Promise(resolve => {
      let results: IMovie[] = [];
      this.storage
        .keys()
        .then(keys =>
          keys
            .filter(key => key.includes(MOVIE_KEY))
            .forEach(key =>
              this.storage.get(key).then(data => results.push(JSON.parse(data)))
            )
        );
      return resolve(results);
    });
  }
}

Tout d’abord, nous injectons dans notre constructeur le Storage qui nous permettra de stocker, récupérer et supprimer des données. Pour plus d’informations sur les méthodes et l’utilisation de Storage, je vous conseille de lire la doc qui est assez détaillée.

Maintenant que notre provider est prêt à gérer des données, utilisons-le.

Application

Remettons-nous sur movie-detail.ts et améliorons ensemble la méthode toggleFavorite() en lui ajoutant la persistance des données. Il nous faut bien évidemment lui injecter FavoriteMovieProvider :

export class MovieDetailPage {
  movie: IMovie;
  favorite: boolean = false;

  constructor(
    public navCtrl: NavController,
    public navParams: NavParams,
    private favoriteMovieProvider: FavoriteMovieProvider
  ) {}

  ionViewDidLoad() {
    this.movie = this.navParams.data;
    this.favoriteMovieProvider
      .isFavoriteMovie(this.movie.id)
      .then(value => (this.favorite = value));
  }

  toggleFavorite(): void {
    this.isFavorite = !this.isFavorite;
    this.favoriteMovieProvider.toogleFavoriteMovie(this.movie);
  }
}

La première chose que l’on fait lors du chargement de la page est d’appeler this.favoriteMovieProvider.isFavoriteMovie(movie). Cette méthode vérifie si l’élément se trouve en base ou pas. Si c’est le cas, cela veut dire que c’est un film de la liste des favoris de l’utilisateur et donc on set la valeur de this.favorite à true.
Pour ce qui est de la persistance des données, on fait simplement appel this.favoriteMovieProvider.toogleFavoriteMovie(this.movie), qui va gérer cela.

Maintenant que les données sont stockées, il faut les afficher sur la page de démarrage.

La question à se poser est : “Quel lifecycle event vais-je utiliser ?”
Eh non, ce n’est pas ionViewDidLoad(), car la root page n’est chargée (loaded) qu’une seule fois (sauf si l’on kill l’application) et elle sera toujours en bas de la pile (stack). Dans notre cas, on souhaite mettre à jour la liste des films favoris à chaque fois que l’on entre dans la page, on utilisera alors ionViewWillEnter().

Alimentons notre liste dans my-movies.ts

export class MyMoviesPage {
  favoriteMovies: IMovie[] = [];

  constructor(
    public navCtrl: NavController,
    public navParams: NavParams,
    private favoriteMovieProvider: FavoriteMovieProvider
  ) {}

  ionViewDidLoad() {
    console.log("ionViewDidLoad MyMoviesPage");
  }

  ionViewWillEnter() {
    this.initFavoriteMovies();
  }

  private initFavoriteMovies() {
    this.favoriteMovieProvider
      .getFavoriteMovies()
      .then(favs => (this.favoriteMovies = favs));
  }

  findMovie() {
    this.navCtrl.push(MovieListPage);
  }

  goToDetail(movie: IMovie) {
    this.navCtrl.push(MovieDetailPage, movie);
  }
}

Il ne reste plus qu’à mettre à jour le template my-movies.html :

<br /><!-- Header ne change pas -->

<ion-content>

    <ion-card *ngIf="favoriteMovies && favoriteMovies.length; else noFavorite">
      <ion-list>
        <ion-list-header class="my-movies-header">Favorite Movies</ion-list-header>
        <ion-item *ngFor="let movie of favoriteMovies" (click)="goToDetail(movie)">
          <ion-row>
            <ion-col col-1>
              <ion-icon name="star"></ion-icon>
            </ion-col>
            <ion-col col-6 text-wrap>
              <h4>{{movie.title}}</h4>
            </ion-col>
            <ion-col col-3>
              <h4>{{movie.release_date}}</h4>
            </ion-col>
            <ion-col col-2>
              <ion-badge color="primary">{{movie.vote_average}}</ion-badge>
            </ion-col>
          </ion-row>
        </ion-item>
      </ion-list>
      <ion-card-content>
        <p>To choose more movies, click on this button.</p>
        <button icon-left ion-button full (click)="findMovie()">
          <ion-icon name="search"></ion-icon>
          Find a movie
        </button>
      </ion-card-content>
    </ion-card>

    <ng-template #noFavorite>
      <ion-card>
        <ion-card-content>
          <p>You haven’t selected any movie</p>
          <p>Please select one by using the button below</p>
          <button ion-button icon-left full (click)="findMovie()">
            <ion-icon name="search"></ion-icon>
            Find a movie
          </button>
        </ion-card-content>
      </ion-card>
    </ng-template>

  </ion-content>

J’ai utilisé ion-list pour afficher la liste des films, et voilà :

Conclusion

Dans ce tuto, nous avons développé une application Ionic from scratch. Nous avons appris à utiliser Ionic CLI pour la création de pages, la création de providers, le build, le lancement de l’appli… Nous avons passé en revue le principe de navigation utilisé dans Ionic et l’utilité des lifecycle events, et pour finir nous avons appris à utiliser les fonctionnalités de stockage d’Ionic.

Nous n’avons vu qu’une petite partie des possibilités de ce Framework. Je vous laisse aller plus loin, notamment en ajoutant une SearchBar, des Alerts, des Toasts et bien plus encore.

Pour rappel, le code lié à cet article est disponible dans son intégralité sur Github.

© SOAT
Toute reproduction interdite sans autorisation de l’auteur

Nombre de vue : 11833

COMMENTAIRES 26 commentaires

  1. Etienne L. dit :

    Excellent !!! Merci Lamine 🙂

  2. Lamine BENDIB dit :

    Merci à toi pour ton retour, c’est motivant de savoir que l’article a été apprécié 🙂

  3. ALLOUL Yacine dit :

    Super cours mais je souhaites savoir si un article sur les api ?

  4. jihed abdelli dit :

    Bravo !! Merci pour vos efforts ..

  5. Tom T dit :

    Merci pour votre tuto !

    Il y a quelque chose que je ne comprends pas par contre.

    – sur l’ordi et sur l’iphone en real device je ne vois pas la même chose
    – sur mon iphone la page qui contient tous les films est totalement vide en dehors de la navbar
    – j’ai testé un live et débug avec -l -c en flags
    – quand je fais ça l’app marche

    Du coup, il semblerait que ça ne marche que dans un espèce de statut web (car on est bien d’accord que le -l -c n’offre pas la même app, je codais avant sur Ionic 1 et je me souviens que j’avais des bugs insolvables car je ne pouvais en comprendre l’origine), pas quand l’app est compilée.

    Avez-vous une piste ?

    Merci par avance,

    Tom

  6. Tom T dit :

    J’ai réussi à corriger ce problème.

    Pour débuguer j’ai utilisé l’inspecteur web dans Développement.

    Et en fait le problème c’était le path dans le fichier providers/movie-api/movie-api.ts :

    private baseUrl: string = “../../assets/api/movies.json”;

    il faut mettre :

    private baseUrl: string = “assets/api/movies.json”;

    et ça marche !

    Thomas

  7. rockwell dit :

    Merci pour votre tuto !

    j’ai un probleme.

    TypeError: Cannot read property ‘toString’ of undefined
    at FavoriteMovieProvider.webpackJsonp.79.FavoriteMovieProvider.getMovieKey (http://localhost:8100/build/main.js:598:37)
    at FavoriteMovieProvider.webpackJsonp.79.FavoriteMovieProvider.isFavortieMovie (http://localhost:8100/build/main.js:587:38)
    at MovieDetailPage.webpackJsonp.50.MovieDetailPage.ionViewDidLoad (http://localhost:8100/build/main.js:537:36)
    at ViewController._lifecycle (http://localhost:8100/build/vendor.js:19095:33)
    at ViewController._didLoad (http://localhost:8100/build/vendor.js:18978:14)
    at NavControllerBase._didLoad (http://localhost:8100/build/vendor.js:49067:18)
    at t.invoke (http://localhost:8100/build/polyfills.js:3:14976)
    at Object.onInvoke (http://localhost:8100/build/vendor.js:4982:33)
    at t.invoke (http://localhost:8100/build/polyfills.js:3:14916)
    at r.run (http://localhost:8100/build/polyfills.js:3:10143)

  8. rockwell dit :

    probleme resolu

  9. ferdinand dit :

    bonjour je suis un debutant avec ionic et je trouve se tutoriel superbe mais serieux j ai eu plus de 1000 milles mais je pense que ceci est du au faite que je suis debutant et voici une autre erreur que j ai
    comment recupper les donnes se trouvant dans le fichier movies.json

  10. ferdinand dit :

    Unexpected token m in JSON at position 0″

  11. Lucien Leroux dit :

    Merci beaucoup pour ton tutoriel. Il est hyper complet et ca fait du bien de lire un article aussi détaille. Peux tu cependant expliquer un peu plus la partie sur les “providers et la mise en place de HTTP”. Le lien dans l’article mène vers une 404. Dans la partie juste après on a l’apparition de l’interface Movie qui n’était pas là avant et que j’ai du mal à saisir aussi.

    En dehors de ça tout le début du tutoriel m’a permis de bien prendre en main Ionic donc : Super boulot ! Je vais continuer d’apprendre des choses par ici !

  12. AJel dit :

    Merci pour ce travail ça m’a clarifier plus les choses. juste une petite question.
    Vous avez commencé une application avec sidemenu. Comment le supprimer? parce que finalement il ne sert à rien. en plus à chaque fois qu’on clic sur le bouton “hamburger” on tombe sur le menu starter! ou au créer un bouton qui nous ramène à Movie App.

  13. Rick dit :

    Hello !

    Je découvre avec bonheur ce site… et ce tuto Ionic, un grand merci il est très clair et complet, idéal pour une 1ère approche d’Ionic !

    Une petite question annexe : la capture d’écran de l’arborescence du projet est faite sous quel Éditeur/IDE (et/ou avec quel plugin) ? Je trouve cette arborescence particulièrement lisible et agréable, avec toutes les icônes correspondant aux types de fichiers. Merci !

    Rick

  14. Lamine BENDIB dit :

    Hello tout le monde, merci à tous pour vos messages, je vais tenter d’y répondre. Je commence par toi Rick, j’utilise VS Code et voici le lien vers le plugin Material Icon Theme => https://marketplace.visualstudio.com/items?itemName=PKief.material-icon-theme, si tu développes en Angular/Typscript je te conseille aussi Angular Essentials fait par John Papa => https://marketplace.visualstudio.com/items?itemName=johnpapa.angular-essentials (qui contient déjà Material Icon Theme)

  15. Lamine BENDIB dit :

    @Ajel pour enlever le “sidemenu”, il te suffit d’aller dans “/src/app/app.html” et de supprimer le “ion-menu”.

  16. Julien dit :

    Bonjour,

    J’ai une erreur sur mon worskspace que je reproduis aussi avec les sources du repo github :

    ERROR Error: Uncaught (in promise): SyntaxError: JSON.parse: unexpected character at line 1 column 2 of the JSON data
    [80]/FavoriteMovieProvider.prototype.getFavoriteMovies/</</</<@http://localhost:8100/build/main.js:589:94
    M</l</t.prototype.invoke@http://localhost:8100/build/polyfills.js:3:14354
    onInvoke@http://localhost:8100/build/vendor.js:4247:24
    M</l</t.prototype.invoke@http://localhost:8100/build/polyfills.js:3:14281
    M</c</r.prototype.run@http://localhost:8100/build/polyfills.js:3:9504
    f/<@http://localhost:8100/build/polyfills.js:3:19620
    M</l</t.prototype.invokeTask@http://localhost:8100/build/polyfills.js:3:15029
    onInvokeTask@http://localhost:8100/build/vendor.js:4238:24
    M</l</t.prototype.invokeTask@http://localhost:8100/build/polyfills.js:3:14942
    M</c</r.prototype.runTask@http://localhost:8100/build/polyfills.js:3:10195
    o@http://localhost:8100/build/polyfills.js:3:7267
    M</h</e.invokeTask@http://localhost:8100/build/polyfills.js:3:16203
    p@http://localhost:8100/build/polyfills.js:2:26991
    v@http://localhost:8100/build/polyfills.js:2:27238

    Stack trace:
    c@http://localhost:8100/build/polyfills.js:3:19132
    f/<@http://localhost:8100/build/polyfills.js:3:19653
    M</l</t.prototype.invokeTask@http://localhost:8100/build/polyfills.js:3:15029
    onInvokeTask@http://localhost:8100/build/vendor.js:4238:24
    M</l</t.prototype.invokeTask@http://localhost:8100/build/polyfills.js:3:14942
    M</c</r.prototype.runTask@http://localhost:8100/build/polyfills.js:3:10195
    o@http://localhost:8100/build/polyfills.js:3:7267
    M</h</e.invokeTask@http://localhost:8100/build/polyfills.js:3:16203
    p@http://localhost:8100/build/polyfills.js:2:26991
    v@http://localhost:8100/build/polyfills.js:2:27238
    vendor.js:1377:5

    Une idée pour m'aider ?

    Merci

  17. Lamine BENDIB dit :

    Hello Julien,
    Je viens de récupérer le code source de Github et ça marche sans problème sur mon poste voici les étapes que j’ai réalisé :
    1- npm install
    2- ionic serve

    Les version installé sur ma machine sont :
    node => 8.9.2
    npm => 5.5.1
    ionic => 3.19.1

  18. Issbil dit :

    Merci pour ce superbe tuto ! C’est vraiment complet!

  19. Bendrog dit :

    Bonjour,

    Merci pour ce tuto, il est très completet permet de bien comprendre la structure d’une appli ionic.

    Par contre le lien “article” dans le premier paragraphe de “Création de notre premier provider” ne redirige pas sur un véritable article.

    Etait-ce un lien vers la doc officielle ou vers l’un de vos article?

    Si c’est la 2ème réponse c’est article existe-il toujours? si oui, où trouver le liens?

    merci pour tout,

    Bendrog

  20. Naillik dit :

    Dans la partie sur le provider, J’ai cette erreur :
    Cannot find name ‘Movie’.
    movies = new Array();

  21. Naillik dit :

    Tu pourrais m’aider, please ? ^^’

  22. Lamine BENDIB dit :

    Hello Naillik,

    C’est une erreur de ma part, il n’y a pas d’objet `Movie` mais c’est plutôt `IMovie` du coup il faut juste que tu déclares ta variable `movies` comme suit:

    `movies = new Array();`

    N’hésite pas à checker le code source sur github => https://github.com/SoatGroup/ionic3-movie-app/blob/master/src/pages/movie-list/movie-list.ts

  23. Farsi dit :

    Bonjour!
    J’ai suivie votre tuto et ça m’a beaucoup aidé pour faire l’application mobile de mon boulot et je vous remercie beaucoup.
    Du coup j’ai fait une mise à jour de mon application pour corriger quelques bugs et faire des évolutions, et je voudrais savoir s’il y a un moyen de demander le client de mettre à jour son application à l’ouverture de sa session.
    Merci

  24. Zineb dit :

    Merci beaucoup pour l’effort et le partage.
    c très intéressant et motivant comme travail.
    Je te félicite.

  25. Olivier dit :

    De loin le meilleur tuto, le plus abordable pour commencer, avec des explications très clair!
    la réussite (et surtout compréhension) encourage à aller plus loin ensuite.
    Merci!!

  26. bob dit :

    une fois créé les pages comment fait on pour déclarer les modules dans le tableau des imports de app.module.ts on dans quoi clairement

AJOUTER UN COMMENTAIRE