• 8 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 02/02/2022

Architecturez votre application à l'aide d'un ViewModel

 Maintenant que nous en savons un peu plus sur l'Android Architecture Components, nous allons pouvoir implémenter certains de ses éléments dans notre mini-application SaveMyTrip.

L'objectif est donc d'appliquer l'Architecture Components à notre application, en créant les classes de Repository et de ViewModel pour ensuite les appeler à l'intérieur de notre activité TodoListActivity.

La Base de données SQLite comprend une branche ItemDao menant à ItemDataRepository et une branche UserDao menant à UserDataRepository. Ces deux branches se rejoignent dans l'ItemViewModel qui présente la TodoListActivity
Architecture désirée

Pour le moment, nous avons déjà développé dans la partie précédente de ce cours les deux classes qui nous serviront de sources de données, à savoir les classes ItemDao et UserDao.

Rendez le code compatible avec Java 8

Dans un premier temps, nous allons assurer la compatibilité de notre code avec Java 8, afin de pouvoir bénéficier des Lambdas. N'hésitez pas à lire ce chapitre du cours de Java dédié à ce sujet. Pour résumer, les Lambdas nous permettront d'écrire moins de code, de manière beaucoup plus lisible... :)

// SANS LES LAMBDAS

button.setOnClickListener(new View.OnClickListener() {

   @Override

   public void onClick(View v) {

      Toast.makeText(this, "Button clicked", Toast.LENGTH_LONG).show();

   }

});

//AVEC LES LAMBDAS

button.setOnClickListener(v -> Toast.makeText(this, "Button clicked", Toast.LENGTH_LONG).show());

 Ainsi, reprenez votre application et modifiez votre fichier build.gradle comme ci-dessous.

Extrait de build.gradle :

android {

   compileOptions {

      targetCompatibility 1.8

      sourceCompatibility 1.8

   }

}

Et voilà, votre application supporte désormais Java 8 et les Lambdas. ;)

Créez des Repository

Vous avez dû le remarquer, mais notre source de données est actuellement la base de données SQLite, manipulée via les différents DAO que nous avons créés précédemment. Afin de continuer dans la logique de l'Architecture Components, nous allons maintenant créer deux classes, ItemDataRepository et UserDataRepository, que nous utiliserons par la suite dans un ViewModel, bien sûr... :)

Ainsi, créez un nouveau package que vous appellerez repositories/, puis placez-y les fichiers suivants :

Classe repositories/UserDataRepository.java :

public class UserDataRepository {

   private final UserDao userDao;

   public UserDataRepository(UserDao userDao) { this.userDao = userDao; }

   // --- GET USER ---

   public LiveData<User> getUser(long userId) { return this.userDao.getUser(userId); }
}

Classe repositories/ItemDataRepository.java :

public class ItemDataRepository {

   private final ItemDao itemDao;

   public ItemDataRepository(ItemDao itemDao) { this.itemDao = itemDao; }

   // --- GET ---

   public LiveData<List<Item>> getItems(long userId){ return this.itemDao.getItems(userId); }
   // --- CREATE ---


   public void createItem(Item item){ itemDao.insertItem(item); }

   // --- DELETE ---

   public void deleteItem(long itemId){ itemDao.deleteItem(itemId); }

   // --- UPDATE ---

   public void updateItem(Item item){ itemDao.updateItem(item); }

}

 Explications : Ces deux classes sont assez simples, puisqu'elles récupèrent, à partir de leur constructeur, un DAO qu'elles réutilisent dans leurs méthodes publiques :). Le but du repository est vraiment d'isoler la source de données (DAO) du ViewModel, afin que ce dernier ne manipule pas directement la source de données.

Créez le ViewModel

Poursuivons l'implémentation de l'Architecture Components au sein de notre application en créant la classe ItemViewModel, que nous placerons dans le package todolist/ et qui sera par la suite intégrée dans notre activité TodoListActivity.

Classe todolist/ItemViewModel.java :

public class ItemViewModel extends ViewModel {

   // REPOSITORIES

   private final ItemDataRepository itemDataSource;

   private final UserDataRepository userDataSource;

   private final Executor executor;

   // DATA

   @Nullable

   private LiveData<User> currentUser;

   public ItemViewModel(ItemDataRepository itemDataSource, UserDataRepository userDataSource, Executor executor) {

       this.itemDataSource = itemDataSource;

       this.userDataSource = userDataSource;

       this.executor = executor;

   }

