Building a Chat UI with XML Layouts

While modern Android development increasingly leans towards Jetpack Compose for building UIs, a vast number of existing Android apps and projects still utilize XML layouts. Creating a functional and aesthetically pleasing chat UI using XML requires a thoughtful approach to layout design, efficient use of RecyclerViews, and effective management of UI elements. This blog post will guide you through the process of building a robust chat UI using XML layouts, covering essential aspects like displaying messages, handling user input, and optimizing for performance.

Understanding the Basics of Chat UI

A typical chat UI comprises several key components:

  • Message Display Area: This usually consists of a RecyclerView or similar component that displays the sequence of messages.
  • Input Field: An EditText where users can type their messages.
  • Send Button: A Button that triggers the sending of the message entered in the input field.
  • Avatar or Profile Picture (Optional): Images or icons representing the users sending and receiving messages.

Designing the Chat UI with XML Layouts

We’ll create a layout file (e.g., activity_chat.xml) for the main chat screen, which includes the RecyclerView for displaying messages and an input area for composing messages.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".ChatActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerViewMessages"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:padding="8dp"
        android:clipToPadding="false"
        android:stackFromEnd="true"
        android:transcriptMode="alwaysScroll"
        tools:listitem="@layout/item_message" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="8dp">

        <EditText
            android:id="@+id/editTextMessage"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:hint="Type a message..."
            android:inputType="textMultiLine"
            android:maxLines="4" />

        <Button
            android:id="@+id/buttonSend"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Send" />

    </LinearLayout>

</LinearLayout>

Key elements of this layout:

  • RecyclerView: Uses a RecyclerView to efficiently display a list of chat messages. Setting stackFromEnd and transcriptMode ensures new messages appear at the bottom of the screen.
  • LinearLayout: Wraps the input field and send button for a simple horizontal layout.
  • EditText: Provides the input field for typing messages, supporting multi-line input.
  • Button: The send button to submit messages.

Creating the Message Item Layout

The item_message.xml file defines how each message is displayed within the RecyclerView.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="8dp">

    <TextView
        android:id="@+id/textViewMessage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/message_bubble_sent"
        android:padding="12dp"
        android:textColor="@android:color/white"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="This is a sample message sent by the user." />

</androidx.constraintlayout.widget.ConstraintLayout>

This layout uses a TextView inside a ConstraintLayout to display the message. The message_bubble_sent drawable provides a background shape for the message.

For received messages, create a similar layout, such as item_message_received.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="8dp">

    <TextView
        android:id="@+id/textViewMessage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/message_bubble_received"
        android:padding="12dp"
        android:textColor="@android:color/black"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="This is a sample message received by the user." />

</androidx.constraintlayout.widget.ConstraintLayout>

Here, the message alignment is adjusted to the start of the layout, and a different background (message_bubble_received) and text color are used to distinguish received messages.

Implementing the RecyclerView Adapter

The adapter is crucial for populating the RecyclerView with chat messages. Let’s create a ChatMessageAdapter that handles displaying different message types (sent and received).

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;

public class ChatMessageAdapter extends RecyclerView.Adapter<ChatMessageAdapter.ChatMessageViewHolder> {

    private List<ChatMessage> messages;
    private static final int VIEW_TYPE_SENT = 1;
    private static final int VIEW_TYPE_RECEIVED = 2;

    public ChatMessageAdapter(List<ChatMessage> messages) {
        this.messages = messages;
    }

    @NonNull
    @Override
    public ChatMessageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view;
        if (viewType == VIEW_TYPE_SENT) {
            view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_message, parent, false);
        } else {
            view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_message_received, parent, false);
        }
        return new ChatMessageViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull ChatMessageViewHolder holder, int position) {
        ChatMessage message = messages.get(position);
        holder.textViewMessage.setText(message.getText());
    }

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

    @Override
    public int getItemViewType(int position) {
        ChatMessage message = messages.get(position);
        if (message.isSentByUser()) {
            return VIEW_TYPE_SENT;
        } else {
            return VIEW_TYPE_RECEIVED;
        }
    }

    public static class ChatMessageViewHolder extends RecyclerView.ViewHolder {
        TextView textViewMessage;

        public ChatMessageViewHolder(@NonNull View itemView) {
            super(itemView);
            textViewMessage = itemView.findViewById(R.id.textViewMessage);
        }
    }

    // Helper method to update messages
    public void updateMessages(List<ChatMessage> newMessages) {
        this.messages = newMessages;
        notifyDataSetChanged();
    }

    // Inner class to represent chat messages
    public static class ChatMessage {
        private String text;
        private boolean sentByUser;

        public ChatMessage(String text, boolean sentByUser) {
            this.text = text;
            this.sentByUser = sentByUser;
        }

        public String getText() {
            return text;
        }

        public boolean isSentByUser() {
            return sentByUser;
        }
    }
}

