Développement d'Applications Android

Developpement d'application Android en utilisant des technologies et librairies modernes pour augmenter le confort de développement ainsi que la qualité des applications

Introduction

Dans ce cours nous allons apprendre des développer des applications Android d'une façon moderne en utilisant Android JetPack.

Android JetPack

Android JetPack est un ensemble de librairies modernes mise à disposition par Google en plus du framework Android qui permettent d'augmenter le confort de développement et la qualité des applications pour les besoins usuels.

Installer Android Studio

Avec JetBrains Toolbox

Jetbrains Toolbox est un petit utilitaire très pratique qui permet d'installer et mettre à jours les logiciels JetBrains en un clic.

Installer Jetbrains Toolbox

Téléchargez et installez Jetbrains Toolbox

Installer IntelliJ

Exécutez JetBrains Toolbox puis faites un clic droit sur son icône dans la barre d'état système de Windows. Trouvez ensuite IntelliJ et Datagrip dans la liste des applications proposée. Cliquez sur "Install" et attendez la fin du téléchargement.

Avec Chocolatey

choco install AndroidStudio -y

Sans Jetbrains Toolbox ni Chocolatey

Téléchargez et installer via ce lien.

Créer un projet avec Android Studio

Lancez Android Studio et créez un nouveau projet. Séléctionnez "Empty Activity" comme template de projet :

Capture d’écran 2021-02-03 225556.png

Remplissez ensuite les caractéristiques de votre projet :

Capture d’écran 2021-02-03 225632.png

Structure du projet

La structure de notre projet est la suivante :

Capture d’écran 2021-02-04 182000.png

On retrouve tout d'abord le fichier AndroidManifest.xml qui va contenir toutes les méta données de l'application, comme par exemple son nom son icone, ses permissions, etc ...

Ensuite dans le dossier java on va retrouver nos codes source Java. Ensuite dans le dossier res on va trouver toutes les ressources et assets de l'application :

Enfin, on a deux fichiers build.gradle qui permettent de configurer les dépendances et la génération de l'application.

Configurer les bibliothèques JetPack

Pour ajouter les librairies de Android JetPack, nous allons justement utiliser ces fichiers build.gradle.

On va commencer par ajouter les versions des librairies que l'on utiliser dans des variables :

def lifecycleExtensionVersion = '1.1.1'
def butterKnifeVersion = '10.1.0'
def supportVersion = '28.0.0'
def retrofitVersion = '2.3.0'
def glideVersion = '4.9.0'
def rxJavaVersion = '2.1.1'
def roomVersion = '2.1.0-rc01'
def navVersion = '2.1.0-alpha05'
def preferencesVersion = '1.1.0'

Juste avante l'object dependencies.

Maintenant nous allons rajouter les déclarations dans dependencies pour réellement importer les librairies :

