Réalisation d'une application mobile <film>

Dans ce projet, nous allons créer une application de film. Dès que l’utilisateur ouvre l’application, elle affiche une liste de films sur le point de sortir dans les cinémas. Nous aurons l’occasion dans ce projet d’aborder de nombreux concepts incroyablement importants pour réaliser une application mobile à l'aide de flutter. Il s’agit notamment de la programmation asynchrone dans Flutter, de la lecture de données JSON à partir du Web, de l’utilisation de widgets et de la transmission de données d’un écran à un autre.

Aperçu du projet

Les écrans du projet

Note :

Tirez le code de l’application sur le lien ci-contre : https://github.com/AFH-Code/flutter-projets

Notre application va tirer ses données par http sur la plateforme TMDB (https://www.themoviedb.org). Créez un compte sur cette plateforme et demandé le Token pour l’accès à l’API.

Création de l’application et connexion à l’API avec la bibliothèque HTTP

Créons une nouvelle application Flutter.

     a)      Appelons là films avec la commande ci-dessous:

> flutter new films

     b)      Ajoutons la dépendance http.

> flutter pub add http

     c)      Tirons le code des dépendances en local

> flutter pub get

     d)      Créons un nouveau fichier, appelé http_helper.dart, que nous utiliserons pour créer les paramètres et les méthodes que nous utiliserons pour nous  connecter au service Web avec le code suivant.

import 'dart:developer';

class HttpHelper {
  final String urlBase = 'api.themoviedb.org';
  final String uriUpcoming = '3/movie/upcoming';

  final queryParameters = {
    'api_key': 'xxxxxxxxxxxxxxxxxxxx0e8127c902e195',
    'language': 'en-US',
  };

  Future getUpcoming() async {
    Uri url = Uri.https(urlBase, uriUpcoming, queryParameters);
    http.Response result = await http.get(url);

    log(url.toString());
    if (result.statusCode == HttpStatus.ok) {
      String responseBody = result.body;
      return responseBody;
    }
    else {
      return 're';
    }
  }
}

L’attribut queryParameters spécifie le paramètre api_key qui définit votre clé d’API récupérée sur la plateforme TMBD.

     e)      Ouvrons le fichier main.dart, supprimez le code par défaut de l’application et créez une application de base avec le code ci-dessous:

import 'package:flutter/material.dart';
import 'movie_list.dart';

void main() => runApp(MyMovies());

class MyMovies extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My Movies',
      theme: ThemeData(
        primarySwatch: Colors.deepOrange,
      ),
      home: Home(),
    );
  }
}

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MovieList();
  }
}

Ce code fait appel à la vue MovieList que nous n’avons pas encore créée, Voyons voir le contenu de cette vue.

     f)      Créons le fichier movie_list.dart avec le code ci-dessous.

import 'package:flutter/material.dart';
import 'http_helper.dart';

class MovieList extends StatefulWidget {
  @override
  _MovieListState createState() => _MovieListState();
}

class _MovieListState extends State {

  String result = '';

  @override
  void initState() {
    super.initState();
  }

    @override
  Widget build(BuildContext context) {
    HttpHelper helper = HttpHelper();
    helper.getUpcoming().then(
            (value){
          setState(() {
            result = value;
          });
        }
    );

    return Scaffold(
        appBar: AppBar(title: Text('Movies')),
        body: Container(
            child: Text(result)
        )
    );
  }
}

En exécutant l’application pour le moment, nous obtenons le texte brut à l’écran comme la montre la capture ci-dessous !

Nb : Le code de cette section est sous la branche « movies_http_text » de notre repos https://github.com/AFH-Code/flutter-projets

Analyse des données JSON et transformation en objets de modèle

Nous sommes maintenant prêts à afficher les données qui ont été récupérées à partir du service Web dans l’interface utilisateur de manière structurée:

    a)      Créons une classe modale dans le fichier movie.dart avec le code ci-dessous.

class Movie{
  int? id;
  String? title;
  double? voteAverage;
  String? releaseDate;
  String? overview;
  String? posterPath;

  Movie(this.id, this.title, this.voteAverage, this.releaseDate, this.overview, this.posterPath);

