Android Chat Tutorial: Building a Messaging UI

Despite their growing prevalence, chat and messaging apps continue to be tedious to implement. Official documentation is non-existent, while unofficial tutorials are scarce and generally outdated.

 

Google Allo's Android chat UI

 

Colorful chat bubbles filled with text, images, even playable media--how do they do it? The answer is surprisingly simple: RecyclerViews, coupled with the ViewHolder pattern.

This tutorial walks you through the steps required to build a quality messaging UI.

 

Setting up the Activity

In this tutorial, we'll create a dedicated activity to host our messages. Let's call it MessageListActivity. The primary component in this activity is a RecyclerView that fills most of the screen. To keep ahead of the curve, a ConstraintLayout will be used as the root ViewGroup.

In addition to displaying message history, chat UIs typically allow the user to send messages through a chatbox. We'll position this chatbox at the bottom of the screen using constraints. This will anchor it to the soft input keyboard that pops up when the user types.

 

/layout/activity_message_list.xml

<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.sendbird.chattutorial.MessageListActivity">

<android.support.v7.widget.RecyclerView
android:id="@+id/reyclerview_message_list"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
</android.support.v7.widget.RecyclerView>

<!-- A horizontal line between the chatbox and RecyclerView -->
<View
android:layout_width="0dp"
android:layout_height="2dp"
android:background="#dfdfdf"
android:layout_marginBottom="0dp"
app:layout_constraintBottom_toTopOf="@+id/layout_chatbox"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"/>

<LinearLayout
android:id="@+id/layout_chatbox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:minHeight="48dp"
android:background="#ffffff"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent">

<EditText
android:id="@+id/edittext_chatbox"
android:hint="Enter message"
android:background="@android:color/transparent"
android:layout_gravity="center"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:maxLines="6"/>

<Button
android:id="@+id/button_chatbox_send"
android:text="SEND"
android:textSize="14dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:layout_width="64dp"
android:layout_height="48dp"
android:gravity="center"
android:layout_gravity="bottom" />
</LinearLayout>

</android.support.constraint.ConstraintLayout>

This creates two elements:

  1.  A flexible chatbox that expands according to the length of the message being typed
  2.  A RecyclerView that designates the remaining space to past messages

It will look like the following image:

 

layout 1

 

Creating a list item for each message

Use a separate XML file to define the layout of each item (i.e. message) for ListViews and RecyclerViews. It is common practice for each message to contain the sender’s name, their profile image, and a timestamp.

 

Screen Shot 2017-04-04 at 5.09.24 PM

 

Since the chat bubble's alignment typically indicates the message's owner, it could be redundant to display the sender's profile image and name for every message. In the above image, for example, the right-aligned chat bubble indicates that it belongs to the sender, so we need not include the image or name. Most chat clients choose to discard these components altogether. We will do the same by creating two different ViewHolder's:

  1. SentMessageHolder for messages that “I” have sent
  2. ReceivedMessageHolder for messages sent by others

To create the above image, we must first create a chat bubble in an XML file. Once again, this is surprisingly simple:

  1. Set a rounded rectangle as the background for TextView
  2. Adjust the paddings
  3. Align the text

/drawable/rounded_rectangle_orange.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle" >

<!-- View background color -->
<solid
android:color="#FF6E00" >
</solid>

<!-- The radius makes the corners rounded -->
<corners
android:radius="20dp">
</corners>
</shape>

<TextView
android:text="hi man, how are you?"
android:background="@drawable/rounded_rectangle_orange"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:textColor="#ffffff"
android:layout_marginTop="4dp"/>

Voila! We now have a handsome chat bubble housing the message.

 

Screen Shot 2017-04-04 at 5.19.36 PM

 

You can appropriately position the other TextViews using constraints. Be sure to create two more constraints before moving on:

  1. Be sure to set your root layout’s height to wrap_content, so it can accommodate messages containing a single line and messages containing more than a single line
  2. Set the message body’s max_width to an appropriate threshold. Without this constraint, the chat bubble will fill the entire screen's width, and will look clumsy.

Now let's apply this model to both the received and sent messages.

 

Received Messages

/layout/item_message_received.xml

<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp">

<ImageView
android:id="@+id/image_message_profile"
android:layout_width="32dp"
android:layout_height="32dp"
android:background="@drawable/circle"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginLeft="8dp"
app:layout_constraintLeft_toLeftOf="parent" />

<TextView
android:id="@+id/text_message_name"
android:text="John Doe"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
app:layout_constraintLeft_toRightOf="@+id/image_message_profile"
android:layout_marginLeft="8dp"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="4dp" />