dependencies {

    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'com.google.android.material:material:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

    // Libraries imports

    // Android
    implementation "com.android.support:design:$supportVersion"
    implementation "android.arch.lifecycle:extensions:$lifecycleExtensionVersion"

    // Butterknife
    implementation "com.jakewharton:butterknife:$butterKnifeVersion"
    annotationProcessor "com.jakewharton:butterknife-compiler:$butterKnifeVersion"

    // Room
    implementation "androidx.room:room-runtime:$roomVersion"
    implementation "androidx.legacy:legacy-support-v4:1.0.0"
    annotationProcessor "androidx.room:room-compiler:$roomVersion"

    // Navigation
    implementation "androidx.navigation:navigation-fragment:$navVersion"
    implementation "androidx.navigation:navigation-ui:$navVersion"

    // Material
    implementation "com.google.android.material:material:1.2.1"

    // Retrofit
    implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
    implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
    implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion"

    // RxJava
    implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
    implementation "io.reactivex.rxjava2:rxandroid:$rxJavaVersion"

    // Glide
    implementation "com.github.bumptech.glide:glide:$glideVersion"

    // Palette
    implementation "com.android.support:palette-v7:$supportVersion"

    // Preferences
    implementation "androidx.preference:preference:$preferencesVersion"


    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

Ensuite pour configurer le plugin de génération de code pour la navigation, rajoutez la ligne :

plugins {
	...
	id 'androidx.navigation.safeargs'
}

dans l'objet plugins tout en haut du fichier. Ensuite, afin de pouvoir utiliser des fonctionnalités récentes de Java comme les streams et lambdas, nous allons ajouter l'objet suivant dans l'objet android :

android {

	...

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    dataBinding.enabled = true
}

Voilà, c'est tout pour le build.gradle de l'app. Maintenez, ouvrez le fichier build.gradle du projet, et ajoutez la ligne suivante dans dependancies :

classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0-alpha05"

Voilà ! C'est tout pour la configuration Gradle. Maintenant vous pouvez faire File > Sync Project with Gradle afin d'impacter la configuration gradle sur le projet.

La configuration des dépendances votre projet est maintenant terminée.

Structure du Code

Nous allons structurer notre code en plusieurs packages pour l'organiser plus clairement :

Vous pouvez donc déplacer votre classe MainActivity dans le package view.

Conseils pour le débogage

Je trouve que l'émulateur Android est pénible à utiliser : il prend beaucoup de ressource, ce qui rend le développement difficile sur des petites configs, et il bug souvent (sur une machine il me faisait des bluescreens systématiques au bout de qq minutes). C'est pourquoi j'utilise mon appareil physique pour le débogage. Cependant, on peut trouver ça pénible de devoir prendre ce dernier à chaque lancement de son appli et de devoir faire les manip sur le téléphone physique.

C'est pourquoi j'utilise ScrCpy pour controler mon téléphone depuis mon ordinateur. C'est un petit outil open source très performant, et simple à setup en USB. Branchez simplement votre téléphone à votre PC par USB et activer le débogage USB dessus. En faisant ça votre téléphone devrait être automatique détécté par Android Studio comme cible de déploiement :

run

Ensuite, installez ScrCpy soit via Chocolatey :

choco install scrcpy -y

ou alors de télécharger le ZIP sur GitHub.

Une fois installé, lancez tout simplement pour ce résultat :

Vous pouvez interagir avec votre téléphone à l'écran avec votre souris et clavier aussi simplement qu'avec l'émulateur !

Navigation

La bibliothèque JetPack Navigation va nous permettre de gérer le chemin de l'utilisateur à travers les différents écrans de l'application. La bibliothèque nous permet d'abstraire toute la complexité liée à ce concept en utilisant des mécaniques de génération de code. Cela va donc simplifier beaucoup pour le développeur :

On va donc pouvoir tisser les liens entre les écrans utilisée une interface d'Android studio qui ressemble un peu à ce qu'on pourrait retrouver sur des applications de conception graphique appelé le Navigation Graph :

Capture d’écran 2021-02-05 135216.png

Utilisation de la naviation

Créer les Fragments

Pour commencer nous allons créer nos différents écrans sous la forme de Fragments. Un fragment est un compostant d'interface graphique que nous allons utiliser à la manière d'une page de notre application. Nous allons avoir besoin de deux écrans : un pour afficher notre liste de Todos, et un pour afficher les détails d'un Todo. Dans le package view faites Clic droit > New > Fragment > Blank Fragment :

Capture d’écran 2021-02-05 135637.png

Cela vous nous créer deux fichiers :

Dans TodoListFramgent vous pouvez supprimer toutes les méthodes et commentaires à l'exception de onCreateView. Ils ne seront pas utiles pour l'instant et on y voit plus clair :

public class TodoListFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        ... 
    }
}

La méthode onCreateView est appelée par le framework lorsqu'il va initialiser notre Fragment. Elle contient pour l'instant que la ligne suivante :

return inflater.inflate(R.layout.fragment_todo_list, container, false);

Cette ligne va inflate ( = gonfler en anglais ) notre fichier de Layout XML afin de le transformer en objet Java contenant notre vue.

Répétez l'opération pour créer un fragment pour le détail d'un Todo. Vous pouvez également modifier la valeur des TextView dans chacun des Layouts de nos Fragment par "Hello List Fragment" et "Hello Detail Fragement" afin de pouvoir les différencier quand on testera la Navigation.

Créer la navigation

On va maintenant créer la navigation. Sur le dossier res faites Clic doit > New > Android Resource File :

Capture d’écran 2021-02-05 151958.png

