0

Xây dựng ứng dụng ChatApp partII

Server NodeJS

Đầu tiên chúng ta xây dựng server bằng cách tạo file server_chat.js như sau

//server_chat.js

// Setup basic express server
var express = require('express');
var app = express();
var server = require('http').createServer(app);
var io = require('socket.io')(server);

var port = process.env.PORT || 3000;

server.listen(port, function () {
  console.log('Server listening at port %d', port);
});

// Routing
app.use(express.static(__dirname + '/public'));

// Chatroom

var numUsers = 0;

io.on('connection', function (socket) {
  var addedUser = false;

  // when the client emits 'new message', this listens and executes
  socket.on('new message', function (data) {
    // we tell the client to execute 'new message'
    socket.broadcast.emit('new message', {
      username: socket.username,
      message: data
    });
  });

  // when the client emits 'add user', this listens and executes
  socket.on('add user', function (username) {
    if (addedUser) return;

    // we store the username in the socket session for this client
    socket.username = username;
    ++numUsers;
    addedUser = true;
    socket.emit('login', {
      numUsers: numUsers
    });
    // echo globally (all clients) that a person has connected
    socket.broadcast.emit('user joined', {
      username: socket.username,
      numUsers: numUsers
    });
  });

  // when the client emits 'typing', we broadcast it to others
  socket.on('typing', function () {
    socket.broadcast.emit('typing', {
      username: socket.username
    });
  });

  // when the client emits 'stop typing', we broadcast it to others
  socket.on('stop typing', function () {
    socket.broadcast.emit('stop typing', {
      username: socket.username
    });
  });

  // when the user disconnects.. perform this
  socket.on('disconnect', function () {
    if (addedUser) {
      --numUsers;

      // echo globally that this client has left
      socket.broadcast.emit('user left', {
        username: socket.username,
        numUsers: numUsers
      });
    }
  });
});

Ở đây chúng ta có hàm nhận tin nhắn từ người dùng xong gửi cho tất cả người đang online

    socket.on('new message', function (data) {
    // we tell the client to execute 'new message'
    socket.broadcast.emit('new message', {
      username: socket.username,
      message: data
    });
    });

Một hàm để thêm người mới vào server, tính toán số người đang online, gửi cho người này số người trong server và thông báo cho tất cả người khác sự xuất hiện của người mới.

 socket.on('add user', function (username) {
    if (addedUser) return;

    // we store the username in the socket session for this client
    socket.username = username;
    ++numUsers;
    addedUser = true;
    socket.emit('login', {
      numUsers: numUsers
    });
    // echo globally (all clients) that a person has connected
    socket.broadcast.emit('user joined', {
      username: socket.username,
      numUsers: numUsers
    });
  });

Một hàm xử lí khi user thoát ra.

 socket.on('disconnect', function () {
    if (addedUser) {
      --numUsers;

      // echo globally that this client has left
      socket.broadcast.emit('user left', {
        username: socket.username,
        numUsers: numUsers
      });
    }
  });

Ok, vậy là xong phần server rồi, để chạy nó, bạn dùng

 node server_chat.js

Bây giờ chúng ta xây dựng client nhé.

Xây dựng Client Android.

  1. Đầu tiên, bạn chọn New -> Project để tạo một project mới tên là Android Chat.

  2. Bây giờ ta sẽ thêm thư viện cho ứng dụng tại build.gradle như sau.

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.1.1'
    compile 'com.android.support:recyclerview-v7:21.0.+'
    compile ('io.socket:socket.io-client:0.7.0') {
        exclude group: 'org.json', module: 'json'
    }
}

Ta thêm thư viện recyclerview để hiện thị list tin nhắn và socketio để liên kết với server nodejs.

  1. Chúng ta định nghĩa color và string cho ứng dụng này như sau. Thêm vào file strings.xml tại res/values như sau.
