Skip to main content

[Make-A] Simple Chat Application using Cloud Firestore Part 8 – Chat

As a user,
When I enter a room,
I want to be able to chat,
So that I can talk with other people
  • Display a field where the user can enter his/her message
  • Display something that the user can press to send his/her message
  • The user should not be able to send an empty message
As a user,
When I enter a room,
I want to see all messages sorted by date sent in descending order,
So that I can see the latest messages
  • Display messages that the user sent to the right
  • Display messages that the user didn’t send to the left

I’d like to congratulate you that you’ve made it this far. 🎉🎉🎉 Just one last push and you’re almost done. These are the last features that we are going to implement.

Table of Contents

  1. Part 1 – Specs and Introduction
  2. Part 2 – Setting up Firebase Cloud Firestore
  3. Part 3 – Adding a Simple Authentication
  4. Part 4 – Adding a FloatingActionButton (FAB)
  5. Part 5 – Creating a Chat Room
  6. Part 6 – Entering The Chat Room
  7. Part 7 – Displaying The Chat Rooms
  8. Part 8 – Chat
  9. 10 Exercises and Thanks!

 

1. Setting up our UI

1. At this point, I’m confident you already know how to copy the icons to your project. Download this icon : https://material.io/icons/#ic_send

2. Open your strings.xml and add 3 new strings

<resources>
    ...
    <string name="hint_message_text">Type a message&#8230;</string>
    <string name="error_empty_message">Please enter a message</string>
    <string name="error_message_failed">Failed to send your message</string>
</resources>

3. Open activity_chat_room.xml. Let’s add a field where we can type our message and a button to send our message.

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/chats"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        app:layout_constraintBottom_toTopOf="@+id/message_text"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageButton
        android:id="@+id/send_message"
        style="?android:borderlessButtonStyle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:adjustViewBounds="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:srcCompat="@drawable/ic_send_black_24dp" />

    <EditText
        android:id="@+id/message_text"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:hint="@string/hint_message_text"
        android:inputType="textMultiLine|textPersonName"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/send_message"
        app:layout_constraintStart_toStartOf="parent" />

</android.support.constraint.ConstraintLayout>

4. Right click your /drawable folder under res/ folder and select Drawable resource file. Set file name to chat_received_rounded_background and set root element to shape.

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@android:color/darker_gray" />
    <corners android:radius="24dp" />
</shape>

5. Right click your /drawable folder under res/ folder and select Drawable resource file. Set file name to chat_sent_rounded_background and set root element to shape.

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@color/colorAccent" />
    <corners android:radius="24dp" />
</shape>

 

1.1 Setting up our RecyclerView.Adapter

Let’s create two new layouts that we will use to differentiate between a message that we received and the message that we sent. Just like the one you see in Messenger where your messages are at the right and other messages are at the left.

1. Create a new layout called item_chat_received.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="8dp"
    android:orientation="vertical">

    <TextView
        android:id="@+id/chat_message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="start"
        android:background="@drawable/chat_received_rounded_background"
        android:padding="8dp" />

</LinearLayout>

2. Create a new layout called item_chat_sent.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="8dp"
    android:orientation="vertical">

    <TextView
        android:id="@+id/chat_message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="end"
        android:background="@drawable/chat_sent_rounded_background"
        android:padding="8dp"
        android:textColor="@android:color/white" />

</LinearLayout>

3. Create a new pojo class called Chat

public class Chat {

    private String id;
    private String chatRoomId;
    private String senderId;
    private String message;
    private long sent;

    public Chat(String id, String chatRoomId, String senderId, String message, long sent) {
        this.id = id;
        this.chatRoomId = chatRoomId;
        this.senderId = senderId;
        this.message = message;
        this.sent = sent;
    }


    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getChatRoomId() {
        return chatRoomId;
    }

    public void setChatRoomId(String chatRoomId) {
        this.chatRoomId = chatRoomId;
    }

    public String getSenderId() {
        return senderId;
    }

    public void setSenderId(String senderId) {
        this.senderId = senderId;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public long getSent() {
        return sent;
    }

    public void setSent(long sent) {
        this.sent = sent;
    }
}

4. Create a new class called ChatsAdapter

public class ChatsAdapter extends RecyclerView.Adapter<ChatsAdapter.ChatViewHolder> {
    private static final int SENT = 0;
    private static final int RECEIVED = 1;