<TextView
android:id="@+id/text_message_body"
android:text="hi man, how are you?"
android:background="@drawable/rounded_rectangle_orange"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxWidth="240dp"
android:padding="8dp"
android:textColor="#ffffff"
android:layout_marginTop="4dp"
app:layout_constraintTop_toBottomOf="@+id/text_message_name"
app:layout_constraintLeft_toRightOf="@+id/image_message_profile"
android:layout_marginLeft="8dp" />

<TextView
android:id="@+id/text_message_time"
android:text="11:40"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="10sp"
app:layout_constraintLeft_toRightOf="@+id/text_message_body"
android:layout_marginLeft="4dp"
app:layout_constraintBottom_toBottomOf="@+id/text_message_body" />

</android.support.constraint.ConstraintLayout>

This produces the following image for received messages.

 

Screen Shot 2017-04-04 at 5.25.31 PM

 

Sent Messages

For sent messages, align the chat bubble to the right, and remove the sender's name and profile image.

/layout/item_message_sent.xml

<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp">

<TextView
android:id="@+id/text_message_body"
android:text="hello, hello!"
android:background="@drawable/rounded_rectangle_green"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxWidth="240dp"
android:padding="8dp"
android:textColor="#ffffff"
android:layout_marginRight="8dp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/text_message_time"
android:text="11:40"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="10sp"
android:layout_marginRight="4dp"
app:layout_constraintBottom_toBottomOf="@+id/text_message_body"
app:layout_constraintRight_toLeftOf="@+id/text_message_body" />

</android.support.constraint.ConstraintLayout>

 

Creating an Adapter for the RecyclerView

Populate the RecyclerView with the above XML items by creating an adapter that extends RecyclerView.Adapter. This adapter will accomplish three things:

  1. Store a list of messages
  2. Determine whether a message is sent or received
  3. Inflate the appropriate layout within RecyclerView.

But before venturing any further, we need a list of messages!

Engineering a backend for real-time messaging is, however, beyond the scope of this tutorial. We'll therefore proceed with the models in SendBird’s chat SDK. It roughly assumes the following structure:

Generic Message and User models

class Message {
String message;
User sender;
long createdAt;
}

class User {
String nickname;
String profileUrl;
}

Since it is common to design messaging implementations in this manner, you will hopefully be able to follow this tutorial with minimal changes to your code.

Now, let's create an adapter and name it MessageListAdapter. It will hold a List of Messages that will be displayed within a RecyclerView.

 

MessageListAdapter.java

public class MessageListAdapter extends RecyclerView.Adapter {

private Context mContext;
private List<BaseMessage> mMessageList;

public MessageListAdapter(Context context, List<BaseMessage> messageList) {
mContext = context;
mMessageList = messageList;
}
}

By default, an adapter requires at least one RecyclerView.ViewHolder. It is a layout that can be inflated and bound by the objects it holds. In our case, two ViewHolders are needed:

  1. SentMessageHolder to hold messages that "I" sent
  2. ReceivedMessageHolder to hold messages that “I” have received from others

We'll write these to be private inner classes of MessageListAdapter.

Each ViewHolder holds member views that can be bound to particular information contained in a message. For example, TextView messageText binds to a message’s content, while nameText binds to the name of a message’s sender.

Antonio Leiva suggests that we implement a bind(object) method within the ViewHolder class. This gives view binding to the ViewHolder class rather than to onBindViewHolder. It therefore produces cleaner code amidst multiple ViewHolders and ViewTypes. It also allows us to add easily OnClickListeners, if necessary.

 

ReceivedMessageHolder.java

private class ReceivedMessageHolder extends RecyclerView.ViewHolder {
TextView messageText, timeText, nameText;
ImageView profileImage;

ReceivedMessageHolder(View itemView) {
super(itemView);

messageText = (TextView) itemView.findViewById(R.id.text_message_body);
timeText = (TextView) itemView.findViewById(R.id.text_message_time);
nameText = (TextView) itemView.findViewById(R.id.text_message_name);
profileImage = (ImageView) itemView.findViewById(R.id.image_message_profile);
}

void bind(UserMessage message) {
messageText.setText(message.getMessage());

// Format the stored timestamp into a readable String using method.
timeText.setText(Utils.formatDateTime(message.getCreatedAt()));

nameText.setText(message.getSender().getNickname());

// Insert the profile image from the URL into the ImageView.
Utils.displayRoundImageFromUrl(mContext, message.getSender().getProfileUrl(), profileImage);
}
}

SentMessageHolder will be identical except that it will not have nameText and profileImage.