//strings.xml

    <resources>
        <string name="app_name">Android Chat</string>
        <string name="action_settings">Settings</string>
        <string name="action_leave">Leave</string>
        <string name="action_send">Send</string>
        <string name="prompt_message">Message</string>

        <string name="error_connect">Failed to connect</string>

        <!-- messages -->
        <string name="message_welcome">Welcome to Socket.IO Chat</string>
        <plurals name="message_participants">
            <item quantity="one">there\'s %d participant</item>
            <item quantity="other">there are %d participants</item>
        </plurals>
        <string name="message_user_joined">%s joined</string>
        <string name="message_user_left">%s left</string>
        <string name="user_action_typing">is typing</string>
        <string name="title_activity_login">Join</string>

        <!-- Strings related to login -->
        <string name="prompt_username">What\'s your nickname?</string>
        <string name="action_sign_in">Join</string>
        <string name="action_sign_in_short">Join</string>

        <string name="error_field_required">This field is required</string>

        <dimen name="spacing">8dp</dimen>

    </resources>

Thêm file colors.xml tại res/values.

   //colors.xml

    <?xml version="1.0" encoding="utf-8"?>
    <resources>

        <color name="username0">#e21400</color>
        <color name="username1">#91580f</color>
        <color name="username2">#f8a700</color>
        <color name="username3">#f78b00</color>
        <color name="username4">#58dc00</color>
        <color name="username5">#287b00</color>
        <color name="username6">#a8f07a</color>
        <color name="username7">#4ae8c4</color>
        <color name="username8">#3b88eb</color>
        <color name="username9">#3824aa</color>
        <color name="username10">#a700ff</color>
        <color name="username11">#d300e7</color>

    </resources>

Thêm file arrays.xml tại res/values.

    //arrays.xml

    <?xml version="1.0" encoding="utf-8"?>
    <resources>

        <array name="username_colors">
            <item>@color/username0</item>
            <item>@color/username1</item>
            <item>@color/username2</item>
            <item>@color/username3</item>
            <item>@color/username4</item>
            <item>@color/username5</item>
            <item>@color/username6</item>
            <item>@color/username7</item>
            <item>@color/username8</item>
            <item>@color/username9</item>
            <item>@color/username10</item>
            <item>@color/username11</item>
        </array>

    </resources>

Sửa file menu_main.xml tại res/values như sau.

    // menu_main.xml

    <menu 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"
        tools:context=".MainActivity">
        <item android:id="@+id/action_leave"
            android:title="@string/action_leave"
            android:orderInCategory="100"
            app:showAsAction="never"/>
    </menu>
  1. Tạo file Constants.java như sau.
    //Constants.java

    package com.ln.androidchat;

    public class Constants {
        public static final String CHAT_SERVER_URL = "http://192.168.1.6:3000";
    }

Đây là đường dẫn để truy cập vào server nodejs, bạn phải thay bằng địa chỉ ip của máy bạn nhé.

  1. Tạo file ChatApplication.java như sau.
    // ChatApplication.java

    package com.ln.androidchat;

    import android.app.Application;

    import java.net.URISyntaxException;

    import io.socket.client.IO;
    import io.socket.client.Socket;

    public class ChatApplication extends Application {

        private Socket mSocket;
        {
            try {
                mSocket = IO.socket(Constants.CHAT_SERVER_URL);
            } catch (URISyntaxException e) {
                throw new RuntimeException(e);
            }
        }

        public Socket getSocket() {
            return mSocket;
        }
    }

File này có chức năng đăng kí socket với server, ta dùng application để có thể sử dụng chức năng này trong toàn ứng dụng

  1. OK, bây giờ ta sẽ tạo hết các file xml layout một lượt nhé, vào res/layout tạo file item_action.xml như sau.
    // item_action.xml

    <LinearLayout
            xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:gravity="center_vertical"
            android:paddingTop="@dimen/spacing">

        <TextView
                android:id="@+id/username"
                style="?android:textAppearanceMedium"
                android:textColor="?android:textColorPrimary"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:singleLine="true"
                android:textStyle="bold"/>
        <TextView
                android:id="@+id/action"
                style="?android:textAppearanceMedium"
                android:textColor="?android:textColorSecondary"
                android:text="@string/user_action_typing"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingLeft="@dimen/spacing"
                android:paddingRight="@dimen/spacing"
                android:singleLine="true"/>

    </LinearLayout>