   public void init(long userId) {

       if (this.currentUser != null) {

           return;

       }

       currentUser = userDataSource.getUser(userId);

   }

   // -------------

   // FOR USER

   // -------------

   public LiveData<User> getUser() { return this.currentUser;  }

   // -------------

   // FOR ITEM

   // -------------

   public LiveData<List<Item>> getItems(long userId) {

   return itemDataSource.getItems(userId);

}

public void createItem(String text, int category, long userId) {

   executor.execute(() -> {

       itemDataSource.createItem(new Item(text, category, userId));

   });

}

   public void deleteItem(long itemId) {

       executor.execute(() -> itemDataSource.deleteItem(itemId));

   }

   public void updateItem(Item item) {

       executor.execute(() -> itemDataSource.updateItem(item));
   }

}

 Explications : Et voilà à quoi ressemble notre fameux ViewModel ! :D Tout d'abord, ce dernier hérite de la classe ViewModel. Puis, nous lui déclarons en variables de classe nos deux repository précédemment créés ainsi qu'une variable de type Executor, qui nous facilitera l'exécution en arrière-plan de certaines méthodes. Ces trois variables sont instanciées directement à partir du constructeur de la classe.

Nous avons également créé une méthode  init()  , afin d'initialiser notre ViewModel dès que l'activité se crée et qui sera donc appelée à l'intérieur de sa méthode   onCreate().

Euh, mais du coup pourquoi vérifies-tu dans cette méthode si l'utilisateur existe déjà dans le ViewModel ?

Eh bien, tout simplement car le ViewModel garde en "mémoire" ses données, même si l'activité qui l'a appelée est détruite, comme après une rotation par exemple… :) C'est d'ailleurs tout l'intérêt du ViewModel ! Ainsi, après une rotation de l'activité TodoListActivity, nous n'aurons pas besoin de re-récupérer l'utilisateur en base de données si ce dernier a été précédemment mémorisé dans le ViewModel.

Puis, nous avons créé différentes méthodes permettant de réaliser des actions sur notre base de données (représentant notre source de données). Nous utilisons la classe Executor afin de réaliser de manière asynchrone les requêtes de mise à jour de nos tables SQLite.

D'ailleurs, c'est également pour cette raison que nous utilisons le type LiveData dans les méthodes   getItems  et   getUser  de nos DAO, afin de bénéficier automatiquement de la récupération asynchrone... ;)

Modifiez l'activité

Avant de déclarer notre ViewModel dans notre activité TodoListActivity, il va falloir qu'on le construise... Car vous avez peut-être déjà remarqué que le constructeur de la classe ItemViewModel demandait en paramètre :

  • la classe ItemDataRepository (elle-même demandant la classe ItemDao en paramètre) ;

  • la classe UserDataRepository (elle-même demandant la classe UserDao en paramètre) ;

  • la classe Executor.

Cela risque de faire beaucoup de choses à déclarer et à instancier dans notre activité… Et rappelons-le, celle-ci n'est pas censée s'occuper de ce genre de choses ! On la veut la plus légère possible. >_<

Cela tombe bien, nous avons la possibilité de déporter la construction de notre ViewModel dans une classe... de Factory (ou fabrique en français) ! 

Qu'est-ce que c'est que ce truc encore ? Encore un Design Pattern, je parie !

Oui, tout à fait… :) Mais pas de panique ! Une factory est simplement un pattern permettant de déléguer la création d'une classe à une autre. Ainsi, dans notre cas, nous n'allons pas créer directement notre classe ItemViewModeldans notre activité, mais nous allons confier cette mission à la classe... ViewModelFactory, que nous placerons dans un package que nous appellerons injections/.

Classe injections/ViewModelFactory.java :

public class ViewModelFactory implements ViewModelProvider.Factory {

   private final ItemDataRepository itemDataSource;

   private final UserDataRepository userDataSource;

   private final Executor executor;

   private static ViewModelFactory factory;

   public static ViewModelFactory getInstance(Context context) {

      if (factory == null) {

          synchronized (ViewModelFactory.class) {

              if (factory == null) {

                  factory = new ViewModelFactory(context);

              }

          }

      }

      return factory;

   }

