Xây dựng úng dụng chát đơn giản bằng RecyclerView

Hầu hết các ứng dụng di động bây giờ đều có tính năng chát, với những ứng dụng chát phức tạp thì đã có khá nhiều thư viện hỗ trợ, nhưng nếu bạn chỉ cần 1 ứng dụng đơn giản mà phải thêm những lib cồng kềnh vào thì sẽ kiến ứng dụng của bạn nặng nề. Dưới đây mình sẽ hướng dẫ các bạn sử dụng RecyclerView để tạo 1 chức năng Chat đơn giản.

1. Cài đặt

Cấu trúc thư mục.

thêm các thư viện hỗ trợ.

    android {
    ...
        // Sử dụng data binding
        dataBinding {
            enabled = true
        }
    }
    dependencies {
    ...
        implementation 'com.android.support:recyclerview-v7:27.1.0'
        implementation 'com.android.support:cardview-v7:27.1.0'

        // Thư viện sử dụng để hiển thị ảnh
        implementation "com.github.bumptech.glide:glide:4.5.0"
    }

2. Chi Tiết

Bây giờ chúng ta sẽ đi vào chi tiết từng file.

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    >
    <data>
        <variable
            name="viewModel"
            type="com.demochatrecyclerview.MainActivity"
            />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context="com.demochatrecyclerview.MainActivity"
        >

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            />
        <View
            android:layout_width="wrap_content"
            android:layout_height="1dp"
            android:background="##FFE3E3E3"
            />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:minHeight="56dp"
            android:orientation="horizontal"
            >

            <android.support.v7.widget.AppCompatImageView
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_gravity="center"
                android:layout_marginStart="10dp"
                android:onClick="@{ viewModel::chooseImage }"
                android:src="@mipmap/ic_camera"
                />

            <android.support.v7.widget.AppCompatEditText
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_margin="10dp"
                android:layout_weight="1"
                android:background="@drawable/bg_edit_text_talk"
                android:gravity="center_vertical"
                android:hint="Write something"
                android:padding="6dp"
                android:text="@={ viewModel.message }"
                android:textSize="14sp"
                />

            <android.support.v7.widget.AppCompatTextView
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_marginEnd="16dp"
                android:gravity="center"
                android:onClick="@{ viewModel::sendMessage }"
                android:text="Send"
                android:textColor="#FF318496"
                android:textSize="16sp"
                android:textStyle="bold"
                />
        </LinearLayout>
    </LinearLayout>
</layout>

ở đây mình chỉ thiết kế layout đơn giản gồm 1 RecyclerView để hiển thị nội dung chát và 1 ô nhâp dữ liệu ở dưới.

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private ActivityMainBinding binding;
    private RecyclerView recyclerView;
    private ChatAdapter chatAdapter;
    private List<Mesage> messageList;

    public ObservableField<String> message = new ObservableField<>();


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        binding.setViewModel(this);

        messageList = new ArrayList<>();
        chatAdapter = new ChatAdapter(messageList);

        recyclerView = binding.recyclerView;
        LinearLayoutManager linearLayoutManager =
                new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
        linearLayoutManager.setReverseLayout(true);
        linearLayoutManager.setSmoothScrollbarEnabled(true);
        linearLayoutManager.setAutoMeasureEnabled(true);
        linearLayoutManager.setStackFromEnd(true);

        recyclerView.setLayoutManager(linearLayoutManager);
        recyclerView.setNestedScrollingEnabled(false);
        recyclerView.setAdapter(chatAdapter);
    }

    public void chooseImage(View view) {
        
    }

    public void sendMessage(View view) {

    }
}

Cài đặt cơ bản cho file MainActivity

Cài đặt file ChatAdapter.java

public class ChatAdapter extends RecyclerView.Adapter<ChatViewHolder> {
    private static final int TYPE_MESSAGE_RECEIVE = 1;
    private static final int TYPE_MESSAGE_SEND = 2;

    private List<Message> messageList;