Key components of this adapter:

  • View Types: Uses different view types (VIEW_TYPE_SENT and VIEW_TYPE_RECEIVED) to display sent and received messages using corresponding layouts.
  • onCreateViewHolder: Inflates the appropriate layout based on the view type.
  • onBindViewHolder: Binds the message data to the views in the ViewHolder.
  • getItemViewType: Determines the view type based on the message’s sender.

Implementing the Chat Activity

In your ChatActivity, you’ll need to initialize the RecyclerView, handle user input, and update the UI with new messages.

import android.os.Bundle;
import android.widget.Button;
import android.widget.EditText;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;

public class ChatActivity extends AppCompatActivity {

    private RecyclerView recyclerViewMessages;
    private EditText editTextMessage;
    private Button buttonSend;
    private ChatMessageAdapter chatMessageAdapter;
    private List<ChatMessageAdapter.ChatMessage> messages = new ArrayList<>();

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

        recyclerViewMessages = findViewById(R.id.recyclerViewMessages);
        editTextMessage = findViewById(R.id.editTextMessage);
        buttonSend = findViewById(R.id.buttonSend);

        // Initialize RecyclerView and Layout Manager
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        recyclerViewMessages.setLayoutManager(layoutManager);

        // Initialize Adapter
        chatMessageAdapter = new ChatMessageAdapter(messages);
        recyclerViewMessages.setAdapter(chatMessageAdapter);

        // Example messages
        messages.add(new ChatMessageAdapter.ChatMessage("Hello, how are you?", false));
        messages.add(new ChatMessageAdapter.ChatMessage("I'm doing great, thanks!", true));
        chatMessageAdapter.updateMessages(messages);
        //recyclerViewMessages.scrollToPosition(messages.size() - 1); // scroll to the last message.

        // Set onClickListener for send button
        buttonSend.setOnClickListener(view -> {
            String messageText = editTextMessage.getText().toString().trim();
            if (!messageText.isEmpty()) {
                // Create new message object
                ChatMessageAdapter.ChatMessage newMessage = new ChatMessageAdapter.ChatMessage(messageText, true);

                // Add message to the list and update the adapter
                messages.add(newMessage);
                chatMessageAdapter.updateMessages(messages);
                recyclerViewMessages.scrollToPosition(messages.size() - 1); // scroll to the last message.

                // Clear EditText
                editTextMessage.setText("");
            }
        });
    }
}

Key steps in the activity implementation:

  • UI Initialization: Get references to the RecyclerView, EditText, and Button.
  • RecyclerView Setup: Initialize the LinearLayoutManager and set the ChatMessageAdapter on the RecyclerView.
  • Message Sending: Implement the button click listener to capture the message text, create a ChatMessage object, and add it to the message list.
  • UI Update: Call chatMessageAdapter.updateMessages() to refresh the RecyclerView with the updated message list.
  • Scroll to Bottom: recyclerViewMessages.scrollToPosition(messages.size() - 1); is used to display the latest messages at the bottom, simulating the real-time behavior of a chat UI.

Enhancements and Best Practices

  • Using Different Layouts for Messages: Implementing separate layouts for received and sent messages provides visual distinction.
  • Keeping RecyclerView Updated: Methods such as notifyItemInserted are available on the adapter if there’s need to reflect single item change instead of refreshing whole list on every insert, use them if that works best for your needs.
  • Adding Bubble Backgrounds: Creating background drawables to display sent and received messages provides a more modern-looking layout. Create two different shape drawables, one for sent and one for received messages. Example drawable message_bubble_sent.xml would look like the following:
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="#3490dc"/>
    <corners android:radius="8dp"/>
</shape>
  • Performance Optimization: Implementing View Holder Pattern using RecyclerViews provides enhanced scrolling experience. Using the ListAdapter implementation with DiffUtil’s can automate RecyclerView animations based on contents, if so required for project.
  • Loading User Profile Pictures/Avatars: Showing user avatars associated with message can add personalized experience to user. These user profile pictures and messages can come through APIs. Libraries such as Glide, Picasso are useful in image loading.

Conclusion

While modern Android development often involves Jetpack Compose, using XML layouts for building UIs remains relevant, particularly in existing applications and projects. Constructing a chat UI with XML layouts requires careful planning and structuring of XML layouts along with an understanding of how RecyclerView adapters work in updating content dynamically in layouts, without re-rendering content that does not need it.