   private ViewModelFactory(Context context) {

     SaveMyTripDatabase database = SaveMyTripDatabase.getInstance(context);

     this.itemDataSource = new ItemDataRepository(database.itemDao());

     this.userDataSource = new UserDataRepository(database.userDao());

     this.executor = Executors.newSingleThreadExecutor();

  }

   @Override

   @NotNull

   public <T extends ViewModel>  T create(Class<T> modelClass) {

       if (modelClass.isAssignableFrom(ItemViewModel.class)) {

           return (T) new ItemViewModel(itemDataSource, userDataSource, executor);
       }

       throw new IllegalArgumentException("Unknown ViewModel class");
   }

}

  Explications : Nous avons créé une classe   ViewModelFactory, implémentant l'interface ViewModelProvider.Factory créée par Google, et qui sera utilisée par la suite pour déclarer notre ViewModel dans notre activité. Nous lui définissons ici un constructeur contenant les objets dont nous avons besoin pour instancier correctement notre classe ItemViewModel.

Attends, je ne comprends pas ! Nous avons créé le même constructeur pour la classe ViewModelFactory que pour la classe ItemViewModel. En quoi cela va nous aider ?

Pour le moment, cela nous permet de regrouper le processus de création de nos ViewModels dans une Factory dédiée, ViewModelFactory. Ainsi, si jamais par la suite nous souhaitons créer un autre ViewModel, par exemple   UserViewModel, nous le déclarerons ici, dans cette même Factory, à l'intérieur de la méthode  create(). :)

Modifions enfin notre activité TodoListActivity afin d'y ajouter notre ViewModel...

Extrait de TodoListActivity.java :

public class TodoListActivity extends AppCompatActivity implements ItemAdapter.Listener {

   // 1 - FOR DATA

   private ItemViewModel itemViewModel;

   private ItemAdapter adapter;

   private static final int USER_ID = 1;

   @Override

   protected void onCreate(Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);

       // 8 - Configure view (with recyclerview) & ViewModel

       configureViewModel();

       initView();

       // 9 - Get current user and items from Database

       getCurrentUser();

       getItems();

   }

   @Override

   public void onClickDeleteButton(Item item) {

       // 7 - Delete item after user clicked on button

       deleteItem(item);

   }

   @Override

   public void onItemClick(Item item) {

       // 7 - Update item after user clicked on it

       updateItem(item);

   }

   // -------------------

   // DATA

   // -------------------

   // 2 - Configuring ViewModel

   private void configureViewModel() {

       this.itemViewModel = new ViewModelProvider(this, ViewModelFactory.getInstance(this)).get(ItemViewModel.class);

       this.itemViewModel.init(USER_ID);

   }

   // 3 - Get Current User

   private void getCurrentUser() {

       LiveData<User> userLiveData = itemViewModel.getUser();

       if (userLiveData != null) {

           userLiveData.observe(this, this::updateView);

       }

   }

   // 3 - Get all items for a user

   private void getItems() {

       this.itemViewModel.getItems(USER_ID).observe(this, this::updateItemsList);
   }

   // 3 - Create a new item

   private void createItem() {

       itemViewModel.createItem(binding.todoListActivityEditText.getText().toString(), binding.todoListActivitySpinner.getSelectedItemPosition(), USER_ID);

       binding.todoListActivityEditText.setText("");

   }

   // 3 - Delete an item

   private void deleteItem(Item item) {

       this.itemViewModel.deleteItem(item.getId());

   }

   // 3 - Update an item (selected or not)

   private void updateItem(Item item) {

       item.setSelected(!item.getSelected());

       this.itemViewModel.updateItem(item);

   }

   // -------------------

   // UI

   // -------------------

   private void initView() {

       // 7 - Create item after user clicked on button

       binding.todoListActivityButtonAdd.setOnClickListener(view -> {

           createItem();

       });

       // 4 - Configure RecyclerView

       configureRecyclerView();

   }

   // 4 - Configure RecyclerView

   private void configureRecyclerView() {

       this.adapter = new ItemAdapter(this);

       binding.todoListActivityRecyclerView.setAdapter(this.adapter);

       binding.todoListActivityRecyclerView.setLayoutManager(new LinearLayoutManager(this));

   }

   // 5 - Update view (username & picture)

   private void updateView(User user) {

       if (user == null) return;

       binding.todoListActivityHeaderProfileText.setText(user.getUsername());

       Glide.with(this).load(user.getUrlPicture()).apply(RequestOptions.circleCropTransform()).into(binding.todoListActivityHeaderProfileImage);

   }

   // 6 - Update the list of items

   private void updateItemsList(List<Item> items) {

       this.adapter.updateData(items);

   }

}

 
Explications : Dans notre activité TodoListActivity, nous avons dans un premier temps déclaré (1) différentes variables comme notre ViewModel ItemViewModel, notre adapter, ainsi qu'une variable statique représentant l'identifiant de notre utilisateur (pour des besoins de tests).

