• 20 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

Ce cours est en vidéo.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

J'ai tout compris !

Mis à jour le 30/04/2018

Architecturez votre application à l'aide d'un ViewModel

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

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.

Architecture désirée
Architecture désirée

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

Support de 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 ce qui suit...

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éation 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 DAOs 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 en soi, 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éation du 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(long userId) { return this.currentUser;  }

    // -------------
    // FOR ITEM
    // -------------

    public LiveData<List<Item>> getItems(long userId) {
        return itemDataSource.getItems(userId);
    }

    public void createItem(Item item) {
        executor.execute(() -> {
            itemDataSource.createItem(item);
        });
    }

    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 DAOs, afin de bénéficier automatiquement de la récupération asynchrone... ;)

Modification de 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  ItemViewModel  dans 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;

    public ViewModelFactory(ItemDataRepository itemDataSource, UserDataRepository userDataSource, Executor executor) {
        this.itemDataSource = itemDataSource;
        this.userDataSource = userDataSource;
        this.executor = executor;
    }

    @Override
    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 besoins 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() ... :)

Afin de mieux visualiser toute la logique de ce processus d'encapsulations assez complexe, nous allons terminer l'implémentation en créant une classe qui sera responsable d'injecter chaque objet dans le constructeur de notre Factory : on appelle ce procédé l'injection de dépendances. Je ne parlerai pas ici de ce concept très avancé, mais sachez qu'il existe des librairies spécialisées comme Dagger2 permettant de réaliser cela de manière plus automatisée

Ainsi, créons ensemble la classe Injection.java dans le package injection/.

Classe injection/Injection.java : 

public class Injection {

    public static ItemDataRepository provideItemDataSource(Context context) {
        SaveMyTripDatabase database = SaveMyTripDatabase.getInstance(context);
        return new ItemDataRepository(database.itemDao());
    }

    public static UserDataRepository provideUserDataSource(Context context) {
        SaveMyTripDatabase database = SaveMyTripDatabase.getInstance(context);
        return new UserDataRepository(database.userDao());
    }

    public static Executor provideExecutor(){ return Executors.newSingleThreadExecutor(); }

    public static ViewModelFactory provideViewModelFactory(Context context) {
        ItemDataRepository dataSourceItem = provideItemDataSource(context);
        UserDataRepository dataSourceUser = provideUserDataSource(context);
        Executor executor = provideExecutor();
        return new ViewModelFactory(dataSourceItem, dataSourceUser, executor);
    }
}

Explications : Cette classe sera responsable de fournir des objets déjà construits, de manière centralisée. Ainsi, dès lors que nous souhaiterons créer les objets présents dans cette classe n'importe où dans notre application, nous invoquerons directement une de ces méthodes publiques statiques au lieu de faire un new MonObjet(). Cela permet de rendre encore plus modulaire notre code, en évitant de créer des dépendances fortes entre chacune de nos classes.

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

Extrait de TodoListActivity.java : 

public class TodoListActivity extends BaseActivity implements ItemAdapter.Listener {

    ...

    // 1 - FOR DATA
    private ItemViewModel itemViewModel;
    private ItemAdapter adapter;
    private static int USER_ID = 1;

    ...
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        
        // 8 - Configure RecyclerView & ViewModel
        this.configureRecyclerView();
        this.configureViewModel();

        // 9 - Get current user & items from Database
        this.getCurrentUser(USER_ID);
        this.getItems(USER_ID);
    }

    // -------------------
    // ACTIONS
    // -------------------

    @OnClick(R.id.todo_list_activity_button_add)
    public void onClickAddButton() {
        // 7 - Create item after user clicked on button
        this.createItem();
    }

    @Override
    public void onClickDeleteButton(int position) {
        // 7 - Delete item after user clicked on button
        this.deleteItem(this.adapter.getItem(position));
    }

    // -------------------
    // DATA
    // -------------------

    // 2 - Configuring ViewModel
    private void configureViewModel(){
        ViewModelFactory mViewModelFactory = Injection.provideViewModelFactory(this);
        this.itemViewModel = ViewModelProviders.of(this, mViewModelFactory).get(ItemViewModel.class);
        this.itemViewModel.init(USER_ID);
    }

    // ---

    // 3 - Get Current User
    private void getCurrentUser(int userId){
        this.itemViewModel.getUser(userId).observe(this, this::updateHeader);
    }

    // ---

    // 3 - Get all items for a user
    private void getItems(int userId){
        this.itemViewModel.getItems(userId).observe(this, this::updateItemsList);
    }

    // 3 - Create a new item
    private void createItem(){
        Item item = new Item(this.editText.getText().toString(), this.spinner.getSelectedItemPosition(), USER_ID);
        this.editText.setText("");
        this.itemViewModel.createItem(item);
    }

    // 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
    // -------------------

    ...

    // 4 - Configure RecyclerView
    private void configureRecyclerView(){
        this.adapter = new ItemAdapter(this);
        this.recyclerView.setAdapter(this.adapter);
        this.recyclerView.setLayoutManager(new LinearLayoutManager(this));
        ItemClickSupport.addTo(recyclerView, R.layout.activity_todo_list_item)
                .setOnItemClickListener((recyclerView1, position, v) -> this.updateItem(this.adapter.getItem(position)));
    }

    // 5 - Update header (username & picture)
    private void updateHeader(User user){
        this.profileText.setText(user.getUsername());
        Glide.with(this).load(user.getUrlPicture()).apply(RequestOptions.circleCropTransform()).into(this.profileImage);
    }

    // 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 , et qui nous servira à initialiser notre ViewModel. Comme vous pouvez le voir, nous initialisons une variable  ViewModelFactory  à partir de notre classe  Injection que nous avons créée précédemment. 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 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é 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  updateHeader()  quand un changement se produit.

 // AVEC LAMBDAS
private void getCurrentUser(int userId){
    this.itemViewModel.getUser(userId).observe(this, this::updateHeader);
}

// SANS LAMBDAS
private void getCurrentUser(int userId){
    this.itemViewModel.getUser(userId).observe(this, new Observer<User>() {
        @Override
        public void onChanged(@Nullable User user) {
            updateHeader(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 enfin, nous appelons nos méthodes de configurations (8) et de récupération de données (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 si 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 ! Je vous conseille de jeter un coup d'œil à la librairie Stetho créée par Facebook, qui vous permettra entre autres de visualiser le contenu de votre base de données depuis votre navigateur web... :)

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