  Movie.fromJson(Map<String, dynamic> parsedJson) {
    this.id = parsedJson['id'];
    this.title = parsedJson['title'];
    this.voteAverage = parsedJson['vote_average']*1.0;
    this.releaseDate = parsedJson['release_date'];
    this.overview = parsedJson['overview'];
    this.posterPath = parsedJson['poster_path'];
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'title': title,
      'voteAverage': voteAverage,
      'releaseDate': releaseDate,
      'overview': overview,
      'posterPath': posterPath,
    };
  }
}

     b)      Créons une nouvelle méthode getUpcoming2() dans le fichier http_helper.dart qui récupère le même données sur TMDB (https://www.themoviedb.org) et formate en objet Movie, le code ci-dessous.

Future<List> getUpcoming2() async {
  Uri url = Uri.https(urlBase, uriUpcoming, queryParameters);
  http.Response result = await http.get(url);

  log(url.toString());
  List movies = [];
  if (result.statusCode == HttpStatus.ok) {
    final jsonResponse = json.decode(result.body);
    List moviesMap = jsonResponse['results'];
    movies = moviesMap.map((i) => Movie.fromJson(i)).toList();

    }
    return movies;
}

La méthode retourne une liste d’objet Movie, il ne nous reste plus qu’à organiser ces données au niveau de la vue movie_list.dart.

Notons qu’il faut faire deux importations dans votre fichier http_helper.dart pour corriger les erreurs que vous avez actuellement dans ce fichier.

Ajoutez les deux lignes ci-dessous :

import 'dart:convert';
import 'movie.dart';
Ajoutons un contrôle ListView pour afficher les données

Au lieu d’afficher un seul morceau de texte, sans aucune interaction de l’utilisateur, pour l’interface utilisateur, nous utiliserons l’un des widgets les plus courants qui traitent des données ListView et FutureBuilder. Nous allons également afficher l’image illustrative pour chaque film, pour cela nous devons utiliser l’url de base fournit par l’API TMDB (https://www.themoviedb.org).

     a)      Ouvrons le fichier movie_list.dart et définissons en haut de la classe _MovieListState les variables iconBase et defaultImage qui définissent respectivement l’url de base pour les images et l’url des images à afficher au cas où une image illustrative d’un film n’existe pas !

final String iconBase = 'https://image.tmdb.org/t/p/w92/';
final String defaultImage = 'https://images.freeimages.com/images/large-previews/5eb/movie-clapboard-1184339.jpg';

     b)      Créons ensuite une nouvelle méthode, appelée initialise(). elle renvoie un futur et est marquée comme async.

Future<List> initialize() async {
  HttpHelper helper = HttpHelper();
  List movies = await helper.getUpcoming2();
  return movies;
}

Cette méthode utilise la méthode définie dans le fichier http_help.dart pour construire une liste d’objet Movie.

     c)      Toujours dans la vue movie_list.dart remplaçons le contenu de la méthode build() par le code suivant :

Widget build(BuildContext context) {
    NetworkImage image;
  return Scaffold(
      appBar: AppBar(title: Text('Movies')),
    body: Container(
      child: FutureBuilder(
          future: initialize(),
          builder: (BuildContext context, AsyncSnapshot<List> movies) {
            return ListView.builder(
              itemCount: movies.data?.length ?? 0,
              itemBuilder: (BuildContext context, int position) {

                if (movies.data![position].posterPath != null) {
                  image = NetworkImage(
                      iconBase + (movies.data![position].posterPath ?? '')
                  );
                }
                else {
                  image = NetworkImage(defaultImage);
                }

                return ListTile(
                  leading: CircleAvatar(
                    backgroundImage: image,
                  ),
                  title: Text(movies.data![position].title ?? ''),
                  subtitle: Text('Released: '+(movies.data![position].releaseDate ?? '')+' - Vote: '+movies.data![position].toString()),
                );
              }
          );
        }
      )
    )
  );
}

Nous n’allons pas nous attarder sur l’explication du code qui contient presque en intégralité les widgets que nous avons abordés dans le cours ……

En exécutant l’application, nous avons le rendu ci-dessous :

Nb : Le code de cette section est sous la branche « movies_http_list » de notre repos https://github.com/AFH-Code/flutter-projets

Affichage de l’écran de détail et transmission des données à travers les écrans