Layout này để hiển thị user đang gõ tin nhắn.

Tạo file item_log.xml như sau

    // item_log.xml

    <LinearLayout
            xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:paddingTop="@dimen/spacing">

        <TextView
                android:id="@+id/message"
                style="?android:textAppearanceSmall"
                android:textColor="?android:textColorSecondary"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center"/>

    </LinearLayout>

Layout này hiển thị số người đang online và 1 số tin nhắn khác.

Tạo file item_message.xml như sau.

     // item_message.xml
    <LinearLayout
            xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:paddingTop="@dimen/spacing">

        <TextView
                android:id="@+id/username"
                style="?android:textAppearanceMedium"
                android:textColor="?android:textColorPrimary"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:singleLine="true"
                android:textStyle="bold"/>
        <TextView
                android:id="@+id/message"
                style="?android:textAppearanceMedium"
                android:textColor="?android:textColorPrimary"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingLeft="@dimen/spacing"
                android:paddingRight="@dimen/spacing"
                android:singleLine="true"/>

    </LinearLayout>

Layout này hiển thị user và tin nhắn của họ.

Tạo file fragment_main.xml như sau.

    // fragment_main.xml

    <LinearLayout
            xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:tools="http://schemas.android.com/tools"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            tools:context=".MainFragment">

        <android.support.v7.widget.RecyclerView
                android:id="@+id/messages"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:paddingLeft="@dimen/activity_horizontal_margin"
                android:paddingRight="@dimen/activity_horizontal_margin"
                android:scrollbars="vertical"
                android:scrollbarStyle="outsideOverlay"/>
        <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:paddingLeft="@dimen/activity_horizontal_margin"
                android:paddingRight="@dimen/activity_horizontal_margin"
                android:orientation="horizontal"
                android:gravity="center_vertical">

            <EditText
                    android:id="@+id/message_input"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:hint="@string/prompt_message"
                    android:imeActionId="@+id/send"
                    android:imeActionLabel="@string/action_send"
                    android:imeOptions="actionSend"
                    android:inputType="text"
                    android:maxLines="1"
                    android:singleLine="true"/>
            <ImageButton
                    android:id="@+id/send_button"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:src="@android:drawable/ic_menu_send"
                    android:contentDescription="@string/action_send"/>

        </LinearLayout>

    </LinearLayout>

Layout này hiển thị list tin nhắn và ô tin nhắn để user nhập và gửi.

Tạo file activity_login.xml như sau

    // activity_login.xml

    <ScrollView
            xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:tools="http://schemas.android.com/tools"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:paddingBottom="@dimen/activity_vertical_margin"
            android:paddingLeft="@dimen/activity_horizontal_margin"
            android:paddingRight="@dimen/activity_horizontal_margin"
            android:paddingTop="@dimen/activity_vertical_margin"
           >

        <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:orientation="vertical">

            <EditText
                    android:id="@+id/username_input"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:hint="@string/prompt_username"
                    android:imeActionId="@+id/login"
                    android:imeActionLabel="@string/action_sign_in_short"
                    android:imeOptions="actionUnspecified"
                    android:inputType="textPersonName"
                    android:maxLength="14"
                    android:maxLines="1"
                    android:singleLine="true"/>
            <Button
                    android:id="@+id/sign_in_button"
                    style="?android:textAppearanceSmall"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="16dp"
                    android:text="@string/action_sign_in"
                    android:textStyle="bold"/>

        </LinearLayout>
    </ScrollView>

