Maintenant que nos utilisateurs sont plus finement gérés grâce au Firestore, nous allons implémenter l'écran affichant le chat de discussion.
Implémentez les requêtes nécessaires au chat
L'écran que nous allons développer dans cette partie est assez simple, puisqu'il n'affichera que les 50 derniers messages issus de trois chats : Android, Firebase et Bug.
Avant toute chose, nous devons créer les requêtes réseau CRUD qui récupéreront depuis le Firestore les derniers messages envoyés sur l'ensemble de ces chats.
Sur le même modèle que pour la gestion de nos utilisateurs, nous allons implémenter deux classes : ChatRepository et ChatManager. Nous les placerons dans leur package respectifs "repository" et "manager".
Classe ChatRepository.java :
public final class ChatRepository {
private static final String CHAT_COLLECTION = "chats";
private static final String MESSAGE_COLLECTION = "messages";
private static volatile ChatRepository instance;
private ChatRepository() { }
public static ChatRepository getInstance() {
ChatRepository result = instance;
if (result != null) {
return result;
}
synchronized(ChatRepository.class) {
if (instance == null) {
instance = new ChatRepository();
}
return instance;
}
}
public CollectionReference getChatCollection(){
return FirebaseFirestore.getInstance().collection(CHAT_COLLECTION);
}
public Query getAllMessageForChat(String chat){
return this.getChatCollection()
.document(chat)
.collection(MESSAGE_COLLECTION)
.orderBy("dateCreated")
.limit(50);
}
}
Explications : Dans cette classe, nous avons créé une méthode (getChatCollection()
) nous permettant de créer une référence de la Collection racine "chats".
Nous avons créé une méthode (getAllMessageForChat()
) nous permettant, à partir de la Collection racine "chats", de récupérer le Document spécifié en paramètre (en l'occurence "Android", "Firebase" ou "Bug"), puis la Sous-Collection "messages", pour récupérer la liste des messages de ces chats.
Nous avons également ordonné orderBy("dateCreated")
(notre requête par date de création pour récupérer les messages les plus récents en premier). Pour finir, nous limitons à 50 le nombre de messages maximum (limit(50)
) que nous souhaitons récupérer.
Il nous reste à remplir notre classe ChatManager :
Classe ChatManager.java :
public class ChatManager {
private static volatile ChatManager instance;
private ChatRepository chatRepository;
private ChatManager() {
chatRepository = ChatRepository.getInstance();
}
public static ChatManager getInstance() {
ChatManager result = instance;
if (result != null) {
return result;
}
synchronized(ChatManager.class) {
if (instance == null) {
instance = new ChatManager();
}
return instance;
}
}
public Query getAllMessageForChat(String chat){
return chatRepository.getAllMessageForChat(chat);
}
}
Explications : Rien de spécial dans cette classe : nous avons une fonctiongetChatCollection()
. Elle fait appel à notre repository pour récupérer les messages.
Implémentez des éléments graphiques du chat
L'écran qui affichera notre chat sera représenté par l'activité MentorChatActivity qui contiendra notamment une RecyclerView.
Il nous reste tout de même à créer la classe de l'adapter (MentorChatAdapter) servant pour la RecyclerView ainsi que la classe de son ViewHolder (MessageViewHolder). Puis, nous allons les implémenter dans l'activité MentorChatActivity que nous allons créer également.
Nous placerons toutes ces nouvelles classes dans le sous-package chat, dans le package déjà existant ui.
Classe MessageViewHolder.java :
public class MessageViewHolder extends RecyclerView.ViewHolder {
private ItemChatBinding binding;
private final int colorCurrentUser;
private final int colorRemoteUser;
private boolean isSender;
public MessageViewHolder(@NonNull View itemView, boolean isSender) {
super(itemView);
this.isSender = isSender;
binding = ItemChatBinding.bind(itemView);
// Setup default colros
colorCurrentUser = ContextCompat.getColor(itemView.getContext(), R.color.colorAccent);
colorRemoteUser = ContextCompat.getColor(itemView.getContext(), R.color.colorPrimary);
}
public void updateWithMessage(Message message, RequestManager glide){
// Update message
binding.messageTextView.setText(message.getMessage());
binding.messageTextView.setTextAlignment(isSender ? View.TEXT_ALIGNMENT_TEXT_END : View.TEXT_ALIGNMENT_TEXT_START);
// Update date
if (message.getDateCreated() != null) binding.dateTextView.setText(this.convertDateToHour(message.getDateCreated()));
// Update isMentor
binding.profileIsMentor.setVisibility(message.getUserSender().getIsMentor() ? View.VISIBLE : View.INVISIBLE);
// Update profile picture
if (message.getUserSender().getUrlPicture() != null)
glide.load(message.getUserSender().getUrlPicture())
.apply(RequestOptions.circleCropTransform())
.into(binding.profileImage);
// Update image sent
if (message.getUrlImage() != null){
glide.load(message.getUrlImage())
.into(binding.senderImageView);
binding.senderImageView.setVisibility(View.VISIBLE);
} else {
binding.senderImageView.setVisibility(View.GONE);
}
updateLayoutFromSenderType();
}
private void updateLayoutFromSenderType(){
//Update Message Bubble Color Background
((GradientDrawable) binding.messageTextContainer.getBackground()).setColor(isSender ? colorCurrentUser : colorRemoteUser);
binding.messageTextContainer.requestLayout();
if(!isSender){
updateProfileContainer();
updateMessageContainer();
}
}
private void updateProfileContainer(){
// Update the constraint for the profile container (Push it to the left for receiver message)
ConstraintLayout.LayoutParams profileContainerLayoutParams = (ConstraintLayout.LayoutParams) binding.profileContainer.getLayoutParams();
profileContainerLayoutParams.endToEnd = ConstraintLayout.LayoutParams.UNSET;
profileContainerLayoutParams.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
binding.profileContainer.requestLayout();
}
private void updateMessageContainer(){
// Update the constraint for the message container (Push it to the right of the profile container for receiver message)
ConstraintLayout.LayoutParams messageContainerLayoutParams = (ConstraintLayout.LayoutParams) binding.messageContainer.getLayoutParams();
messageContainerLayoutParams.startToStart = ConstraintLayout.LayoutParams.UNSET;
messageContainerLayoutParams.endToStart = ConstraintLayout.LayoutParams.UNSET;
messageContainerLayoutParams.startToEnd = binding.profileContainer.getId();
messageContainerLayoutParams.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
messageContainerLayoutParams.horizontalBias = 0.0f;
binding.messageContainer.requestLayout();
// Update the constraint (gravity) for the text of the message (content + date) (Align it to the left for receiver message)
LinearLayout.LayoutParams messageTextLayoutParams = (LinearLayout.LayoutParams) binding.messageTextContainer.getLayoutParams();
messageTextLayoutParams.gravity = Gravity.START;
binding.messageTextContainer.requestLayout();
LinearLayout.LayoutParams dateLayoutParams = (LinearLayout.LayoutParams) binding.dateTextView.getLayoutParams();
dateLayoutParams.gravity = Gravity.BOTTOM | Gravity.START;
binding.dateTextView.requestLayout();
}
private String convertDateToHour(Date date){
DateFormat dfTime = new SimpleDateFormat("HH:mm", Locale.getDefault());
return dfTime.format(date);
}
}
Explications : Ce ViewHolder représente chaque ligne de la RecyclerView, et donc visuellement chaque message. Je vous laisse consulter le layout correspondant afin de vous en imprégner et comprendre la logique visuelle. Pour résumer, nous avons la méthode updateWithMessage()
qui mettra à jour les différentes vues du ViewHolder en fonction d'un objet Message passé en paramètre.
Puis, celle-ci appellera la méthode updateLayoutFromSenderType()
permettant de changer la position à l'écran des vues affichées (correspondants aux messages) grâce à la mise à jour de leurs paramètres de layout : à droite les messages que vous avez envoyés, à gauche les messages que vous avez reçus.
Classe MentorChatAdapter.java :
public class MentorChatAdapter extends FirestoreRecyclerAdapter<Message, MessageViewHolder> {
public interface Listener {
void onDataChanged();
}
// VIEW TYPES
private static final int SENDER_TYPE = 1;
private static final int RECEIVER_TYPE = 2;
private final RequestManager glide;
private Listener callback;
public MentorChatAdapter(@NonNull FirestoreRecyclerOptions<Message> options, RequestManager glide, Listener callback) {
super(options);
this.glide = glide;
this.callback = callback;
}
@Override
public int getItemViewType(int position) {
// Determine the type of the message by if the user is the sender or not
String currentUserId = UserManager.getInstance().getCurrentUser().getUid();
boolean isSender = getItem(position).getUserSender().getUid().equals(currentUserId);
return (isSender) ? SENDER_TYPE : RECEIVER_TYPE;
}
@Override
protected void onBindViewHolder(@NonNull MessageViewHolder holder, int position, @NonNull Message model) {
holder.itemView.invalidate();
holder.updateWithMessage(model, this.glide);
}
@Override
public MessageViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new MessageViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_chat, parent, false), viewType == 1);
}
@Override
public void onDataChanged() {
super.onDataChanged();
this.callback.onDataChanged();
}
}
Explications : Nous avons créé ici un Adapter qui semble plutôt simple à première vue. Cependant, ce dernier hérite de FirestoreRecyclerAdapter, objet disponible dans la librairie FirebaseUI.
Mais pourquoi ne pas faire hériter notre Adapter de RecyclerView.Adapter, comme d'habitude ?
Tout simplement car FirestoreRecyclerAdapter permet de gérer automatiquement et à notre place beaucoup de choses, comme la mise à jour en temps réel du RecyclerView afin de refléter exactement notre base de données Firestore (et plus particulièrement le résultat d'une requête Query), ou encore la mise en cache de nos données afin d'y avoir accès même sans Internet ! Je vous en parle juste après ! ;)
Par exemple, si un message est ajouté à notre chat sur le Firestore, une nouvelle ligne apparaîtra automatiquement dans la RecyclerView. De même, si un message est supprimé sur le Firestore, la ligne correspondante ne sera plus affiché dans la RecyclerView.
C'est aussi simple que cela !
Classe MentorChatActivity.java :
public class MentorChatActivity extends BaseActivity<ActivityMentorChatBinding> implements MentorChatAdapter.Listener {
private MentorChatAdapter mentorChatAdapter;
private String currentChatName;
private static final String CHAT_NAME_ANDROID = "android";
private static final String CHAT_NAME_BUG = "bug";
private static final String CHAT_NAME_FIREBASE = "firebase";
private UserManager userManager = UserManager.getInstance();
private ChatManager chatManager = ChatManager.getInstance();
@Override
protected ActivityMentorChatBinding getViewBinding() {
return ActivityMentorChatBinding.inflate(getLayoutInflater());
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
configureRecyclerView(CHAT_NAME_ANDROID);
setupListeners();
}
private void setupListeners(){
// Chat buttons
binding.androidChatButton.setOnClickListener(view -> { this.configureRecyclerView(CHAT_NAME_ANDROID); });
binding.firebaseChatButton.setOnClickListener(view -> { this.configureRecyclerView(CHAT_NAME_FIREBASE); });
binding.bugChatButton.setOnClickListener(view -> { this.configureRecyclerView(CHAT_NAME_BUG); });
}
// Configure RecyclerView
private void configureRecyclerView(String chatName){
//Track current chat name
this.currentChatName = chatName;
//Configure Adapter & RecyclerView
this.mentorChatAdapter = new MentorChatAdapter(
generateOptionsForAdapter(chatManager.getAllMessageForChat(this.currentChatName)),
Glide.with(this), this);
mentorChatAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
binding.chatRecyclerView.smoothScrollToPosition(mentorChatAdapter.getItemCount()); // Scroll to bottom on new messages
}
});
binding.chatRecyclerView.setLayoutManager(new LinearLayoutManager(this));
binding.chatRecyclerView.setAdapter(this.mentorChatAdapter);
}
// Create options for RecyclerView from a Query
private FirestoreRecyclerOptions<Message> generateOptionsForAdapter(Query query){
return new FirestoreRecyclerOptions.Builder<Message>()
.setQuery(query, Message.class)
.setLifecycleOwner(this)
.build();
}
public void onDataChanged() {
// Show TextView in case RecyclerView is empty
binding.emptyRecyclerView.setVisibility(this.mentorChatAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
}
}
Explications : Voici enfin l'implémentation de notre activité MentorChatActivity.
Nous déclarons différents objets comme l'Adapter pour la RecyclerView, les différents managers et une variable String représentant le chat sur lequel l'utilisateur souhaite discuter. Ces chats sont d'ailleurs identifiés par des variables statiques.
Au démarrage de l'activité, nous configurons la RecyclerView grâce à la méthode configureRecyclerView()
prenant en identifiant le nom du chat à afficher. Elle crée l'Adapter, et lui passe en paramètre l'objet FirestoreRecyclerOptions généré par la méthode generateOptionsForAdapter()
.
Cette dernière méthode, grâce à une requête Query que nous lui définissons (précédemment créée dans la classe ChatManager ), permettra à la RecyclerView d'afficher en temps réel le résultat de cette requête, en l'occurrence ici, la liste des derniers messages du chat correspondant ("Android", "Firebase" ou "Bug").
Enfin, nous implémentons le listener MentorChatAdapter.Listener
, qui nous permet d'être alerté si la liste de messages est vide, afin d'afficher un message à l'utilisateur via la méthode onDataChanged()
.
On oublie pas de déclarer cette nouvelle activité dans le manifeste de notre application.
Extrait de AndroidManifest.xml :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.openclassrooms.firebaseoc">
<application
...
>
...
<!-- MENTOR CHAT ACTIVITY -->
<activity android:name=".ui.chat.MentorChatActivity"
android:label="@string/toolbar_title_mentor_chat_activity"
android:parentActivityName=".ui.MainActivity"/>
</application>
</manifest>
Pour terminer, modifions l'activité MainActivity afin de lancer cette activité quand l'utilisateur clique sur le bouton "Discute avec un mentor".
Extrait de MainActivity :
public class MainActivity extends BaseActivity<ActivityMainBinding> {
...
private void setupListeners(){
...
// Chat Button
binding.chatButton.setOnClickListener(view -> {
if(userManager.isCurrentUserLogged()){
startMentorChatActivity();
}else{
showSnackBar(getString(R.string.error_not_connected));
}
});
}
...
// Launch Mentor Chat Activity
private void startMentorChatActivity(){
Intent intent = new Intent(this, MentorChatActivity.class);
startActivity(intent);
}
...
}
Explications : Nous créons ici simplement une méthode permettant de lancer notre activité MentorChatActivity quand l'utilisateur clique sur le bouton "Discute avec un mentor". On vérifie simplement que l'utilisateur est bien connecté avant de lancer l'activité.
Lancez maintenant votre application et cliquez sur le bouton "Discute avec un mentor". Vous ne pouvez pour le moment pas envoyer de message, c'est normal ! Nous verrons cela dès le prochain chapitre.
Bon je sais, nous avons écrit beaucoup de code dans ce chapitre ! Le but n'est pas de vous perdre complètement, mais de vous donner des automatismes visuels sur la manière dont vous devez écrire et organiser du code propre. N'hésitez donc pas à vous servir de cette mini-application comme modèle pour tous vos futurs projets !
En résumé
Afin d'afficher une liste de données stockées sur Firestore, il est judicieux d'utiliser un FirestoreRecyclerAdapter pour que les données soient implémentées et mises à jour facilement.
Pensez à gérer l'affichage de vos cellules dans votre ViewHolder (notamment l'affichage de l'orientation des messages).