N'oubliez pas de séléctionner le Type de ressource "Navigation" :

Capture d’écran 2021-02-05 152404.png

On a donc maintenant notre interface de conception de Navigation. Cependant elle n'est pas encore connectée à notre application. Nous allons devoir configurer notre MainActivity comme hôte de notre navigation. Pour ce faire, ouvrez le fichier de Layout de notre MainActivity situé à res/layout/activity_main.xml. Dans l'interface de conception, cherchez un controle appelé NavHostFragment :

Capture d’écran 2021-02-05 153422.png

Remplacez le texte "Hello World!" par ce controle. Un graphe de navigation vous est demandé, choisissez celui que nous venont de créer : todo_navigation. Ensuite, utilisez le bouton "Infer Constraint" pour configurer autmatiquement les contraintes du Constraints Layout, sur lequel nous reviendrons plus en détail plus tard :

Capture d’écran 2021-02-05 154303.png

Ainsi, en revenant sur notre Navigation (en rouvrant res/navigation/todo_navigation.xml) on peut constaté que notre Activity a bien été ajoutée comme hôte. On peut désormais ajouter nos fragment à notre navigation :

image-1612536590942.png

Et les connecter entre eux par drag & drop :

nav

Vous pouvez séléctionner la flèche et changer son id dans l'éditeur graphique par quelquechose de plus court que ce qui a été mis par défaut, par exemple actionDetail. Enfin pour activer la génération du code correspondant lancez le Build de votre projet. Cela va nous permettre d'utiliser la navigation dans notre code pour pouvoir effectivement naviguer.

Pour ce faire, ajout un Button dans le layout de votre TodoListFragment et créez dans son code behing une méthode telle que :

public void navigateToDetail(View button) {

}

Il faut maintenant lier notre méthode à l'événement de notre bouton : Pour ce faire, donnons un android:id à notre Button afin de pouvoir la récupérer :

<Button
          android:id="@+id/button"
          ....
/>

Ensuite on peut récupérer le Button à l'aide de la bibliothèque ButterKnife :

@BindView(R.id.button)
public Button button;

Il faut aussi changer notre onCreateView comme suit avant d'activer ButterKnife pour ce Fragment :

public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
  View view = inflater.inflate(R.layout.fragment_todo_list, container, false);
  ButterKnife.bind(this,view);
  return view;
}

On va maintenant dans notre TodoDetailFragment override une nouvelle méthode, onViewCreated, qui va être appelée par le framework juste après la création du Fragment. C'est là qu'on va lié notre événement :

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
  button.setOnClickListener(this::navigateToDetail);
}

On peut ensuite rajouter dans la méthode le code de navigation :

Navigation.findNavController(button).navigate(TodoListFragmentDirections.actionDetail());

On récupère le controleur de navigation du Framework, et on lui passe l'action de navigation qui a été générée. Vous pouvez maintenant lancer votre app sur un terminal (physique ou émulé) pour tester tout ça.

navigation.gif

Navigation avec des arguments

Dans l'UI de navigation, en cliquant sur un écran, vous pouvez le configurer. Dans le menu de configuration de votre écran vous pouvez ajouter un argument.

Capture d’écran 2021-02-06 113943.png

On doit maintenant modifier notre code pour gérer le passage de l'argument, mais d'abord il faut build notre projet pour mettre à jours le code généré. Voici comment rajouter le passage de l'argument, il suffit de le passer en paramètre à notre action :

TodoListFragmentDirections.ActionDetail actionDetail = TodoListFragmentDirections.actionDetail(UUID.randomUUID().toString());
Navigation.findNavController(button).navigate(actionDetail);

Nous allons ensuite récupérer et afficher notre UUID dans le DetailFragment. Pour ce faire, donnons un android:id à notre TextView afin de pouvoir la récupérer :

<TextView
          android:id="@+id/detailTextView"
          ....
/>

Ensuite on peut récupérer la TextView à l'aide de la bibliothèque ButterKnife :

@BindView(R.id.detailTextView)
public TextView textView;

Il faut aussi changer notre onCreateView comme suit avant d'activer ButterKnife pour ce Fragment :

public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
  View view = inflater.inflate(R.layout.fragment_todo_detail, container, false);
  ButterKnife.bind(this,view);
  return view;
}