Layout này để user đăng nhập tên vào server

  1. Hơi nhiều thứ cần làm nhỉ 😃) làm tiếp nào, bạn lên trên, tạo file LoginActivity.java nhé
     //LoginActivity.java

    package com.ln.androidchat;

    import android.app.Activity;
    import android.content.Intent;
    import android.os.Bundle;
    import android.text.TextUtils;
    import android.view.KeyEvent;
    import android.view.View;
    import android.view.View.OnClickListener;
    import android.view.inputmethod.EditorInfo;
    import android.widget.Button;
    import android.widget.EditText;
    import android.widget.TextView;

    import org.json.JSONException;
    import org.json.JSONObject;

    import io.socket.client.Socket;
    import io.socket.emitter.Emitter;

    /**
     * A login screen that offers login via username.
     */
    public class LoginActivity extends Activity {

        private EditText mUsernameView;

        private String mUsername;

        private Socket mSocket;

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

            ChatApplication app = (ChatApplication) getApplication();
            mSocket = app.getSocket();

            // Set up the login form.
            mUsernameView = (EditText) findViewById(R.id.username_input);
            mUsernameView.setOnEditorActionListener(new TextView.OnEditorActionListener() {
                @Override
                public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) {
                    if (id == R.id.login || id == EditorInfo.IME_NULL) {
                        attemptLogin();
                        return true;
                    }
                    return false;
                }
            });

            Button signInButton = (Button) findViewById(R.id.sign_in_button);
            signInButton.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View view) {
                    attemptLogin();
                }
            });

            mSocket.on("login", onLogin);
        }

        @Override
        protected void onDestroy() {
            super.onDestroy();

            mSocket.off("login", onLogin);
        }

        /**
         * Attempts to sign in the account specified by the login form.
         * If there are form errors (invalid username, missing fields, etc.), the
         * errors are presented and no actual login attempt is made.
         */
        private void attemptLogin() {
            // Reset errors.
            mUsernameView.setError(null);

            // Store values at the time of the login attempt.
            String username = mUsernameView.getText().toString().trim();

            // Check for a valid username.
            if (TextUtils.isEmpty(username)) {
                // There was an error; don't attempt login and focus the first
                // form field with an error.
                mUsernameView.setError(getString(R.string.error_field_required));
                mUsernameView.requestFocus();
                return;
            }

            mUsername = username;

            // perform the user login attempt.
            mSocket.emit("add user", username);
        }

        private Emitter.Listener onLogin = new Emitter.Listener() {
            @Override
            public void call(Object... args) {
                JSONObject data = (JSONObject) args[0];

                int numUsers;
                try {
                    numUsers = data.getInt("numUsers");
                } catch (JSONException e) {
                    return;
                }

                Intent intent = new Intent();
                intent.putExtra("username", mUsername);
                intent.putExtra("numUsers", numUsers);
                setResult(RESULT_OK, intent);
                finish();
            }
        };
    }

Chúng ta đăng nhập vào server bằng tên, gửi lên server như sau.

     mSocket.emit("add user", username);

Để lắng nghe tin nhắn ta dùng.

     mSocket.on("login", onLogin);

    private Emitter.Listener onLogin = new Emitter.Listener() {
            @Override
            public void call(Object... args) {
                JSONObject data = (JSONObject) args[0];

                int numUsers;
                try {
                    numUsers = data.getInt("numUsers");
                } catch (JSONException e) {
                    return;
                }

                Intent intent = new Intent();
                intent.putExtra("username", mUsername);
                intent.putExtra("numUsers", numUsers);
                setResult(RESULT_OK, intent);
                finish();
            }
        };