Puis, nous avons créé une méthode (2) appelée   configureViewModel, qui nous servira à initialiser notre ViewModel. Comme vous pouvez le voir, nous récupérons une instance du   ViewModelFactorygrâce à son singleton. Grâce à cette Factory, nous allons pouvoir instancier notre variable   ItemViewModel, sans avoir à passer directement par son constructeur. :) Et enfin, une fois que notre ViewModel est récupéré, nous appelons sa méthode   init()  afin de récupérer une première fois l'utilisateur et de le stocker dans le ViewModel.

Nous avons également créé différentes méthodes privées (3) appelant les méthodes publiques de notre ViewModel afin d'observer leur résultat. Pour les méthodes de type Get, nous avons utilisé la méthode   observe()  pour être alertés automatiquement si le résultat en base de données change...  :)

Nous avons également utilisé les lambdas pour réduire notre expression et appeler la méthode   updateView()quand un changement se produit.

// AVEC LAMBDAS

private void getCurrentUser() {

   LiveData<User> userLiveData = itemViewModel.getUser();

   if (userLiveData != null) {

       userLiveData.observe(this, this::updateView);

   }

}

// SANS LAMBDAS

private void getCurrentUser() {

   LiveData<User> userLiveData = itemViewModel.getUser();

   if (userLiveData != null) {

       userLiveData.observe(this, new Observer<User>() {

           @Override

           public void onChanged(User user) {

               updateView(user);

           }

       });

   }

}

 Nous avons aussi créé (4) une méthode   configureRecyclerView()nous permettant, comme son nom l'indique, de configurer notre RecyclerView.

Nous avons ajouté également deux méthodes (5) et (6) mettant à jour notre interface graphique lorsque nous récupérons un objet représentant notre utilisateur (  User  ) ou une liste de choses à faire (   List<Item>  ).

On pense également à mettre à jour nos listeners (7) des boutons d'action et à implémenter l'interface   ItemAdapter.Listener  à notre activité pour gérer le clic sur le bouton de suppression et sur l’item pour le modifier.

Nous appelons nos méthodes de configuration (8) et de récupération du user et des items associés (9) dans le   onCreate()  de notre activité.

Exécutez votre application et jouez un petit peu avec. Elle devrait être maintenant 100% fonctionnelle... :D

Ah super ! Cependant, je me demandais s' il n'existait pas un moyen de visualiser notre base de données SQLite depuis notre PC, afin de faciliter son déboggage ?

Bien sûr ! Android Studio possède un inspecteur de base de données appelé Database Inspector. :) Il vous suffit de cliquer sur l’onglet “Database Inspector” en bas de l’écran et vous aurez accès à votre base de données en cours d’exécution sur votre émulateur ou smartphone. Vous pourrez visualiser les différentes tables mais aussi exécuter vos propres requêtes :

Exécution d’une requête après l’avoir écrite et avoir cliqué sur le bouton run
Exécution d’une requête après l’avoir écrite et avoir cliqué sur le bouton run
Visualisation d’une table en double-cliquant dessus dans la liste de gauche
Visualisation d’une table en double-cliquant dessus dans la liste de gauche

En résumé

  • L’activité délègue toute l'intelligence de la donnée à son ViewModel et s’occupe seulement de l’affichage.

  • Le ViewModel permet :

  1. de garder les données de la vue même lorsque celle-ci subit son cycle de vie ;

  2. de bien séparer la vue de la récupération des données.

  • Le repository permet d’ajouter une couche d’abstraction à la récupération des données afin que le viewmodel ne les manipule pas directement.

Vous savez maintenant comment architecturer correctement votre projet à l’aide de l’Architecture Components. Bravo ! Votre code est propre et facilement modifiable.

Je vous propose maintenant d’aller encore plus loin en regardant comment exposer notre base de données à d’autres applications.

Exemple de certificat de réussite
Exemple de certificat de réussite