On va maintenant dans notre TodoDetailFragment override une nouvelle méthode, onViewCreated, qui va être appelée par le framework juste après la création du Fragment. C'est là qu'on va récupérer notre argument :

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
  if(getArguments() != null){
    String uuid = TodoDetailFragmentArgs.fromBundle(getArguments()).getTodoId();
    textView.setText(uuid);
  }
}

Résultat :

navigation_args.gif

Animations de navigation

Dans l'UI de navigation, en cliquant sur votre action (la flèche) vous pouvez la configurer. Dans le menu de configuration de votre action, vous pouvez configurer des animations. Il y a en a plusieurs proposées par défaut, vous pouvez en choisir et tester le résultat :

animation

Vous pouvez en créer des customisées mais c'est hors de la scope de ce tuto.

Layout

Les Layout sont des composants de l'interface graphique qui n'ont pas de rendu à l'écran, mais vont décider comme les composants qu'ils contiennent vont être disposés à l'écran. Il en existe de très nombreuses variantes, mais nous allons étudier les trois principales qui sont les plus régulièrement utilisées.

Pour positionner un élément dans un Layout, deux propriétés XML sont obligatoire : layout_width et layout_height. Elle peuvent prendre des valeur numériques de taille ou alors :

Linear Layout

Le Linear Layout positionne les éléments les uns à la suites des autres en ligne ou en colonne selon l'orientation. On peut donner un poids au éléments pour le permettre de s'étendre plus ou moins par rapport aux autres.

Faisons par exmple un Linear Layout pour notre élément de liste de Todo :

<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" 
    android:layout_width="match_parent"
    android:layout_height="92dp">
  
</LinearLayout>

On commence par définir un LinearLayout avec un largeur qui remplit toute la ligne et une hauteur qui correspond à la hauteur qu'on choisit pour une ligne.

On va ensuite rajouter le contenu :

    <TextView
        android:id="@+id/todo_list_item_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="2"
        android:text="Todo Title"
        android:textSize="28dp"
        android:textFontWeight="700"
        >
    </TextView>
  
    <TextView
        android:id="@+id/todo_list_item_description"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="Todo Description"></TextView>

On donne au titre un poid double pour qu'il puisse prendre plus de place. Cela donne le résultat suivant :

ealative_layout

Contraint Layout

Le Constraint Layout permet de disposer les éléments en définissant des contraintes. On peut établir des contraintes de positionnement pour chaque controles par rapport aux autres éléments, au parent, par rapport au bord, etc ... Ce controle est très puissant notamment pour définir des Layouts responsive qui vont s'adapter à des téléphones de différentes dimensions.

Le Constraint Layout est fait pour être manipuler avec l'éditeur d'interface d'Android Studio. Nous allons l'utiliser pour concevoir l'interface de notre Todo Detail Fragment :

constraint_layout.gif

On utilise les connecteurs de contraintes sur l'éléments pour définir une contrainte par rapport à un autre élément (ici les bords). Il faut ensuite utiliser l'interface de Layout pour éditer les contraintes en changeant leur type ou leur longueur :

Sur les schéma des contraintes vous pouvez :

delete_constraint.gif

change_constraint.gif

margin_constraint.gif

Recycler View

La Recycler View est un élément qui permet d'afficher une liste d'éléments. Contrairement à la List View, la Recycler View va effectuer tout un tas d'optimisations sous le capot afin de pouvoir afficher avec de bonnes performances un grand nombre d'éléments.

Créer le modèle

Nous allons avoir besoin d'une classe de modèle pour afficher des éléments dans notre liste, créez une simple classe Todo dans le package model :

public class Todo {
    private String id;
    private String title;
    private String descrption;
  
    ... constructeur, getters & setters ...
}

Créer la Recycler View

Pour créer votre Recycler View, aller tout simplement dans le Layout de votre TodoListFragment, recherchez le dans la liste des controles et ajoutez le.

Adapteur de Liste

Nous allons avoir besoin de développer une classe Adapteur afin de faire le lien entre notre Liste d'objet Todo, la Recycler View et notre Layout d'Item. Créez donc une classe TodoListAdapter dans le package view qui étend la classe RecyclerView.Adapter :