    private String userId;
    private List<Chat> chats;

    public ChatsAdapter(List<Chat> chats, String userId) {
        this.chats = chats;
        this.userId = userId;
    }

    @Override
    public ChatViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view;
        if (viewType == SENT) {
            view = LayoutInflater.from(parent.getContext()).inflate(
                    R.layout.item_chat_sent,
                    parent,
                    false
            );
        } else {
            view = LayoutInflater.from(parent.getContext()).inflate(
                    R.layout.item_chat_received,
                    parent,
                    false);
        }
        return new ChatViewHolder(view);
    }

    @Override
    public void onBindViewHolder(ChatViewHolder holder, int position) {
        holder.bind(chats.get(position));
    }

    @Override
    public int getItemViewType(int position) {
        if (chats.get(position).getSenderId().contentEquals(userId)) {
            return SENT;
        } else {
            return RECEIVED;
        }
    }

    @Override
    public int getItemCount() {
        return chats.size();
    }

    class ChatViewHolder extends RecyclerView.ViewHolder {
        TextView message;

        public ChatViewHolder(View itemView) {
            super(itemView);
            message = itemView.findViewById(R.id.chat_message);
        }

        public void bind(Chat chat) {
            message.setText(chat.getMessage());
        }
    }
}

 

2. Sending a chat message

1. Open your ChatRoomRepository.class. Add a new method called addMessageToChatRoom()

public class ChatRoomRepository {
    ...

    public void addMessageToChatRoom(String roomId,
                                     String senderId,
                                     String message,
                                     final OnSuccessListener<DocumentReference> successCallback,
                                     final OnFailureListener failureCallback) {
        Map<String, Object> chat = new HashMap<>();
        chat.put("chat_room_id", roomId);
        chat.put("sender_id", senderId);
        chat.put("message", message);
        chat.put("sent", System.currentTimeMillis());

        db.collection("chats")
                .add(chat)
                .addOnSuccessListener(new OnSuccessListener<DocumentReference>() {
                    @Override
                    public void onSuccess(DocumentReference documentReference) {
                        successCallback.onSuccess(documentReference);
                    }
                })
                .addOnFailureListener(new OnFailureListener() {
                    @Override
                    public void onFailure(@NonNull Exception e) {
                        failureCallback.onFailure(e);
                    }
                });
    }
}

2. Open your ChatRoomActivity.class

public class ChatRoomActivity extends AppCompatActivity {

    private static final String CURRENT_USER_KEY = "CURRENT_USER_KEY";
    ...

    ...
    private String userId = "";

    private ChatRoomRepository chatRoomRepository;

    private EditText message;
    private ImageButton send;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_chat_room);

        chatRoomRepository = new ChatRoomRepository(FirebaseFirestore.getInstance());

        ...

        userId = getCurrentUserKey();

        initUI();
    }

    private String getCurrentUserKey() {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        return preferences.getString(CURRENT_USER_KEY, "");
    }

    private void initUI() {
        message = findViewById(R.id.message_text);
        send = findViewById(R.id.send_message);

        send.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (message.getText().toString().isEmpty()) {
                    Toast.makeText(
                            ChatRoomActivity.this,
                            getString(R.string.error_empty_message),
                            Toast.LENGTH_SHORT
                    ).show();
                } else {
                    addMessageToChatRoom();
                }
            }
        });
    }

    private void addMessageToChatRoom() {
        String chatMessage = message.getText().toString();
        message.setText("");
        send.setEnabled(false);
        chatRoomRepository.addMessageToChatRoom(
                roomId,
                userId,
                chatMessage,
                new OnSuccessListener<DocumentReference>() {
                    @Override
                    public void onSuccess(DocumentReference documentReference) {
                        send.setEnabled(true);
                    }
                },
                new OnFailureListener() {
                    @Override
                    public void onFailure(@NonNull Exception e) {
                        send.setEnabled(true);
                        Toast.makeText(
                                ChatRoomActivity.this,
                                getString(R.string.error_message_failed),
                                Toast.LENGTH_SHORT
                        ).show();
                    }
                }
        );
    }
}