    public ChatAdapter(List<Message> messageList){
        this.messageList = messageList;
    }
    @NonNull
    @Override
    public ChatViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        switch (viewType) {
            case TYPE_MESSAGE_RECEIVE:
                ItemMessageReceiveBinding itemMessageReceiveBinding =
                        ItemMessageReceiveBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
                return new ViewHolderReceive(itemMessageReceiveBinding);

            case TYPE_MESSAGE_SEND:
                ItemMessageSendBinding itemMessageSendBinding =
                        ItemMessageSendBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
                return new ViewHolderSend(itemMessageSendBinding);
        }
        return null;
    }

    @Override
    public void onBindViewHolder(@NonNull ChatViewHolder holder, int position) {
        Message messageNext = position == 0 ? null : messageList.get(position - 1);
        Message messagePrevious = position >= getItemCount() - 1 ? null : messageList.get(position + 1);

        switch (holder.getItemViewType()) {
            case TYPE_MESSAGE_RECEIVE:
                ((ViewHolderReceive) holder).bind(messageNext, messageList.get(position), messagePrevious);

                break;
            case TYPE_MESSAGE_SEND:
                ((ViewHolderSend) holder).bind(messageNext, messageList.get(position), messagePrevious);
                break;
        }
    }

    @Override
    public int getItemViewType(int position) {
        if (messageList.get(position).isMyMessage()) {
            return TYPE_MESSAGE_SEND;
        } else {
            return TYPE_MESSAGE_RECEIVE;
        }
    }

    @Override
    public int getItemCount() {
        return messageList == null ? 0 : messageList.size();
    }
}

Ở đây chúng ta sẽ có 2 kiểu tin nhắn là tin nhắn do mình gửi đi và tin nhắn nhận về TYPE_MESSAGE_RECEIVETYPE_MESSAGE_SEND chúng ta sẽ lựa chọn từng kiểu tin nhắn để hiển thị sao cho đúng vị trí(giả sử tin nhắn gửi đi sẽ hiển thị ở bên phải còn con nhắn nhận về sẽ hiển thị ở bên trái)

Ở trong hàm onBindViewHolder mình có truyền vào messagePrevious để khi hiển thị chúng ta có thể kiểm tra các điều kiện.

File ViewHolderReceive.javaViewHolderSend.java

public class ViewHolderReceive extends ChatViewHolder<ItemMessageReceiveBinding> {

    public ViewHolderReceive(ItemMessageReceiveBinding itemView) {
        super(itemView);
    }

    @Override
    public void bind(Message messageCurrent, Message messagePrevious) {
        if (binding.getViewModel() == null) {
            binding.setViewModel(this);
        }
        super.bind(messageCurrent, messagePrevious);
    }
}
...
public class ViewHolderSend extends ChatViewHolder<ItemMessageSendBinding> {
    public ViewHolderSend(ItemMessageSendBinding itemView) {
        super(itemView);
    }

    @Override
    public void bind(Message messageCurrent, Message messagePrevious) {
        if (binding.getViewModel() == null) {
            binding.setViewModel(this);
        }
        super.bind(messageCurrent, messagePrevious);
    }
}

File item_message_send.xml

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tool="http://schemas.android.com/tools"
    >
    <data>
        <variable
            name="viewModel"
            type="com.demochatrecyclerview.ViewHolderSend"
            />
        <import type="android.view.View"/>

        <import type="android.text.TextUtils"/>
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="end"
        >
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:background="#FFFFFF"
            android:orientation="vertical"
            >

            <android.support.v7.widget.AppCompatTextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginBottom="20dp"
                android:gravity="center"
                android:text="@{ viewModel.date }"
                android:textColor="#FF909090"
                android:visibility="@{ TextUtils.isEmpty(viewModel.date) ? View.GONE : View.VISIBLE }"
                tool:text="2018-02-28"
                />
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginEnd="10dp"
                android:gravity="end"
                android:minHeight="36dp"
                android:orientation="horizontal"
                >

                <android.support.v7.widget.AppCompatTextView
                    android:id="@+id/time"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="4dp"
                    android:minWidth="40dp"
                    android:text="@{ viewModel.time }"
                    android:textSize="14sp"
                    android:visibility="@{ TextUtils.isEmpty(viewModel.time) ? View.INVISIBLE : View.VISIBLE }"
                    tool:text="15:03"
                    />

                <android.support.v7.widget.AppCompatTextView
                    android:id="@+id/content"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:layout_marginStart="10dp"
                    android:background="@drawable/bg_edit_text_talk"
                    android:gravity="center_vertical"
                    android:minHeight="36dp"
                    android:text="@{ viewModel.content }"
                    android:textColor="#000000"
                    android:textSize="14sp"
                    android:visibility="@{TextUtils.isEmpty(viewModel.content) ? View.GONE : View.VISIBLE }"
                    />
            </LinearLayout>
        </LinearLayout>
    </LinearLayout>
</layout>

file item_message_receive.xml có thiết kế gần giống với file item_message_send.xml nên mình sẽ không nêu ở đây. Và các bạn áp dụng và project của mình thì nên căn chỉnh lại chút cho đẹp nhé.