public class TodoListAdapter extends RecyclerView.Adapter<> {
    
}

Ensuite, nous devons créer un ViewHolder, c'est un pattern imposé par la RecyclerView mais dans notre cas il ne fera rien, donc nous n'allons pas rentrer dans les détails. Pour ce faire, créez une classe interne TodoViewHolder qui étend RecyclerView.ViewHolder, ajoutez la en paramètre de type de notre adapteur :

public class TodoListAdapter extends RecyclerView.Adapter<TodoViewHolder> {
   
    class TodoViewHolder extends RecyclerView.ViewHolder {

        public View itemView;

        public TodoViewHolder(@NonNull View itemView) {
            super(itemView);
            this.itemView = itemView;
        }
    }
}

Il nous faut aussi un attributs pour stocker les éléments de notre liste, et un constructeur qui les récupère :

    private List<Todo> items;

    public TodoListAdapter(List<Todo> items) {
        this.items = items;
    }

Et également un setter pour mettre à jours la liste :

public void setDogList(List<DogBreed> dogList) {
        this.dogList.clear();
        this.dogList.addAll(dogList);
        notifyDataSetChanged();
}

L'appelle à notifyDataSetChanged va notifier la RecyclerView qu'il vaut se mettre à jours.

Enfin, il faut implémenter les méthodes onCreateViewHolder,onBindViewHolder et getItemCount.

    @Override
    public int getItemCount() {
        return this.items.size();
    }
    @NonNull
    @Override
    public TodoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.todo_list_item, parent, false);
        return new TodoViewHolder(view);
    }
    @Override
    public void onBindViewHolder(@NonNull TodoViewHolder holder, int position) {
        TextView title = holder.itemView.findViewById(R.id.todo_list_item_title);
        TextView description = holder.itemView.findViewById(R.id.todo_list_item_description);

        title.setText(items.get(position).getTitle());
        description.setText(items.get(position).getDescrption());
    }

Rajoutez également un setter pour la liste Todos, en cas de besoin de changer le contenu de la liste. Appelez, notifyDataSetChanged à la fin de ce setter afin de mettre à jours le rendu de la liste avec les nouvelles données.

Connecter RecyclerView et Adapter

Dans notre TodoListFragment, commençons par récupérer notre RecyclerView avec ButterKnife :

@BindView(R.id.recyclerView)
public RecyclerView recyclerView;

Dans la méthode onViewCreated, nous allons ensuite par créer des données de test et les connecter à notre Adapter et notre RecyclerView :

List<Todo> todos = Arrays.asList(
  new Todo(UUID.randomUUID().toString(), "Test Todo Title 1", "Test Todo Description 1"),
  new Todo(UUID.randomUUID().toString(), "Test Todo Title 2", "Test Todo Description 2"),
  new Todo(UUID.randomUUID().toString(), "Test Todo Title 3", "Test Todo Description 3"),
  new Todo(UUID.randomUUID().toString(), "Test Todo Title 4", "Test Todo Description 4")
  
  ....
  
);

TodoListAdapter adapter = new TodoListAdapter(todos);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.setAdapter(adapter);

Cela permet bien d'afficher notre Liste :

recyclerview.gif

MVVM et LiveData

Le design pattern MVVM

Le desgin pattern MVVM, pour Model-View-ViewModel est un Pattern pour séparer plus distinctement les responsabilités dans le développement d'une application avec une interface graphique.

Les principaux intérêt du Pattern MVVM sont :

MVVM et Android JetPack

Le framework Android fournit des outils afin d'implémenter facilement le pattern MVVM. Tout d'abord, une classe Abstraite ViewModel est fourni que nos ViewModels pourront étendre pour bénéficier de certaines fonctionnalités.

Ensuite, nous allons pour tirer parti des LiveData. Les LiveData sont des valeurs observables, c'est à dire qu'on peut s'abonner à leur changement. On peut créer des LiveData pour tout classe en utilisant un paramètre de type :

MutableLiveData<String> name = new MutableLiveDate<>();

Une LiveData possède une méthode setValue qui permet de mettre à jours la valeur, et une méthode observe qui prend en paramètre une lambda qui va permettre de réagir à la nouvelle valeur. Exemple :

name.observe(this, textView::setText);

Premier ViewModel