3. Launch the app, open a room, type a message and tap send. You should see the chats collection in your Firebase project console with your message. 🎉🎉🎉

 

3. Fetch and display all chat messages in a chat room

1. Open your ChatRoomRepository.class. Add a new method called getChats()

public class ChatRoomRepository {
    private static final String TAG = "ChatRoomRepo";

    private FirebaseFirestore db;

    public ChatRoomRepository(FirebaseFirestore db) {
        this.db = db;
    }

    ...

    public void getChats(String roomId, EventListener<QuerySnapshot> listener) {
        db.collection("chats")
                .whereEqualTo("chat_room_id", roomId)
                .orderBy("sent", Query.Direction.DESCENDING)
                .addSnapshotListener(listener);
    }

    ...
}

2. Open your ChatRoomActivity.class. Let’s fetch all the chat messages and display it in our RecyclerView. We have already setup our RecyclerView.Adapter above. It’s time to use it.

public class ChatRoomActivity extends AppCompatActivity {

    ...
    private RecyclerView chats;
    private ChatsAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_chat_room);

        ...

        initUI();

        showChatMessages();
    }

    ...

    private void initUI() {
        message = findViewById(R.id.message_text);
        send = findViewById(R.id.send_message);
        chats = findViewById(R.id.chats);
        LinearLayoutManager manager = new LinearLayoutManager(this);
        manager.setReverseLayout(true);
        chats.setLayoutManager(manager);

        ...
    }

    ...

    private void showChatMessages() {
        chatRoomRepository.getChats(roomId, new EventListener<QuerySnapshot>() {
            @Override
            public void onEvent(QuerySnapshot snapshots, FirebaseFirestoreException e) {
                if (e != null) {
                    Log.e("ChatRoomActivity", "Listen failed.", e);
                    return;
                }

                List<Chat> messages = new ArrayList<>();
                for (QueryDocumentSnapshot doc : snapshots) {
                    messages.add(
                            new Chat(
                                    doc.getId(),
                                    doc.getString("chat_room_id"),
                                    doc.getString("sender_id"),
                                    doc.getString("message"),
                                    doc.getLong("sent")
                            )
                    );
                }

                adapter = new ChatsAdapter(messages, userId);
                chats.setAdapter(adapter);
            }
        });
    }
}

 

3.1 Launch the app and fix “the query requires an index” Firestore error

1. If you launched the app, you should see an error like this:

2. Just click on the link provided in the error. Click Create Index and wait for it to build.

If you want to know why we got that error, you can check this link https://firebase.google.com/docs/firestore/query-data/indexing?authuser=0.

Basically, if we do compound queries we need to add an index. If you take a look at our query:

public void getChats(String roomId, EventListener<QuerySnapshot> listener) {
    db.collection("chats")
            .whereEqualTo("chat_room_id", roomId)
            .orderBy("sent", Query.Direction.DESCENDING)
            .addSnapshotListener(listener);
}

Looking at the code we have a compound query – whereEqualTo() and orderBy(). Taking into account two attributes also – chat_room_id and sent.

 

4. Let’s test it out!

1. Launch the app again. Launch another app on an emulator for example or another mobile device. Make sure you enter the same room and you should see something like this (please wait for a few seconds for this gif):

I’m using an another emulator to have a conversation with myself 😂

 

5. Review

As a user,
When I enter a room,
I want to be able to chat,
So that I can talk with other people
  • Display a field where the user can enter his/her message
  • Display something that the user can press to send his/her message
  • The user should not be able to send an empty message

✓ Display a field where the user can enter his/her message
✓ Display something that the user can press to send his/her message
✓ The user should not be able to send an empty message

As a user,
When I enter a room,
I want to see all messages sorted by date sent in descending order,
So that I can see the latest messages
  • Display messages that the user sent to the right
  • Display messages that the user didn’t send to the left

✓ Display messages that the user sent to the right
✓ Display messages that the user didn’t send to the left

 

CONGRATULATIONS!!! 🎉🎉🎉

You’ve just completed the challenge! You should be proud of yourself! You’re making progress and improving a lot just by completing this challenge.

Where to go from here?

Here are 10 exercises that can make your chat app feature-rich and resumé/cv-worthy