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
stackFromEndandtranscriptModeensures 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_SENTandVIEW_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
notifyItemInsertedare 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.xmlwould 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.