Nếu nhận được kết quả trả về ok thì sẽ thoát ra và vào trang tin nhắn.

  1. Tạo file Message.java như sau
     // Message.java

    package com.ln.androidchat;

    public class Message {

        public static final int TYPE_MESSAGE = 0;
        public static final int TYPE_LOG = 1;
        public static final int TYPE_ACTION = 2;

        private int mType;
        private String mMessage;
        private String mUsername;

        private Message() {}

        public int getType() {
            return mType;
        };

        public String getMessage() {
            return mMessage;
        };

        public String getUsername() {
            return mUsername;
        };

        public static class Builder {
            private final int mType;
            private String mUsername;
            private String mMessage;

            public Builder(int type) {
                mType = type;
            }

            public Builder username(String username) {
                mUsername = username;
                return this;
            }

            public Builder message(String message) {
                mMessage = message;
                return this;
            }

            public Message build() {
                Message message = new Message();
                message.mType = mType;
                message.mUsername = mUsername;
                message.mMessage = mMessage;
                return message;
            }
        }
    }

Đây là 1 đối tượng Message gồm user, tin nhắn và loại gõ.

  1. Tạo 1 adapter để quản lí list tin nhắn MessageApdapter.java như sau

    // MessageAdapter.java

    package com.ln.androidchat;

    import android.content.Context;
    import android.support.v7.widget.RecyclerView;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.TextView;

    import java.util.List;

    public class MessageAdapter extends RecyclerView.Adapter<MessageAdapter.ViewHolder> {

        private List<Message> mMessages;
        private int[] mUsernameColors;

        public MessageAdapter(Context context, List<Message> messages) {
            mMessages = messages;
            mUsernameColors = context.getResources().getIntArray(R.array.username_colors);
        }

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            int layout = -1;
            switch (viewType) {
            case Message.TYPE_MESSAGE:
                layout = R.layout.item_message;
                break;
            case Message.TYPE_LOG:
                layout = R.layout.item_log;
                break;
            case Message.TYPE_ACTION:
                layout = R.layout.item_action;
                break;
            }
            View v = LayoutInflater
                    .from(parent.getContext())
                    .inflate(layout, parent, false);
            return new ViewHolder(v);
        }

        @Override
        public void onBindViewHolder(ViewHolder viewHolder, int position) {
            Message message = mMessages.get(position);
            viewHolder.setMessage(message.getMessage());
            viewHolder.setUsername(message.getUsername());
        }

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

        @Override
        public int getItemViewType(int position) {
            return mMessages.get(position).getType();
        }

        public class ViewHolder extends RecyclerView.ViewHolder {
            private TextView mUsernameView;
            private TextView mMessageView;

            public ViewHolder(View itemView) {
                super(itemView);

                mUsernameView = (TextView) itemView.findViewById(R.id.username);
                mMessageView = (TextView) itemView.findViewById(R.id.message);
            }

            public void setUsername(String username) {
                if (null == mUsernameView) return;
                mUsernameView.setText(username);
                mUsernameView.setTextColor(getUsernameColor(username));
            }

            public void setMessage(String message) {
                if (null == mMessageView) return;
                mMessageView.setText(message);
            }

            private int getUsernameColor(String username) {
                int hash = 7;
                for (int i = 0, len = username.length(); i < len; i++) {
                    hash = username.codePointAt(i) + (hash << 5) - hash;
                }
                int index = Math.abs(hash % mUsernameColors.length);
                return mUsernameColors[index];
            }
        }
    }
  1. Tạo file MainFragment.java như sau
     // MainFragment.java

    package com.ln.androidchat;

    import android.app.Activity;
    import android.content.Intent;
    import android.os.Bundle;
    import android.os.Handler;
    import android.support.v4.app.Fragment;
    import android.support.v7.widget.LinearLayoutManager;
    import android.support.v7.widget.RecyclerView;
    import android.text.Editable;
    import android.text.TextUtils;
    import android.text.TextWatcher;
    import android.view.KeyEvent;
    import android.view.LayoutInflater;
    import android.view.Menu;
    import android.view.MenuInflater;
    import android.view.MenuItem;
    import android.view.View;
    import android.view.ViewGroup;
    import android.view.inputmethod.EditorInfo;
    import android.widget.EditText;
    import android.widget.ImageButton;
    import android.widget.TextView;
    import android.widget.Toast;

    import org.json.JSONException;
    import org.json.JSONObject;

    import java.util.ArrayList;
    import java.util.List;

    import io.socket.client.Socket;
    import io.socket.emitter.Emitter;

    /**
     * A chat fragment containing messages view and input form.
     */
    public class MainFragment extends Fragment {

        private static final int REQUEST_LOGIN = 0;

        private static final int TYPING_TIMER_LENGTH = 600;

        private RecyclerView mMessagesView;
        private EditText mInputMessageView;
        private List<Message> mMessages = new ArrayList<Message>();
        private RecyclerView.Adapter mAdapter;
        private boolean mTyping = false;
        private Handler mTypingHandler = new Handler();
        private String mUsername;
        private Socket mSocket;

        public MainFragment() {
            super();
        }

        @Override
        public void onAttach(Activity activity) {
            super.onAttach(activity);
            mAdapter = new MessageAdapter(activity, mMessages);
        }

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);

            setHasOptionsMenu(true);

            ChatApplication app = (ChatApplication) getActivity().getApplication();
            mSocket = app.getSocket();
            mSocket.on(Socket.EVENT_CONNECT_ERROR, onConnectError);
            mSocket.on(Socket.EVENT_CONNECT_TIMEOUT, onConnectError);
            mSocket.on("new message", onNewMessage);
            mSocket.on("user joined", onUserJoined);
            mSocket.on("user left", onUserLeft);
            mSocket.on("typing", onTyping);
            mSocket.on("stop typing", onStopTyping);
            mSocket.connect();

            startSignIn();
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                                 Bundle savedInstanceState) {
            return inflater.inflate(R.layout.fragment_main, container, false);
        }

        @Override
        public void onDestroy() {
            super.onDestroy();

            mSocket.disconnect();
            mSocket.off(Socket.EVENT_CONNECT_ERROR, onConnectError);
            mSocket.off(Socket.EVENT_CONNECT_TIMEOUT, onConnectError);
            mSocket.off("new message", onNewMessage);
            mSocket.off("user joined", onUserJoined);
            mSocket.off("user left", onUserLeft);
            mSocket.off("typing", onTyping);
            mSocket.off("stop typing", onStopTyping);
        }

        @Override
        public void onViewCreated(View view, Bundle savedInstanceState) {
            super.onViewCreated(view, savedInstanceState);

            mMessagesView = (RecyclerView) view.findViewById(R.id.messages);
            mMessagesView.setLayoutManager(new LinearLayoutManager(getActivity()));
            mMessagesView.setAdapter(mAdapter);

            mInputMessageView = (EditText) view.findViewById(R.id.message_input);
            mInputMessageView.setOnEditorActionListener(new TextView.OnEditorActionListener() {
                @Override
                public boolean onEditorAction(TextView v, int id, KeyEvent event) {
                    if (id == R.id.send || id == EditorInfo.IME_NULL) {
                        attemptSend();
                        return true;
                    }
                    return false;
                }
            });
            mInputMessageView.addTextChangedListener(new TextWatcher() {
                @Override
                public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                }

                @Override
                public void onTextChanged(CharSequence s, int start, int before, int count) {
                    if (null == mUsername) return;
                    if (!mSocket.connected()) return;

                    if (!mTyping) {
                        mTyping = true;
                        mSocket.emit("typing");
                    }

                    mTypingHandler.removeCallbacks(onTypingTimeout);
                    mTypingHandler.postDelayed(onTypingTimeout, TYPING_TIMER_LENGTH);
                }

                @Override
                public void afterTextChanged(Editable s) {
                }
            });

            ImageButton sendButton = (ImageButton) view.findViewById(R.id.send_button);
            sendButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    attemptSend();
                }
            });
        }

        @Override
        public void onActivityResult(int requestCode, int resultCode, Intent data) {
            super.onActivityResult(requestCode, resultCode, data);
            if (Activity.RESULT_OK != resultCode) {
                getActivity().finish();
                return;
            }

            mUsername = data.getStringExtra("username");
            int numUsers = data.getIntExtra("numUsers", 1);

            addLog(getResources().getString(R.string.message_welcome));
            addParticipantsLog(numUsers);
        }

        @Override
        public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
            // Inflate the menu; this adds items to the action bar if it is present.
            inflater.inflate(R.menu.menu_main, menu);
        }

        @Override
        public boolean onOptionsItemSelected(MenuItem item) {
            // Handle action bar item clicks here. The action bar will
            // automatically handle clicks on the Home/Up button, so long
            // as you specify a parent activity in AndroidManifest.xml.
            int id = item.getItemId();

            //noinspection SimplifiableIfStatement
            if (id == R.id.action_leave) {
                leave();
                return true;
            }

            return super.onOptionsItemSelected(item);
        }

        private void addLog(String message) {
            mMessages.add(new Message.Builder(Message.TYPE_LOG)
                    .message(message).build());
            mAdapter.notifyItemInserted(mMessages.size() - 1);
            scrollToBottom();
        }

        private void addParticipantsLog(int numUsers) {
            addLog(getResources().getQuantityString(R.plurals.message_participants, numUsers, numUsers));
        }

        private void addMessage(String username, String message) {
            mMessages.add(new Message.Builder(Message.TYPE_MESSAGE)
                    .username(username).message(message).build());
            mAdapter.notifyItemInserted(mMessages.size() - 1);
            scrollToBottom();
        }

        private void addTyping(String username) {
            mMessages.add(new Message.Builder(Message.TYPE_ACTION)
                    .username(username).build());
            mAdapter.notifyItemInserted(mMessages.size() - 1);
            scrollToBottom();
        }

        private void removeTyping(String username) {
            for (int i = mMessages.size() - 1; i >= 0; i--) {
                Message message = mMessages.get(i);
                if (message.getType() == Message.TYPE_ACTION && message.getUsername().equals(username)) {
                    mMessages.remove(i);
                    mAdapter.notifyItemRemoved(i);
                }
            }
        }

        private void attemptSend() {
            if (null == mUsername) return;
            if (!mSocket.connected()) return;

            mTyping = false;

            String message = mInputMessageView.getText().toString().trim();
            if (TextUtils.isEmpty(message)) {
                mInputMessageView.requestFocus();
                return;
            }

            mInputMessageView.setText("");
            addMessage(mUsername, message);

            // perform the sending message attempt.
            mSocket.emit("new message", message);
        }

        private void startSignIn() {
            mUsername = null;
            Intent intent = new Intent(getActivity(), LoginActivity.class);
            startActivityForResult(intent, REQUEST_LOGIN);
        }

        private void leave() {
            mUsername = null;
            mSocket.disconnect();
            mSocket.connect();
            startSignIn();
        }

        private void scrollToBottom() {
            mMessagesView.scrollToPosition(mAdapter.getItemCount() - 1);
        }

        private Emitter.Listener onConnectError = new Emitter.Listener() {
            @Override
            public void call(Object... args) {
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(getActivity().getApplicationContext(),
                                R.string.error_connect, Toast.LENGTH_LONG).show();
                    }
                });
            }
        };

        private Emitter.Listener onNewMessage = new Emitter.Listener() {
            @Override
            public void call(final Object... args) {
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        JSONObject data = (JSONObject) args[0];
                        String username;
                        String message;
                        try {
                            username = data.getString("username");
                            message = data.getString("message");
                        } catch (JSONException e) {
                            return;
                        }

                        removeTyping(username);
                        addMessage(username, message);
                    }
                });
            }
        };

        private Emitter.Listener onUserJoined = new Emitter.Listener() {
            @Override
            public void call(final Object... args) {
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        JSONObject data = (JSONObject) args[0];
                        String username;
                        int numUsers;
                        try {
                            username = data.getString("username");
                            numUsers = data.getInt("numUsers");
                        } catch (JSONException e) {
                            return;
                        }

                        addLog(getResources().getString(R.string.message_user_joined, username));
                        addParticipantsLog(numUsers);
                    }
                });
            }
        };

        private Emitter.Listener onUserLeft = new Emitter.Listener() {
            @Override
            public void call(final Object... args) {
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        JSONObject data = (JSONObject) args[0];
                        String username;
                        int numUsers;
                        try {
                            username = data.getString("username");
                            numUsers = data.getInt("numUsers");
                        } catch (JSONException e) {
                            return;
                        }

                        addLog(getResources().getString(R.string.message_user_left, username));
                        addParticipantsLog(numUsers);
                        removeTyping(username);
                    }
                });
            }
        };

        private Emitter.Listener onTyping = new Emitter.Listener() {
            @Override
            public void call(final Object... args) {
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        JSONObject data = (JSONObject) args[0];
                        String username;
                        try {
                            username = data.getString("username");
                        } catch (JSONException e) {
                            return;
                        }
                        addTyping(username);
                    }
                });
            }
        };

        private Emitter.Listener onStopTyping = new Emitter.Listener() {
            @Override
            public void call(final Object... args) {
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        JSONObject data = (JSONObject) args[0];
                        String username;
                        try {
                            username = data.getString("username");
                        } catch (JSONException e) {
                            return;
                        }
                        removeTyping(username);
                    }
                });
            }
        };

        private Runnable onTypingTimeout = new Runnable() {
            @Override
            public void run() {
                if (!mTyping) return;

                mTyping = false;
                mSocket.emit("stop typing");
            }
        };
    }

