đŸ‘šâ€đŸ«

Étapes du Dojo

Application de découverte de films:

Durant les prochaines heures, nous allons créer une application Android.

Cette application utilisera l'API mise à disposition par la base de données de films The Movie Database (TMDb) pour afficher sous forme de carte :

En bonus (🃏), il sera aussi possible d'ajouter ces films en favoris afin de garder une sĂ©lection de film dans l'application.

Architecture du code

Model View Presenter (MVP) + Repository

Executors (threading)

Sur Android, il est impossible de faire des appels réseaux sur le main thread (thread en charge de la vue, il est en charge d'afficher l'application avec un certain nombre de frame par seconde).

Il faut donc passer les appels réseau sur un autre thread puis envoyé la réponse sur le main thread.

Pour facilité la gestion des threads, en s'inspirant des recommandations de Google en terme d'architecture d'application, la classe AppExecutors a été créée. Elle permet d'avoir accÚs à 3 types d'executors qui permet de lancer des actions sur 3 types de threads: le main thread, le thread network IO et le thread disk IO.

Activité:

An Activity is a single, focused thing that the user can do. (https://developer.android.com/reference/android/app/Activity)

Une application Android, est composĂ©e d’une ou plusieurs activitĂ©s (et de fragments).

Ces objets suivent un cycle de vie dĂšs lors que l’application est lancĂ©e.

Simplified activity lifecycle

https://developer.android.com/guide/components/activities/activity-lifecycle

1. Création de la carte d'un film

đŸ›«
Pour commencer: git stash -a && git checkout step1.0
💡
Pour la liste complÚte des raccourcis : https://developer.android.com/studio/intro/keyboard-shortcuts Vous trouverez le raccourci permettant de corriger une erreur ou un warning en étant guidé par Android Studio sous le nom Project quick fix (show intention actions and quick fixes). C'est Alt+Enter sur Windows / Linux et Option+Enter sur Mac.

Inflate un layout dans une activité

A layout defines the structure for a user interface in your app, such as in an activity. All elements in the layout are built using a hierarchy of View and ViewGroup objects. A View usually draws something the user can see and interact with. (https://developer.android.com/guide/topics/ui/declaring-layout)

La façon la plus utilisée pour créer l'interface utilisateur de notre activité est d'utiliser un layout.

C'est un fichier XML qui définit la structure de notre interface utilisateur.

✍
Ouvrir le fichier app/res/layout/activity_discover.xml

Vous devriez voir l'écran ci-dessous:

Comme vous pouvez le voir, un layout ressemble plus ou moins à un fichier HTML. On a ici un ViewGroup, le FrameLayout qui contient tout simplement des vues sans arrangement spécial, et une View, la TextView qui permet d'afficher du texte.

Pour utiliser ce layout dans notre activité, il faut utiliser la méthode setContentView lors de la création de l'activité.

✍
Ouvrir la classe com.fabernovel.codingdojo.app.main.DiscoverActivity et utiliser setContentView pour inflate le layout activity_discover.
💡
Pour accéder aux ressources de l'application depuis une classe, il faut utiliser R.<type de la ressource>.<nom de la ressource>. Ici ce sera donc R.layout.activity_discover
💡
Pour chercher une classe utiliser le raccourci CTRL+N / CMD + O Pour chercher une fichier, utiliser CTRL+SHIFT+N / CMD+SHIFT+N
đŸ“±
Relancer l'application


Création de la carte d'un film

âČ
Si tu as pris du retard: git stash -a && git checkout step1.2

Le but maintenant va ĂȘtre d'afficher la carte ci-dessous:

Layout d'une carte de film
Blueprint d'une carte de film
💡
Les vues d'un layout ont deux attributs obligatoires: layout_width et layout_height . La valeur de ces attributes peut-ĂȘtre: - Un dimension (en dp de prĂ©fĂ©rences exemple: layout_width="56dp" ) - wrap_content : Android calcule la taille de vue minimale possible. - match_parent : la vue prend tout l'espace dans le parents.

Pour faire la carte en elle-mĂȘme, nous allons utiliser une CardView

💡
CardView vient de la librairie de material design de google qui a déjà été ajoutée dans le projet. (https://material.io/develop/android/docs/getting-started/)

Afin d'organiser les éléments au sein de cette CardView, nous allons utiliser des LinearLayout. Grùce à ce layout, il est possible d'aligner des vues de façon horizontales ou bien vertical.

<!-- VERTICAL -->
<LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
				android:orientation="vertical"
        >
	<!-- VIEWS -->
</LinearLayout>

<!-- HORIZONTAL -->
<LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
				android:orientation="horizontal"
        >
	<!-- VIEWS -->
</LinearLayout>

Pour les textes, nous allons utiliser des TextView. Le titre du film utilisera android:textAppearance="@style/Headline6" , les label (date de sortie et genre)android:textAppearance="@style/Label" et les valeurs de ces labels: android:textAppearance="@style/Caption" .

Le texte affiché par les labels se trouve dans le fichier de resource res/values/string.xml . Pour les utiliser dans le layout, ajouter un attribut: android:text="@string/<nom de la ressource>" .

Pour les Ă©toiles de la note du film, une RatingBar, sur laquelle on utilisera l'attribut isIndicator="true" pour empĂȘcher les utilisateurs de modifier le nombre d'Ă©toiles.

Vues Ă  ajouter dans le layout

VueIDType de vueCommentaireVariable
Postermovie_card_posterImageViewmoviePoster
Titremovie_card_titleTextViewmovieTitle
Étoiles movie_card_starsRatingBar- Ajouter un style: style="@style/Widget.AppCompat.RatingBar.Small"; - Ajouter android:isIndicator="true"movieRatingBar
Notemovie_card_ratingTextViewmovieRating
Label de date de sortiemovie_card_release_date_titleTextView
Date de sortiemovie_card_release_date_valueTextViewmovieReleaseDateValue
Label du genremovie_card_genre_titleTextView
Genremovie_card_genre_valueTextViewmovieGenreValue
Message de loadingdiscover_messageTextViewdiscoverMessage
Cartemovie_cardCardViewmovieCard
✍
Ouvrir le layout item_movie dans le dossier app/res/layout. La vue sera composée d'une CardView parente dans laquelle se trouvera un premier LinearLayout horizontal qui contiendra le poster et un second LinearLayout vertical qui contiendra les autres vues. Pour le poster, il est possible d'utiliser l'image joker.png mise à disposition. Dans le layout activity_discover, ajouter <include layout="@layout/item_movie" /> au dessus de la vue discoverMessage et ajouter android:visibility="gone" sur la vue discoverMessage.
đŸ“±
Lancer l'application.

2. Afficher les données des films dans la carte créée

âČ
Si tu as pris du retard: git stash -a && git checkout step2.0

Pour l'instant, les données affichées sont en dur dans le layout. Le but de cette partie sera de récupérer les vues de la carte de film dans l'activité et d'y mettre les valeurs venant d'un objet Movie.

Récupérer les vues d'un layout

💡
Pour récupérer une vue venant d'un layout dans une activité, il faut utiliser la méthode findViewById qui permet de rechercher une vue avec un certain id dans toute la hiérarchie de la vue utilisée par notre activité avec setContentView. https://developer.android.com/reference/android/app/Activity#findViewById(int) mentionner R.id. ?

dGV

✍
Dans la classe DiscoverActivity, créer des variables permettant de récupérer les différentes vues de notre carte film (poster, note, étoiles, titre, genre, etc). Initialiser ces variables aprÚs l'appel à setContentView.

Pour rappel sur les types de vue:

Vues Ă  ajouter dans le layout

VueIDType de vueVariableCommentaire
Genremovie_card_genre_valueTextViewmovieGenreValue
Label du genremovie_card_genre_titleTextView
Date de sortiemovie_card_release_date_valueTextViewmovieReleaseDateValue
Label de date de sortiemovie_card_release_date_titleTextView
Notemovie_card_ratingTextViewmovieRating
Étoiles movie_card_starsRatingBarmovieRatingBar- Ajouter un style: style="@style/Widget.AppCompat.RatingBar.Small"; - Ajouter android:isIndicator="true"
Titremovie_card_titleTextViewmovieTitle
Postermovie_card_posterImageViewmoviePoster
Message de loadingdiscover_messageTextViewdiscoverMessage
Cartemovie_cardCardViewmovieCard

Afficher du contenues dans des vues

âČ
Si tu as pris du retard: git stash -a && git checkout step2.2

Maintenant que les vues ont bien été récupérées, il reste à récupérer un object Movie à afficher. Pour cela utiliser la classe DiscoverPresenter.

✍
Tout d'abord, il faut implémenter l'interface DiscoverViewContract et override ses méthodes dans DiscoverActivity: 1. showLoading: afficher le message R.string.loading dans la vue R.id.discoverMessage, puis changer sa visibilité (setVisibility ) à View.VISIBLE et celle de la carte film à View.GONE. 2. showContent: afficher les différentes propriétés du paramÚtre Movie dans les vues correspondantes puis changer sa visibilité à View.VISIBLE et celle du message à View.GONE 3. showError: afficher le message en paramÚtre dans la vue R.id.discoverMessage, puis changer sa visibilité (setVisibility ) à View.VISIBLE et celle de la carte film à View.GONE.
💡
- Pour afficher l'image dans le poster, utiliser Picasso.get().load(model.getPosterPath()).into(moviePoster); . Picasso permet de charger des images dans des ImageView . - Pour mettre du texte dans une TextView, utiliser la méthode setText(<text>) . - Pour mettre en forme la date de sortie, utiliser DateUtils.formatDate(<date>) qui formate la date en chaßne de caractÚres - Pour mettre le nombre d'étoile de la RatingBar, utiliser le méthode setRating(<note>)

Il faut maintenant récupérer et initialiser le DiscoverPresenter.

✍
1. Dans la DiscoverActivity, crĂ©er une variable de type DiscoverPresenter initialisĂ©e dans le onCreate en utilisant la mĂ©thode statique getInstance(this). (cela permet de ne pas exposer le repository Ă  la vue) 2. Dans la mĂ©thode onStart appeler la mĂ©thode start du presenter en lui passant le view contract (ici this puisque notre activitĂ© implĂ©mente ce dernier). 3. Dans la mĂ©thode onStop, juste avant l'appel Ă  super.onStop(), appeler la mĂ©thode stop du prĂ©senter. Cela permet, Ă  la vue de ne pas ĂȘtre mise Ă  jour lorsqu'elle n'est plus visible.

Récupérer un film dans le repository

âČ
Si tu as pris du retard: git stash -a && git checkout step2.3

Le MovieRepository fourni permet de récupérer un film. Pour l'instant, le film est déclaré en dur dans le repository.

💡
Pour récupérer un film à l'aide du MovieRepository, l'utilisation d'un callback est nécessaire. En effet, la récupération d'un film est asynchrone et il faut attendre d'obtenir le résultat pour l'afficher. Le callback est ici déclaré dans l'interface GetMovieCallback qui comporte deux méthodes onGetMovie lorsque le film a été récupéré et onError en cas d'erreur. Le repository va récupérer les films en utilisant le thread Network I/O puis appeler le callback sur le main thread.
✍
Dans la méthode start du presenter: 1. Afficher l'écran de chargement du view contract 2. Dans l'attribut movieCallback de type GetMovieCallback et appeler respectivement showContent et showError en cas de succÚs (onGetMovie) et d'erreur (onError). 3. Récupérer le film avec le repository: movieRepository.getMovie(movieCallback)
đŸ“±
Lancer l'application.

3. Afficher une liste de films

âČ
Si tu as pris du retard: git stash -a && git checkout step3.0

Pour l'instant, le layout qu'on a mis en place ne supporte que l'affichage d'une seule carte de film.

💡
Pour pouvoir afficher une liste de films de n'importe quelle taille, on va utiliser l'incontournable RecyclerView https://developer.android.com/guide/topics/ui/layout/recyclerview
✍
1. Dans le fichier app/res/layout/activity_discover.xml, remplacer le bloc qui permettait d'afficher la carte de film (<include layout"@layout/item_movie">)par une RecyclerView avec l'id movie_recycler . 2. Faites les changements nécessaires dans la classe DiscoverActivity pour récupérer la RecyclerView dans l'activité, et ensuite gérer sa visibilité en fonction de l'état de la vue : loading, content, etc. Si besoin, se référer à la section Add RecyclerView to your layout.
💡
Commenter le code permettant de mettre les données dans la carte de film afin de pouvoir le réutiliser aprÚs dans le ViewHolder

Passer la liste de films Ă  la RecyclerView

âČ
Si tu as pris du retard: git stash -a && git checkout step3.1

Pour pouvoir fonctionner, une RecyclerView s'utilise toujours avec un Adapter qui gĂšre la liste des Ă©lĂ©ments Ă  afficher. L'Adapter a Ă  son tour besoin d'un ViewHolder qui lui gĂšre l'affichage d'un Ă©lĂ©ment de la liste. Par souci d'efficacitĂ©, ces 2 classes vous ont Ă©tĂ© fournies et sont prĂȘtes Ă  ĂȘtre utilisĂ©es. Quand on implĂ©mente une recycler view pour la premiĂšre fois, on peut avoir un peu de mal Ă  comprendre comment tout cela fonctionne, voici donc quelques explications :

Le ViewHolder

Un view holder a simplement pour but de gérer l'affichage d'un élément de la liste. Si vous jetez un coup d'oeil sur la classe MovieViewHolder, vous verrez que son constructeur prend en paramÚtre une View. Cette vue correspond à une carte de film, et en appelant la méthode findViewById du paramÚtre itemView avec les bons ids, on récupÚre séparément des références sur les vues contenues dans la carte : la TextView qui affiche le titre, l'ImageView qui contient le poster, etc. Ces références sont utilisées plus tard dans la méthode bind() pour alimenter chaque carte de film avec les données d'un élément de la liste. C'est là qu'entre en jeu l'Adapter.

L'Adapter

Un adapter a pour rÎle de faire le lien entre une liste d'objets de type Movie et les ViewHolders qui vont afficher chacun des éléments de la liste. Si vous jetez un coup d'oeil sur la classe MoviesAdapter, vous verrez qu'il contient les éléments suivants :

💡
Un recycler view peut supporter une liste composĂ©e d'Ă©lĂ©ments construits via des layouts diffĂ©rents (par exemple, si on veut afficher les films par catĂ©gorie on peut ajouter des cartes "catĂ©gorie" au dĂ©but de chaque section), d'oĂč le paramĂštre viewType qui nous aurait permis de choisir le layout et la classe du ViewHolder en fonction de l'Ă©lĂ©ment Ă  afficher.

Récupérer une liste de films dans le repository

Pour tester notre recycler view, utiliser la méthode getMovies() de MovieRepository (qui retourne une liste de 3 films) à la place de getMovie() dans le presenter.

✍
Faites les changements nécessaires sur la méthode showContent() (dans DiscoverViewContract et DiscoverActivity). Dans DiscoverPresenter remplacez le GetMovieCallback par GetMoviesCallback pour récupérer une liste de film.

Passer la liste Ă  l'Adapter

✍
1. Dans la classe DiscoverActivity, déclarer un attribut de type MoviesAdapter et le relier à la recycler view en utilisant la méthode setAdapter(Adapter adapter) de la classe RecyclerView. 2. Dans la méthode showContent de DiscoverActivity, transmettre la liste reçue en paramÚtre à l'Adapter via submitList().
đŸ“±
Lancer l'application.

4. Utiliser une API pour chercher les films Ă  afficher

Comme vous vous en doutez, Ă©crire nous mĂȘme la liste des films Ă  afficher n'a pas trop d'intĂ©rĂȘt. Pour que notre application devienne vraiment utile, on va se servir de la base de donnĂ©es (non-officielle) disponible ici The Movie Database (TMDb) Plus prĂ©cisĂ©ment, on va utiliser l'API REST qui expose diffĂ©rents endpoints qui permettent d'interroger la base de donnĂ©es de diffĂ©rentes maniĂšres. La documentation exhaustive de cette API est disponible ici https://developers.themoviedb.org/3/getting-started

Découvrir l'API

Avant de l'intĂ©grer dans le code de notre application, il est pertinent de prendre quelques minutes pour interagir avec L'API et comprendre son fonctionnement. Pour ce dojo, le endpoint /discover/movie suffit Ă  notre besoin. Ce endpoint renvoie une liste de films qui correspond Ă  la combinaison de filtres (paramĂštres) spĂ©cifiĂ©e dans la requĂȘte qu'on lui envoie. L'accĂšs Ă  l'API se fait Ă  l'aide une clĂ© d'API qu'il faut insĂ©rer dans chaque requĂȘte Ă  l'aide du paramĂštre api_key.

💡
Pour plus d'informations sur les moyens d'authentification Ă  l'API, consulter la page https://developers.themoviedb.org/3/getting-started/authentication
✍
1. Consulter cette page de la doc https://www.themoviedb.org/documentation/api/discover qui illustre certains paramĂštres de requĂȘte qu'on peut utiliser avec le endpoint /discover/movie pour restreindre la liste de films qu'on rĂ©cupĂšre 2. Cette page https://developers.themoviedb.org/3/discover/movie-discover (onglet : Try it out) permet d'interagir avec le endpoint simplement, en insĂ©rant des paramĂštres dans un formulaire. Envoyer une ou deux requĂȘtes et vĂ©rifier que la rĂ©ponse (en format json) est cohĂ©rente (ne pas oublier de renseigner la clĂ© d'api) 3. Quelles sont les 3 combinaisons de paramĂštres qui nous permettront de rĂ©cupĂ©rer les 3 listes de films Ă  afficher dans les 3 onglets de l'app ?

Compléter la définition de l'API

Pour faciliter l'envoi de requĂȘtes et la rĂ©ception des rĂ©ponses, la librairie Retrofit est trĂšs pratique. Vous pouvez jeter un coup d'oeil sur la la page web de Retrofit pour voir comment ça marche (principalement les sections Introduction, REQUEST METHOD, et URL MANIPULATION)

Retrofit est un client REST qui facilite l'Ă©change avec une API REST. Pour pouvoir s'en servir, il faut dĂ©clarer une interface qui dĂ©crit les requĂȘtes qu'on va faire auprĂšs de l'API. Chaque requĂȘte se traduit par un prototype de mĂ©thode, annotĂ©e du type de requĂȘte (@GET, @POST, etc). L'annotation prend en paramĂštre le "path" de la requĂȘte sans la base url. On vous a fourni l'interface (MovieApi) avec la mĂ©thode suivante que nous utiliserons pour chercher les films qui vont sortir bientĂŽt :

@GET("/3/discover/movie")
Call<RestDiscoverResponse> discoverMovies(
    @Query("api_key") String apiKey,
    @Nullable @Query("primary_release_date.gte") String fromDate,
    @Nullable @Query("primary_release_date.gte") String toDate,
    @Nullable @Query("primary_release_year") Integer year,
    @Query("language") String language
);
💡
* la fameuse base url qu'on a Ă©voquĂ©e ci-haut est dĂ©finie dans la classe Network.java. Comme son nom l'indique, c'est le commencement du path de toutes les requĂȘtes de l'API. * le path de la requĂȘte "/3/discover/movie" commence par /3 pour choisir la version n°3 de l'API qui permet de s'authentifier via une clĂ© d'API * Il y a diffĂ©rentes façons de communiquer ses paramĂštres de requĂȘte Ă  l'API. Ici on envoie la clĂ© d'API, la date minimum de sortie, et la langue du film sous forme de @Query params comme indiquĂ© par la doc du endpoint. * Comme type de retour nous avons indiquĂ© Call<RestDiscoverResponse>. RestDiscoverResponse est le modĂšle qu'on reçoit du serveur. On a besoin de dĂ©finir les modĂšles qu'on Ă©change avec l'API parce que structurellement parlant, ils ne correspondent pas forcĂ©ment aux entitĂ©s qu'on manipule dans le code, d'oĂč la dĂ©finition d'un modĂšle RestMovie par exemple. Dans cette classe, nous avons fait appel Ă  l'annotation @SerializedName(...) pour indiquer l'appellation du champ dans le Json renvoyĂ© par l'API, et qui peut diffĂ©rer de la façon dont on a appelĂ© notre champ dans le code.

Compléter le repository

âČ
Si tu as pris du retard: git stash -a && git checkout step4.1

Le MovieRepository qu'on a actuellement nous renvoie des "mocks", c'est Ă  dire des donnĂ©es stockĂ©es actuellement. Dans cette partie, on va remplacer ces mocks par des films rĂ©cupĂ©rĂ©s via l'API. Pour ce faire, le repository va maintenant appeler les mĂ©thodes qu'on vient de dĂ©finir dans l'API. Comme vous pouvez le constater, ces mĂ©thodes retournent une variable de type Call<type du rĂ©sultat>. C'est seulement en appelant la mĂ©thode execute() de cette variable de type Call que la requĂȘte est rĂ©ellement envoyĂ©e et si aucune exception I/O n'est levĂ©e, on rĂ©cupĂšre un objet de type Response<type du rĂ©sultat>. Cet objet a une mĂ©thode isSuccessful() qui nous permet de savoir si le serveur a rĂ©ussi Ă  bien interprĂ©ter notre requĂȘte et nous a renvoyĂ© une rĂ©ponse 200. Dans ce cas on peut rĂ©cupĂ©rer le contenu de la rĂ©ponse via la mĂ©thode body() de l'objet Response qu'on a reçu.

✍
1. Dans la classe MovieRepository, utiliser la méthode discoverMovies de MovieApi avec les bons paramÚtres dans les méthodes getUpcomingMovies, getTopMoviesOfTheYear et getMoviesInTheatre . 2. Décommenter le dispatchSuccess dans la méthode fetchMovieList 3. Adapter le code de DiscoverPresenter pour qu'au lancement il demande au repository de chercher les films qui sont dans les salles de cinéma en ce moment.
⚠
Ici la clef d'API est écrite en claire dans le code de l'application. Ce n'est pas une pratique recommander car il est facile pour un acteur malveillant de décompiler l'application et d'utiliser cette clef.
đŸ“±
Lancer l'application.

Compléter le layout avec les 3 onglets

âČ
Si tu as pris du retard: git stash -a && git checkout step4.2

Le but de cette partie est de rajouter ces 3 onglets pour pouvoir naviguer entre les différentes listes :

Pour cela, on va utiliser un TabLayout.

✍
1. Dans activity_discover.xml, rajouter un TabLayout en ajustant bien les contraintes pour qu'il apparaisse en haut de l'écran. (on pourra par exemple mettre les vues actuellement dans un frame layout puis mettre ce frame layout sous les tabs à l'aide d'un linear layout) 2. Rajouter 3 TabItem dans le TabLayout de maniÚre à afficher 3 onglets. Pour attribuer à chaque onglet son titre, utiliser l'attribut android:text 3. Le titre de l'onglet du milieu est dynamique puisqu'il contient l'année en cours. Il faut donc lui donner une valeur "programmatiquement" (dans le code Java). Pour cela récupérer la vue correspondant au TabLayout dans DiscoverActivity, et récupérer ensuite l'onglet du milieu via la méthode getTabAt de la classe TabLayout. Enfin, utiliser une ressource de type string avec un argument. Pour ce faire, jeter un coup d'oeil à la section Formatting strings.
💡
Dans un LinearLayout, layout_weight permet de donner un poids aux vues afin qu'elle prenne le plus de poids possible.

Gestion des clicks sur les onglets

âČ
Si tu as pris du retard: git stash -a && git checkout step4.3
✍
1. Dans DiscoverActivity, utiliser la mĂ©thode addOnTabSelectedListener de la classe TabLayout pour dĂ©finir un listener sur vos onglets. Ce listener est une interface que l'on doit implĂ©menter. Laissez-vous guider par les indications et les raccourcis d'Android Studio pour gĂ©nĂ©rer le code minimum de l'implĂ©mentation de cette interface. 2. La mĂ©thode qui nous intĂ©resse le plus est onTabSelected, on peut laisser les 2 autres mĂ©thodes vides. onTabSelected prend en paramĂštre l'onglet qui vient d'ĂȘtre cliquĂ©. RĂ©cupĂ©rer l'indice de cet onglet via getPosition() et en fonction de cet indice, appeler la bonne mĂ©thode du presenter pour dĂ©clencher la rĂ©cupĂ©ration de la bonne liste de films.