Thực chất các tin nhắn hiển thị có logic giống nhau nên mình sẽ viết chung logic của chúng trong file ChatViewHolder.java còn 2 file này mục đích là chia View mà thôi.

File logic chính để hiển thị Message là trong file ChatViewHolder.java

public class ChatViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {
    @BindingAdapter({ "glideImageUrl" })
    public static void setGlideImageUrl(ImageView imageview, String url) {
        RequestOptions requestOptions = new RequestOptions().placeholder(R.mipmap.ic_launcher_round)
                .diskCacheStrategy(DiskCacheStrategy.RESOURCE);

        Glide.with(imageview.getContext()).load(url).apply(requestOptions).into(imageview);
    }

    protected T binding;
    protected Message messageCurrent;

    public ObservableField<String> date = new ObservableField<>();
    public ObservableField<String> time = new ObservableField<>();
    public ObservableField<String> content = new ObservableField<>();
    public ObservableField<String> fullName = new ObservableField<>();

    public ObservableField<String> imageAvatar = new ObservableField<>();
    public ObservableBoolean isShowAvatar = new ObservableBoolean(true);

    public ChatViewHolder(T itemView) {
        super(itemView.getRoot());
        binding = itemView;
    }

    public void bind(Message message, Message messagePrevious) {
        this.messageCurrent = messageCurrent;

        // Show Date
        // Hiển thị thời gian khi 2 tin nhắn nằm ở 2 ngày khác nhau
        if (messagePrevious == null || DateUtils.compareTwoDate(messagePrevious.getCreatedAt(),
                messageCurrent.getCreatedAt(), DateUtils.TIMEZONE_FORMAT_YYYY_MM_DD)) {

            date.set(DateUtils.convertStringToStringFormat(messageCurrent.getCreatedAt(),
                    DateUtils.TIMEZONE_FORMAT_MESSAGE, DateUtils.TIMEZONE_FORMAT_YYYY_MM_DD_VIEW));
        } else {
            date.set("");
        }

        // Show Avatar
        if (messagePrevious == null
                || messageCurrent.getUser() == null
                || messagePrevious.getUser() == null
                || messageCurrent.getUser().getId() != messagePrevious.getUser().getId()) {

            isShowAvatar.set(true);
            if (messageCurrent.getUser() != null) {
                imageAvatar.set(messageCurrent.getUser().getAvata());
                fullName.set(TextUtils.isEmpty(messageCurrent.getUser().getName()) ? "User unknown"
                        : messageCurrent.getUser().getName());
            } else {
                imageAvatar.set("");
                fullName.set("User unknown");
            }
        } else {
            isShowAvatar.set(false);
        }

        // Show Time
        // Hiển thị thời gian gửi hoặc nhận tin nhắn
        if (messagePrevious == null
                || messagePrevious.getUser() == null
                || messageCurrent.getUser() == null
                || messageCurrent.getUser().getId() != messagePrevious.getUser().getId()
                || Math.abs(DateUtils.compareTwoTime(messagePrevious.getCreatedAt(), messageCurrent.getCreatedAt()))
                > 0) {

            time.set(DateUtils.convertStringToStringFormat(messageCurrent.getCreatedAt(),
                    DateUtils.TIMEZONE_FORMAT_MESSAGE, DateUtils.TIMEZONE_FORMAT_HH_MM));
        } else {
            time.set("");
        }

        // Hiển thị nội dung tin nhắn văn bản
        content.set(messageCurrent.getMessage());
    }
}

Vậy là chúng ta đã hoàn thành phần viết code chính cho tính năng chát. Bây giờ sẽ quay lại file MainActivity.java để thêm phần code cho hàm sendMessage

    public void sendMessage(View view) {
            long time = System.currentTimeMillis();
            User user = User.fakeUser();
            Message message = new Message();
            message.setMessageId(time);
            message.setUser(user);
            message.setMessage(messageChat.get());
            message.setMyMessage(time % 2 == 0);
            message.setCreatedAt(dateToString(time, "yyyy/MM/dd HH:mm:ss"));

            // Thêm item vào List và cập nhật lại hiển thị trên View
            messageList.add(0, message);
            chatAdapter.notifyItemInserted(0);
        }
    public String dateToString(long timestamp, String format) {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format, Locale.getDefault());
            Date date = new Date(timestamp);
            return simpleDateFormat.format(date);
        }

Đến đây gần như là chúng ta đã hoàn thành 1 tính năng chát cơ bản rồi. Hi vọng thông qua bài viết này các bạn có thể tự xây dựng cho mình 1 app chát đơn giản để nói chuyện với bạn bè.