L’écran de détail de l’application affichera une image plus grande et un aperçu du film. Toutes les données requises, à l’exception de l’image, ont déjà été téléchargées et analysées à partir du service Web, nous n’aurons donc pas besoin d’utiliser la bibliothèque HTTP pour cet écran.

Les étapes requises pour remplir cette partie sont les suivantes :

     a)      Créons donc un nouveau fichier appelé movie_detail.dart, dans le dossier lib de l’application. Ici, il nous suffira d’importer la bibliothèque material.dart pour accéder aux widgets de matériel et à notre fichier movie.dart pour la classe Movie. Le contenu de ce fichier est le suivant :

import 'package:flutter/material.dart';
import 'movie.dart';

class MovieDetail extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
    );
  }
}

Remarquez que nous avons utilisé un widget sans état pour définir cette vue.

Comme vous le savez déjà, vous utilisez des widgets avec état lorsque l’état d’un widget change au cours de son cycle de vie. Vous pourriez être tenté de penser que comme l’image, le titre et la vue d’ensemble d’un film peuvent changer, nous avons besoin d’un widget avec état ici, mais ce n’est pas le cas. Lorsque l’utilisateur clique sur l’un des éléments du ListView, nous construirons toujours une nouvelle instance de l’écran, en transmettant les données du film. Cet écran n’aura donc pas besoin de changer au cours de son cycle de vie :

     b)      Lorsque nous appelons MovieDetail, nous voulons passer un Movie. Ainsi, en haut de la classe MovieDetail, créez une propriété movie et marquez-la comme final car il s'agit d'un widget sans état et un constructeur de la classe MovieDetail.

const MovieDetail({Key? key, required this.movie}) : super(key: key);
final Movie movie;

     c)      Dans la méthode build(), au lieu de renvoyer un Container, renvoyez un Scaffold. Son appBar contiendra le titre du Movie, et dans le corps, nous placerons un SingleChildScrollView. Ce widget rendra son enfant défilable s'il ne rentre pas dans l'écran. Puis dans le SingleChildScrollView, nous allons placer un widget Center, dont l'enfant sera une colonne contenant du texte avec la vue d'ensemble du film comme spécifié dans le code ci-dessous.

return Scaffold(
    appBar: AppBar(
      title: Text(movie.title ?? ''),
    ),
    body: SingleChildScrollView(
        child: Center(
            child: Column(
              children: [
                Container(
                    padding: EdgeInsets.all(16),
                    height: height / 1.5,
                    child: Image.network(path)),
                Container(
                  padding: EdgeInsets.only(left: 16, right: 16),
                  child: Text(movie.overview ?? ''),
                )
              ],
            )
        )
    )
);

     d)      Corrigé l’erreur sur la variable path et height

Votre code devrait indiquer une erreur sur la variable path et height, car nous n’avons pas encore crée ces variables qui indique respectivement le chemin de l’image à charger et sa hauteur. Insérons ces variables au début de la méthode build comme indiqué dans le code ci-dessous.

double height = MediaQuery.of(context).size.height;
String path;
if (movie.posterPath != null) {
  path = imgPath + (movie.posterPath ?? '');
} else {
  path = 'https://images.freeimages.com/images/large-previews/5eb/movie-clapboard-1184339.jpg';
}

     e)      L’interface utilisateur de la vue détaillée est prête. Nous n’avons qu’à l’appeler à partir de ListView. Revenons donc au fichier  movie_list.dart et importons le fichier movie_detail.dart:

import 'movie_detail.dart';

      f)      Définissons la méthode onTap du widget ListTile dans le fichier movie_list.dart comme indiqué dans le code ci-dessous.

onTap: () {
    MaterialPageRoute route = MaterialPageRoute(
        builder: (_) => MovieDetail(movie: movies.data![position]));
    Navigator.push(context, route);
  },

En exécutant l’application, puis en cliquant sur un film dans la liste des films, nous obtenons la page suivante.

Nb : Le code de cette section est sous la branche « movies_item_detail» de notre repos https://github.com/AFH-Code/flutter-projets

Ajout de la fonction de recherche

En tirant parti de la fonctionnalité de recherche du service Web Base de données de films, nous permettrons à nos utilisateurs de rechercher n’importe quel film par titre.