Nous allons donc procéder au refactoring de notre application en utilisant le Design Pattern MVVM. Créez un package viewmodel et dedans une nouvelles classe, TodoListViewModel qui étend la classe ViewModel :

public class TodoListViewModel extends AndroidViewModel{
	
}

Commençons par définir des propriété de données avec des LiveData :

private MutableLiveData<List<Todo>> todos = new MutableLiveData<>();

Ajoutons ensuite une méthode init qui va charger quelques Todos :

public void init() {
  todos.setValue(Arrays.asList(
    new Todo(UUID.randomUUID().toString(), "Test Todo Title 1", "Test Todo Description 1"),
    new Todo(UUID.randomUUID().toString(), "Test Todo Title 2", "Test Todo Description 2")
  ));
}

Nous pouvons aussi ajouter un gestionnaire d'événement qui ajoute un Todo :

public void addTodo(){
  List<Todo> todoList = todos.getValue();
  todoList.add(new Todo(UUID.randomUUID().toString(), "New Test Todo Title", "New Test Todo Description"));
  todos.setValue(todoList);
}

Nous devons maintenant lier notre ViewModel dans notre View. Commencez par supprimez l'initialisation de donnée, dont la resposabilité incombe au ViewModel. Créez également un FloatingActionButton et récupérez sa référence avec ButterKnife.

Ensuite créez un nouvel attributs de type TodoListViewModel dans votre classe, dans la méthode onViewCreated, récupérez le :

viewModel = ViewModelProviders.of(this).get(TodoListViewModel.class);

On peut maintenant appeler init pour créer les données de base, puis faire not liaisons de données et d'événements :

TodoListAdapter adapter = new TodoListAdapter(new ArrayList<>());

recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.setAdapter(adapter);

viewModel.getTodos().observe(this, adapter::setItems);
fab.setOnClickListener(v -> viewModel.addTodo());

On crée notre adapter avec une liste de vide et ensuite, on lie la LiveData de la TodoList au setters de l'adapteur afin de mettre à jours la valeur en l'adapteur à chaque fois que la valeur du LiveData est mise à jours.

Ensuite on ajoute tout simplement notre addTodo en gestionnaire d'événement OnClick du FloatingActionButton.

Voilà, notre application est refactorée avec le Pattern MVVM ! On peut observer le resultat suivant :

mvvm.gif

Databinding

Le Databinding ou liaison de donnée permet de déléguer au framework le fait de remplir des données dans la vue en utilisant des mécanismes de génération de code, afin de limiter le code répétitif.

Databinding dans la RecyclerView

Dans un premier temps, voyons comment utiliser le databinding pour les éléments de la RecyclerView.

Layout

Commençons par le Layout, todo_list_item, dans lequel nous allons rajouter une balise qui va englober tout notre Layout :

<layout xmlns:android="http://schemas.android.com/apk/res/android">
  
  ....
  
</layout>

(n'oubliez pas de déplacer le XMLNS Android de votre LinearLayout à cette nouvelle balise.

Nous devons ensuite déclarer le contexte de donnée du layout :

<layout xmlns:android="http://schemas.android.com/apk/res/android">
  
  <data>
    <variable
              name="todo"
              type="fr.arsenelapostolet.jetpacktuto.model.Todo" />
  </data>  
  ....
  
</layout>

Ces balises permettent de déclarer que ce Layout possède un contexte de donnée qui dépend d'une variable de la classe Todo et qui s'appelle todo. On va donc pouvoir utiliser cette variable dans les éléments de notre Layout :

...
            <TextView
                android:id="@+id/name"
                style="@style/Title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@{todo.title}"></TextView>

            <TextView
                android:id="@+id/lifespan"
                style="@style/Text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@{todo.description}"></TextView>
...

En utilise la syntaxe @{variable.champs} on peut lier des données dans notre Layout.

Adapteur

Voyons maintenant du coté de notre adapteur de liste, TodoListAdapter, qui doit être légèrement modifié pour supporter la liaison de données.

Tout d'abord, notre classe interne ViewHolder ne va plus détenir une View mais un TodoListItem, une classe générée par le framework pour opérer notre liaison de donnée.

   class TodoViewHolder extends RecyclerView.ViewHolder {

        public TodoListItem itemView;

        public DogViewHolder(@NonNull TodoListItem binding) {
            super(binding.getRoot());
            this.itemView = binding;
        }
    }

Ensuite, dans la méthode onCreateViewHolder, nos devons légèrement modifier la façon dont on inflate notre View :

    public TodoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        TodoListItem view = DataBindingUtil.inflate(inflater, R.layout.todo_list_item, parent, false);
        return new DogViewHolder(view);
    }

