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.
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 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;
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 ViewModelFactory
grâ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 :
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 :
de garder les données de la vue même lorsque celle-ci subit son cycle de vie ;
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.