Ce que nous voulons faire, c'est afficher un bouton d'icône de recherche dans l'AppBar. Lorsque l'utilisateur appuie sur le bouton, il pourra saisir une partie d'un titre de film dans un TextField, et lorsqu'il appuie sur le bouton de recherche du clavier, l'application appelle le service Web pour récupérer tous les films contenant l'entrée de l'utilisateur.

La mise en place de la logique métier se fait suivant les étapes ci-dessous :

     a)      Dans la classe HttpHelper du fichier http_helper.dart, créons un variable contenant le uri du chemin de recherche des films fournit par TMDB (https://www.themoviedb.org).

final String uriSearchBase = '3/search/movie';

     b)      Créons dans la même classe la fonction asynchrone findMovies qui va rechargée les films en fonction du mot clé fournit par l’utilisateur.

Future<List> findMovies(String? title) async {
  Uri url = Uri.https(urlBase, uriSearchBase, {
    'api_key': 'xxxxxxxxxxxxxxxxxxxxxxx27c902e195',
    'query': title,
  });
  http.Response result = await http.get(url);

  log(url.toString());
  List movies = [];

  if (result.statusCode == HttpStatus.ok) {
    final jsonResponse = json.decode(result.body);
    List moviesMap = jsonResponse['results'];
    movies = moviesMap.map((i) => Movie.fromJson(i)).toList();
    return movies;
  }
  return movies;
}

Le lien de la recherche constitué qu’on peut avoir à travers la variable url resemble à ce ci :  ‘https://api.themoviedb.org/3/search/movie?api_key=[YOUR API KEY HERE]&query=’

Nous devons par la suite implémenter la fonction de recherche dans l'interface utilisateur. Il existe plusieurs façons d'y parvenir, mais nous allons utiliser le widget AppBar, qui peut contenir non seulement du texte, mais également des icônes, des boutons et plusieurs autres widgets, y compris un TextField.

     c)      Revenons au fichier movie_list.dart. Nous allons créer deux propriétés dans la classe _MovieListState : une pour l'icône visible (l'icône de recherche lorsque l'écran est chargé) et la seconde un widget générique qui au départ sera un widget Texte contenant des Films.

Icon visibleIcon = Icon(Icons.search);
Widget searchBar= Text('Movies');

     d)      Ensuite, dans l'appBar du Scaffold dans la méthode build(), changeons le titre pour prendre le widget searchBar, et ajoutons un paramètre actions. Cela prend un tableau de widgets qui sont affichés après le titre. Celui-ci ne contiendra qu'un seul IconButton contenair Icons.search

appBar: AppBar(title: searchBar,
    actions: [
      IconButton(
          icon: visibleIcon,
          onPressed: () {
            setState(() {
              if (this.visibleIcon.icon == Icons.search) {
                this.visibleIcon = Icon(Icons.cancel);
                this.searchBar = TextField(
                      textInputAction: TextInputAction.search,
                      style: TextStyle(color: Colors.white, fontSize: 20.0),
                      onSubmitted: (String text) {
                        initialize(text);
                      },
                  autofocus: true,
                 );
              } else {
                setState(() {
                  this.visibleIcon = Icon(Icons.search);
                  this.searchBar= Text('Movies');
                  searchKey = '';
                });
              }
            }
            );
          },
      ),
    ] 
) 

     e)      Quand le formulaire est soumit, la fonction initialize(text) est appelée en prenant le texte saisie en paramètre, nous devons donc modifier cette méthode pour appeler la méthode appropriée pour effectuer la requête.

Future<List> initialize(String? title) async {
  HttpHelper helper = HttpHelper();
  if (this.visibleIcon.icon == Icons.search) {
    List movies = await helper.getUpcoming2();
    return movies;
  }else{
    List movies = await helper.findMovies(title);
    setState((){
      searchKey = title ?? '';
    });
    return movies;
  }
}

À ce stade notre application est prête avec toutes les fonctionnalités envisagées.

Nb : Le code de cette section est sous la branche « movies_http_search » de notre repos https://github.com/AFH-Code/flutter-projets

Vous avez un projet ?

Ne perdez plus le temps, Nos techniciens s'en occupent comme il se doit

Inscription en cours

Vérifiez votre commande et cliquez sur suivant pour poursuivre l'inscrption.