Once you complete ViewHolder, turn back to the adapter and override the necessary methods to

  1. Obtain a message from MessageList
  2. Determine whether it is a sent message or a received message
  3. Inflate the appropriate layout for the ViewHolder
  4. Pass the message to the ViewHolder for binding

Implementing this process is standard for most types of adapters, and pretty self-explanatory; therefore, we'll show the code for the full adapter without further explanation.

 

MessageListAdapter.java

public class MessageListAdapter extends RecyclerView.Adapter {

private static final int VIEW_TYPE_MESSAGE_SENT = 1;
private static final int VIEW_TYPE_MESSAGE_RECEIVED = 2;

private Context mContext;
private List<BaseMessage> mMessageList;

public MessageListAdapter(Context context, List<BaseMessage> messageList) {
mContext = context;
mMessageList = messageList;
}

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

// Determines the appropriate ViewType according to the sender of the message.
@Override
public int getItemViewType(int position) {
UserMessage message = (UserMessage) mMessageList.get(position);

if (message.getSender().getUserId().equals(SendBird.getCurrentUser().getUserId())) {
// If the current user is the sender of the message
return VIEW_TYPE_MESSAGE_SENT;
} else {
// If some other user sent the message
return VIEW_TYPE_MESSAGE_RECEIVED;
}
}

// Inflates the appropriate layout according to the ViewType.
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view;

if (viewType == VIEW_TYPE_MESSAGE_SENT) {
view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_message_sent, parent, false);
return new SentMessageHolder(view);
} else if (viewType == VIEW_TYPE_MESSAGE_RECEIVED) {
view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_message_received, parent, false);
return new ReceivedMessageHolder(view);
}

return null;
}

// Passes the message object to a ViewHolder so that the contents can be bound to UI.
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
UserMessage message = (UserMessage) mMessageList.get(position);

switch (holder.getItemViewType()) {
case VIEW_TYPE_MESSAGE_SENT:
((SentMessageHolder) holder).bind(message);
break;
case VIEW_TYPE_MESSAGE_RECEIVED:
((ReceivedMessageHolder) holder).bind(message);
}
}

private class SentMessageHolder extends RecyclerView.ViewHolder {
TextView messageText, timeText;

SentMessageHolder(View itemView) {
super(itemView);

messageText = (TextView) itemView.findViewById(R.id.text_message_body);
timeText = (TextView) itemView.findViewById(R.id.text_message_time);
}

void bind(UserMessage message) {
messageText.setText(message.getMessage());

// Format the stored timestamp into a readable String using method.
timeText.setText(Utils.formatDateTime(message.getCreatedAt()));
}
}

private class ReceivedMessageHolder extends RecyclerView.ViewHolder {
TextView messageText, timeText, nameText;
ImageView profileImage;

ReceivedMessageHolder(View itemView) {
super(itemView);

messageText = (TextView) itemView.findViewById(R.id.text_message_body);
timeText = (TextView) itemView.findViewById(R.id.text_message_time);
nameText = (TextView) itemView.findViewById(R.id.text_message_name);
profileImage = (ImageView) itemView.findViewById(R.id.image_message_profile);
}

void bind(UserMessage message) {
messageText.setText(message.getMessage());

// Format the stored timestamp into a readable String using method.
timeText.setText(Utils.formatDateTime(message.getCreatedAt()));

nameText.setText(message.getSender().getNickname());

// Insert the profile image from the URL into the ImageView.
Utils.displayRoundImageFromUrl(mContext, message.getSender().getProfileUrl(), profileImage);
}
}
}

We've just completed the adapter. We can now display a list of messages, both sent and received, in the chat bubbles that we defined earlier.

 

Putting it all together

We've created all the components, and now we need to initialize them inside our Activity. Instantiate a RecyclerView and a MessageListAdapter with a list of messages, and voila! You've just created your own messaging UI.

 

MessageListActivity.java

public class MessageListActivity extends AppCompatActivity {
private RecyclerView mMessageRecycler;
private MessageListAdapter mMessageAdapter;

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

mMessageRecycler = (RecyclerView) findViewById(R.id.reyclerview_message_list);
mMessageAdapter = new MessageListAdapter(this, messageList);
mMessageRecycler.setLayoutManager(new LinearLayoutManager(this));

}
}

You can build more advanced features like "image preview," animated GIFs within chat bubbles, and others. See examples of advanced chat UI at SendBird’s open source sample UI app! They build upon the use of RecyclerViews and adapters that we covered in this tutorial, and will help you create a production-level chat app with diverse features.

Read our API documentation 

Edited by Alek Jeziorek on 08/16/2017.

Written by

TAGS Android, chat, messaging, RECENT POSTS, ENGINEERING, User Interface

Subscribe to Email Updates

Recent Posts

Language