This simple tutorial will cover how to build a chat-style user interface using a RecyclerView
and Model-View-Presenter architectural pattern.
Before we get into creating the layout files, we are going to need to add a couple dependcies to our build.gradle
to allow us to use the Android RecyclerView
and CardView
:
compile 'com.android.support:appcompat-v7:25.2.0'
compile 'com.android.support:design:25.2.0'
compile 'com.android.support:cardview-v7:25.2.0'
AppCompat
should have already been included if you created an empty activity Android Studio project.
First thing we will start with is the resource XMLs needed for the UI. We will need one main XML for the activity holding the RecyclerView
and EditText
used for user input:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#E0E0E0"
tools:context="com.nesfeder.chatmvp.ChatActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_chat"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="20dp"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:layout_marginBottom="60dp"
android:clipToPadding="false">
</android.support.v7.widget.RecyclerView>
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_margin="8dp"
android:layout_alignParentBottom="true">
<EditText
android:id="@+id/et_search_box"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:inputType="text"
android:imeOptions="actionDone"
android:backgroundTint="#00FFFFFF"
android:hint="@string/ask_something_here"/>
</android.support.v7.widget.CardView>
</RelativeLayout>
Second and third XMLs will be for the the two different chat bubbles we will display in the RecyclerView
. Both of these will be inflated in their respective RecyclerView.ViewHolders
:
<?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:orientation="vertical"
android:gravity="end">
<LinearLayout
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="60dp"
android:layout_marginEnd="20dp"
android:paddingStart="10dp"
android:paddingEnd="18dp"
android:background="@drawable/chat_input_background">
<TextView
android:id="@+id/tv_input_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:textSize="16sp"
android:textColor="#FFF"
android:text="Input Text"/>
</LinearLayout>
<Space
android:layout_width="match_parent"
android:layout_height="10dp" />
</LinearLayout>
The code for the @drawable/chat_input_background
is:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<rotate
android:fromDegrees="45"
android:pivotX="100%"
android:pivotY="0%"
android:toDegrees="0" >
<shape android:shape="rectangle" >
<solid android:color="@color/botPrimary" />
</shape>
</rotate>
</item>
<item android:right="8dp">
<shape android:shape="rectangle" >
<solid android:color="@color/botPrimary" />
<corners android:radius="4dp" />
</shape>
</item>
</layer-list>
Let's also create some simple data objects. We will use a simple ChatObject.java
class that can be subclassed to create specific data objects that will populate our RecyclerView.Viewholder
s:
public abstract class ChatObject {
public static final int INPUT_OBJECT = 0;
public static final int RESPONSE_OBJECT = 1;
private String text;
@NonNull
public String getText() {
return text;
}
public void setText(@NonNull String text) {
this.text = text;
}
public abstract int getType();
}
So each time we extend ChatObject
, we will need to implement getType()
and provide a type for the object. This will later be used by our RecyclerView.Adapter
to determine what ViewHolder is needed to be created for the given object:
public class ChatInput extends ChatObject {
@Override
public int getType() {
return ChatObject.INPUT_OBJECT;
}
}
Now that we have the data objects / resources needed, we will look at the RecyclerView.ViewHolder
and RecyclerView.Adapter
. We can create a "base" ViewHolder class that will extend RecyclerView.ViewHolder
:
public abstract class BaseViewHolder extends RecyclerView.ViewHolder {
BaseViewHolder(View itemView) {
super(itemView);
}
public abstract void onBindView(ChatObject object);
}
BaseViewHolder.java
will then be subclassed by any specific ViewHolder we want to show in our RecyclerView
:
public class ChatInputVH extends BaseViewHolder {
private TextView tvInputText;
public ChatInputVH(View itemView) {
super(itemView);
this.tvInputText = (TextView) itemView.findViewById(R.id.tv_input_text);
}
@Override
public void onBindView(ChatObject object) {
this.tvInputText.setText(object.getText());
}
}
We will have Viewholders for a user input as well as a response (like a text message in a chat or a message from a bot). With our ViewHolders ready to go, let's take a look at our RecyclerView.Adapter
:
public class ChatAdapter extends RecyclerView.Adapter<BaseViewHolder> {
private ArrayList<ChatObject> chatObjects;
public ChatAdapter(ArrayList<ChatObject> chatObjects) {
this.chatObjects = chatObjects;
}
@Override
public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
// Create the ViewHolder based on the viewType
View itemView;
switch (viewType) {
case ChatObject.INPUT_OBJECT:
itemView = inflater.inflate(R.layout.chat_input_layout, parent, false);
return new ChatInputVH(itemView);
case ChatObject.RESPONSE_OBJECT:
itemView = inflater.inflate(R.layout.chat_response_layout, parent, false);
return new ChatResponseVH(itemView);
default:
itemView = inflater.inflate(R.layout.chat_response_layout, parent, false);
return new ChatResponseVH(itemView);
}
}
@Override
public void onBindViewHolder(BaseViewHolder holder, int position) {
holder.onBindView(chatObjects.get(position));
}
@Override
public int getItemViewType(int position) {
return chatObjects.get(position).getType();
}
@Override
public int getItemCount() {
return chatObjects.size();
}
}
Next, we can take a look at adhering to a model-view-presenter architectual pattern. Let's look at the contract that the view and presenter will follow:
public interface ChatContract {
interface View {
void notifyAdapterObjectAdded(int position);
void scrollChatDown();
}
interface Presenter {
void attachView(ChatContract.View view);
ArrayList<ChatObject> getChatObjects();
void onEditTextActionDone(String inputText);
}
}
ChatContract.View
will be implement by our main activity ChatActivity.java
and our presenter, ChatPresenter.java
will implement ChatContract.Presenter
:
public class ChatPresenter implements ChatContract.Presenter {
private ArrayList<ChatObject> chatObjects;
private ChatContract.View view;
public ChatPresenter() {
// Create the ArrayList for the chat objects
this.chatObjects = new ArrayList<>();
// Add an initial greeting message
ChatResponse greetingMsg = new ChatResponse();
greetingMsg.setText("Hello, world!");
chatObjects.add(greetingMsg);
}
@Override
public void attachView(ChatContract.View view) {
this.view = view;
}
@Override
public ArrayList<ChatObject> getChatObjects() {
return this.chatObjects;
}
@Override
public void onEditTextActionDone(String inputText) {
// Create new input object
ChatInput inputObject = new ChatInput();
inputObject.setText(inputText);
// Add it to the list and tell the adapter we added something
this.chatObjects.add(inputObject);
view.notifyAdapterObjectAdded(chatObjects.size() - 1);
// Also scroll down if we aren't at the bottom already
view.scrollChatDown();
}
}
When we construct the presenter, we create the list for the data objects that will be displayed in our RecyclerView
. The presenter has methods to attach the view implemented by ChatActivity
as well as handle input from the EditText
in our main layout. onCreate()
in ChatActivity
will look like this:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
rvChatList = (RecyclerView) findViewById(R.id.rv_chat);
etSearchBox = (EditText) findViewById(R.id.et_search_box);
etSearchBox.setOnEditorActionListener(searchBoxListener);
// Instantiate presenter and attach view
this.presenter = new ChatPresenter();
presenter.attachView(this);
// Instantiate the adapter and give it the list of chat objects
this.chatAdapter = new ChatAdapter(presenter.getChatObjects());
// Set up the RecyclerView with adapter and layout manager
rvChatList.setAdapter(chatAdapter);
rvChatList.setLayoutManager(new LinearLayoutManager(getApplicationContext()));
rvChatList.setItemAnimator(new DefaultItemAnimator());
}
Giving the RecyclerView
a DefaultItemAnimator
gives us animations that make adding / removing objects from the list look a lot smoother:
It's important to note that when you add or remove items from the ArrayList
in the presenter, you most also notify the adapter that we have updated our dataset.
// Add it to the list and tell the adapter we added something
this.chatObjects.add(inputObject);
view.notifyAdapterObjectAdded(chatObjects.size() - 1);
@Override
public void notifyAdapterObjectAdded(int position) {
this.chatAdapter.notifyItemInserted(position);
}
The end product from this code should look something like this when we enter something in our EditText
:
For the complete Android Studio project, check out this Git repo: [Chat MVP] (https://github.com/danesfeder/chat-mvp/)
Happy coding! -Dan