Cela permet de faire appel au mécanisme de Databinding pour inflate notre View.

Enfin, nous allons remplacer le contenu de la méthode onBindViewHolder par une seule ligne, l'affectation de l'objet du modèle à la liaison de donnée :

    public void onBindViewHolder(@NonNull TodoViewHolder holder, int position) {
        holder.itemView.setTodo(todoList.get(position));
    }

Lier des gestionnaires d'événement

On peut également utiliser le Databinding pour faire des liaisons de gestionnaire d'événements. Faisons ça pour implémenter la navigation au clic d'un élément de la RecyclerView.

...

Dans votre contexte de donnée, rajoutez une variable de type OnClickListener. Il s'agit d'une interface fonctionnelle qui définit un contrat de service pour la gestion d'un événements de clique.

Ensuite dans notre adapteur, dans la méthode onBindViewHolder, rajoutons le binding en passant une :

holder.itemView.setOnTodoClicked(v -> {
  ListFragmentDirections.ActionDetail action = ListFragmentDirections.actionDetail();
  action.setDogUuid(Integer.parseInt(todo.getId()));
  Navigation.findNavController(v).navigate(action);
});

Databinding dans un Fragment

Dans le Layout, on va fait exactement la même chose :

Pour le Code Behind du fragment, on va pouvoir supprimer tous nos @BindView et remplacer le contenu notre méthode onCreateView :

    private FragmentDetailBinding binding;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        this.binding = DataBindingUtil.inflate(inflater, R.layout.fragment_detail, container, false);
        return binding.getRoot();
    }

Retrofit et RxJava

Dans une application graphique, on a souvent besoin d'exécuter des opérations asynchrones, afin de pouvoir exécuter des opérations prennant du temps (réseau, fichiers, etc...) sans bloquer le thread qui gère l'interface graphique. Pour ce faire, on va faire appelle à RxJava.

RxJava

RxJava est une biblothèque qui apporte des outils pour faciliter la programmation avec une approche réactive et asynchrone au langage Java. Le concept principal de RxJava est l'utilisation du pattern Observer. Ce dernier consiste en la manipulation d'Observable, qui correspond à un flux (de données, d'événements, etc ...) auquel au Observer peut s'abonner pour en observer les changements, on peut égalmement appliquer des opérateurs sur ces flux pour les manipuler.

Comme nous allons appliquer ce modèle à des appels réseau à des API ReST, nous allons utiliser principalement des Single qui sont un cas particulier d'Observable qui au lieu d'émettre une série de valeur, émet seulement une valeur ou alors une erreur.

Retrofit

Pour ce tuto nous allons imaginer que nous disposons d'une simple API ReST avec les endpoints suivants :

Retrofit est une bibliothèque Java qui permet de créer facilement des clients pour des API ReST. Avec Retrofit, on a juste à définir une interface Java qui correspond à notre API, et l'implémentation sera générée.

Voici l'interface Retrofit correspondante à notre API :

public class TodoApi {
  
  @GET("/api/todos")
  Single<List<Todo>> getTodos();
  
  @GET("/api/todos/{id}")
  Single<List<Todo>> getTodo(@Path("id") String todoId);
  
  @POST("/api/todos")
  Single<Todo> createTodo(@Body Todo todo);
  
  @DELETE("/api/todos/{id}")
  Single<ResponseBody> deleteTodo(@Path("id") String todoId);
  
  @PUT("/api/todos/{id}")
  Single<Todo> update(@Path("id") String todoId, @Body Todo todo);
}

Les annotations suivantes permettent de définir notre interface :

Pour récupérer l'implémentation d'une interface Retrofit :

TodoApi api = new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .build()
                .create(TodoApi.class);

Intégration

On va créer une classe de Service qui sera consommée par notre ViewModel.

Glide

Room