Maintenant que Room (notre ORM) est installé, nous allons pouvoir commencer à représenter nos données sous forme de modèles (via des simples classes POJO). Le but sera de modéliser la fonctionnalité "liste de choses à faire" de notre écran TodoListActivity.
Pour y voir un peu plus clair, reprenons cet écran afin de mieux visualiser nos besoins et surtout les modèles que nous devrons créer.
Dans notre cas, j'ai choisi de modéliser cette fonctionnalité de la manière suivante :
La classe
User
: représentera l'utilisateur actuellement connecté à notre application. Ce dernier possèdera un nom et une photo de profil.La classe
Item
: représentera une chose à faire. Elle aura un titre ainsi qu'une catégorie et aura la possibilité d'être marquée comme étant "Faite". Elle gardera également en mémoire l'utilisateur qui l'aura créée.
Maintenant, créons les classes correspondantes en Java. :) Pour cela, créez un nouveau package appelé models/ puis ajoutez-y les classes User.java et Item.java.
Classe User.java :
public class User {
private long id;
private String username;
private String urlPicture;
public User(long id, String username, String urlPicture) {
this.id = id;
this.username = username;
this.urlPicture = urlPicture;
}
// --- GETTER ---
public long getId() { return id; }
public String getUsername() { return username; }
public String getUrlPicture() { return urlPicture; }
// --- SETTER ---
public void setId(long id) { this.id = id; }
public void setUsername(String username) { this.username = username; }
public void setUrlPicture(String urlPicture) { this.urlPicture = urlPicture; }
}
Classe Item.java :
public class Item {
private long id;
private String text;
private int category;
private Boolean isSelected;
private long userId;
public Item() { }
public Item(String text, int category, long userId) {
this.text = text;
this.category = category;
this.userId = userId;
this.isSelected = false;
}
// --- GETTER ---
public long getId() { return id; }
public String getText() { return text; }
public int getCategory() { return category; }
public Boolean getSelected() { return isSelected; }
public long getUserId() { return userId; }
// --- SETTER ---
public void setId(long id) { this.id = id; }
public void setText(String text) { this.text = text; }
public void setCategory(int category) { this.category = category; }
public void setSelected(Boolean selected) { isSelected = selected; }
public void setUserId(long userId) { this.userId = userId; }
}
Explications : Nous avons créé pour le moment ces deux objets qui semblent assez basiques. Nous leur avons ajouté à chacun un constructeur et des getters/setters.
Effectivement, ces classes modèles sont assez simples, mais comment faire pour les sauvegarder en base de données ? :euh:
Pour nous aider, j'ai créé dans un premier temps un Modèle Physique de Données (MPD) afin de mieux représenter les relations entre ces objets, qui deviendront par la suite... des tables ! :)
Nous avons ici deux tables, correspondant à nos deux précédents modèles :
la table
User
:clé primaire (PK) : le champ
id
représentant l'identifiant unique pour notre utilisateur ;
la table
Item
:clé primaire (PK) : le champ
id
représentant l'identifiant unique d'une "chose à faire" ;clé étrangère (FK) : le champ
userId
représentant l'utilisateur qui a créé la "chose à faire".
Maintenant que nous avons une représentation un peu plus avancée de la structure de notre base de données, nous allons modifier nos deux classes de modèles afin de les définir comme étant des tables aux yeux de Room.
Extrait de User.java :
@Entity
public class User {
@PrimaryKey
private long id;
}
Explications : Pour notre plus grand plaisir, Room nous propose énormément d'annotations afin de faciliter sa configuration. Ainsi, nous avons défini ici notre classe "User" comme étant une table grâce à l'annotation @Entity
.
Une table devant posséder au moins une clé primaire, nous définissons la propriété id
comme étant la clé primaire de la table "User" grâce à l'annotation @PrimaryKey
. Rapide et efficace, non ? :)
Extrait de Item.java :
@Entity(foreignKeys = @ForeignKey(entity = User.class,
parentColumns = "id",
childColumns = "userId"))
public class Item {
@PrimaryKey(autoGenerate = true)
private long id;
private long userId;
}
Explications : La table "Item" est un peu différente, car nous avons ajouté, à l'intérieur de l'annotation @Entity
, la relation clé-étrangère/clé-primaire grâce à l'annotation @ForeignKey
.
Nous aurions pu aussi utiliser les classes@Embedded
pour inclure la classe User
directement dans Item
sans se soucier des clés étrangères. La classe Item
ressemblerait à :
@Entity
public class Item {
@Embedded(prefix = "user_")
private User user;
Cette annotation va ajouter automatiquement les champs de notre classe User
dans Item
et ainsi créer une relation. À noter que nous devons ajouter le paramètre prefix
, car sinon notre table Item aurait 2 champs nommés id
, celui de Item
et celui de User
. Autrement dit, avec cette annotation, nous avons ajouté les champs de User
préfixé par “user_”, donc user_id, user_username et user_urlPicture.
Vous l'avez peut-être remarqué, mais nous avons ajouté le paramètreautoGenerate = true
à l'annotation @PrimaryKey : cela permettra à Room de générer automatiquement un identifiant unique pour chaque item sauvegardé. ;)
Une bonne chose de faite ! Maintenant, je vais vous donner simplement le contenu de l'Adapter et du ViewHolder que nous utiliserons par la suite à travers la RecyclerView de l'activité TodoListActivity. Et vous vous en doutez, la RecyclerView contiendra une liste... d'Item ! :D
Placez ces classes dans le package todolist/.
Classe todolist/ItemViewHolder.java :
public class ItemViewHolder extends RecyclerView.ViewHolder {
private final ActivityTodoListItemBinding binding;
public ItemViewHolder(ActivityTodoListItemBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void updateWithItem(Item item, ItemAdapter.Listener callback) {
binding.getRoot().setOnClickListener(view -> callback.onItemClick(item));
binding.activityTodoListItemRemove.setOnClickListener(view -> callback.onClickDeleteButton(item));
binding.activityTodoListItemText.setText(item.getText());
switch (item.getCategory()) {
case 0: // TO VISIT
binding.activityTodoListItemImage.setBackgroundResource(R.drawable.ic_room_black_24px);
break;
case 1: // IDEAS
binding.activityTodoListItemImage.setBackgroundResource(R.drawable.ic_lightbulb_outline_black_24px);
break;
case 2: // RESTAURANTS
binding.activityTodoListItemImage.setBackgroundResource(R.drawable.ic_local_cafe_black_24px);
break;
}
if (item.getSelected()) {
binding.activityTodoListItemText.setPaintFlags(binding.activityTodoListItemText.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
} else {
binding.activityTodoListItemText.setPaintFlags(binding.activityTodoListItemText.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG));
}
}
}
Explications : La classe représentant chaque ligne de la RecyclerView. Je ne l'explique pas plus que ça, car vous devez normalement comprendre assez facilement l'essentiel du code.
Classe todolist/ItemAdapter.java :
public class ItemAdapter extends RecyclerView.Adapter<ItemViewHolder> {
// CALLBACK
public interface Listener {
void onClickDeleteButton(Item item);
void onItemClick(Item item);
}
private final Listener callback;
// FOR DATA
private List<Item> items;
// CONSTRUCTOR
public ItemAdapter(Listener callback) {
this.items = new ArrayList<>();
this.callback = callback;
}
@Override
@NotNull
public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Context context = parent.getContext();
LayoutInflater inflater = LayoutInflater.from(context);
return new ItemViewHolder(ActivityTodoListItemBinding.inflate(inflater, parent, false));
}
@Override
public void onBindViewHolder(ItemViewHolder viewHolder, int position) {
viewHolder.updateWithItem(this.items.get(position), this.callback);
}
@Override
public int getItemCount() {
return this.items.size();
}
public void updateData(List<Item> items){
this.items = items;
this.notifyDataSetChanged();
}
}
Explications : Cette classe représente l'Adapter faisant le lien entre la RecyclerView et le ViewHolder. Je ne vais également pas plus l'expliquer que ça, car celle-ci reste assez compréhensible en soi. Si vous avez des difficultés à assimiler le fonctionnement du callback et de son interface, n'hésitez pas à relire le chapitre de ce cours.
En résumé
Pour définir qu’une classe est une table, on l’annote avec
@Entity
.On peut gérer des relations entre classe soit avec l’annotation
@ForeignKey
soit avec@Embedded
, la différence étant que cette dernière intégrera les champs de la table relationnelle.
Nos entités sont prêtes à être manipulées ! Nous allons voir dès le prochain chapitre comment créer nos premières requêtes SQL sur notre base de données SQLite.