Ở đây chúng ta có thể thấy, mới vào sẽ gọi hàm startSignin() để bật lên LoginActivity để đăng nhập. Tại hàm onCreate() ta lắng nghe các sự kiện của server trả về như sau

     mSocket.on(Socket.EVENT_CONNECT_ERROR, onConnectError);
    mSocket.on(Socket.EVENT_CONNECT_TIMEOUT, onConnectError);
    mSocket.on("new message", onNewMessage);
    mSocket.on("user joined", onUserJoined);
    mSocket.on("user left", onUserLeft);
    mSocket.on("typing", onTyping);
    mSocket.on("stop typing", onStopTyping);

Chúng ta lắng nghe khi có user đăng nhập vào, user thoát ra, khi user đang gõ, khi kết nối lỗi.

Tại hàm attemptmptSend() chúng ta gửi tin nhắn mới lên server . File này rất dễ hiểu phải không nào, có chỗ nào không hiểu, hãy comment để mình trả lời nhé 😃

  1. Ta sửa file activity_main.xml như sau.
      // activity_main.xml

    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
                 xmlns:tools="http://schemas.android.com/tools"
                 android:id="@+id/container"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
                 tools:context=".MainActivity"
                 tools:ignore="MergeRootFrame">

        <fragment
                class="com.ln.androidchat.MainFragment"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>
    </FrameLayout>

Chúng ta sử dụng fragment để dễ tuỳ biến hơn, bạn nhớ thay com.ln.androidchat bằng package của mình nhé.

  1. Ok, cuối cùng là chỉnh sửa file AndroidManifest.xml là xong rồi, thêm quyền truy cập internet, khai báo file application và khai báo activity nhé
     // AndroidManifest.xml

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.ln.androidchat" >

        <uses-permission android:name="android.permission.INTERNET" />

        <application
            android:name=".ChatApplication"
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:theme="@style/AppTheme" >
            <activity
                android:name=".MainActivity"
                android:label="@string/app_name" >
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />

                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>

            <activity
                android:name=".LoginActivity"
                android:label="@string/title_activity_login"
                android:theme="@style/Theme.AppCompat"
                android:windowSoftInputMode="adjustResize|stateVisible" >
            </activity>
        </application>

Vậy là bạn đã hoàn thành ứng dụng ChatApp cực kì đơn giản rồi. Bạn build thử xem nhé. Nhớ phải luôn chạy server nhé.

Cảm ơn bạn đã đọc.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí