Creating a realtime chat app with android , NodeJs and Socket.io
Mohamed Aymen Ourabi
Senior Frontend Engineer | React | Angular | Node Js | Typescript
Introduction
WebSockets are very beautiful tools that allows us to establish a realtime communication in modern web applications. In fact this mechanism is so powerfull and it's used to build different kind of apps like realtime chat or notification system etc ..
In this article we will show you how to build a realtime chat app using android nodeJs and Socket.io
Getting started
Our chat app is divded into 2 part :
1- Server side : a node js server with the implementation of socket.io for server
2- Client side : creating the android app and implementing socket.io for client
Our NodeJs Server
well , to make things clear our project architecture will be composed of 2 files :
package.json which will handle all the depandencies for our node js app and index.js which will be our main server .
After creating the two files , we open the commande line under our project directory and execute this command
npm install --save express socket.io
now in our index.js file we will build our server and make all the configurations so that it will look like this
const express = require('express'),
http = require('http'),
app = express(),
server = http.createServer(app),
io = require('socket.io').listen(server);
app.get('/', (req, res) => {
res.send('Chat Server is running on port 3000')
});
server.listen(3000,()=>{
console.log('Node app is running on port 3000')
});
to make sure that our server is runing go to the command line under our project directory and execute this command
node index.js
NOTE: using node command we can run any server created with node environment but the problem is that we have to run the same commande every time we update our index.js file , so to make things more simple we can use nodemon command which will automatically restart our server every time we make changes
so to install nodemon go to your command line and run
npm install -g nodemon
to make sure that our project is running we should see this log in our console
now comes the best part !!
we will try now to implement some socket.io methods in our server to handle all the events of our chat app including users connection states and messages .
in our index.js file we add the first implementation that will detect if we have a user connected to our server
io.on('connection', (socket) => {
console.log('user connected')
socket.on('join', function(userNickname) {
console.log(userNickname +" : has joined the chat " )
socket.broadcast.emit('userjoinedthechat',userNickname +" : has joined the chat ")
});
});
Actually socket.io mechanism is based on listening and firing events , in this first implementation that we have done the (on) method which takes two parameters ('eventname',callback) defines a listener to an event called connection and this event will be fired from the client side so that node js can handle it , after that we defined a method that will listen to an emitted event called 'join' and will log the name of the user who has join the chat in the console .
Now when node js detect a user it fires an event to the client side called 'userjoinedthechat' using the method emit , note that socket.broadcast.emit will send the event to every single user connected to the server except the sender .
if we want to send the message to all users including the sender we just have to use io.emit() instead of socket.emit().
Now to handle messages we add these few lines and we can see that we have added extra argument to the callback function which are the user nickname and the message content , actually these informations will be sent from the client side when firing the event 'messagedetection'
socket.on('messagedetection', (senderNickname,messageContent) => {
//log the message in console
console.log(senderNickname+" :" +messageContent)
//create a message object
let message = {"message":messageContent, "senderNickname":senderNickname}
// send the message to the client side
socket.emit('message', message )
});
And finally when the user disconnect from the client side , the event will be handled by this implementation
socket.on('disconnect', function() {
console.log( 'user has left ')
socket.broadcast.emit( "userdisconnect" ,' user has left')
});
Now that our server is ready the index.js file should look like this
const express = require('express'),
http = require('http'),
app = express(),
server = http.createServer(app),
io = require('socket.io').listen(server);
app.get('/', (req, res) => {
res.send('Chat Server is running on port 3000')
});
io.on('connection', (socket) => {
console.log('user connected')
socket.on('join', function(userNickname) {
console.log(userNickname +" : has joined the chat " );
socket.broadcast.emit('userjoinedthechat',userNickname +" : has joined the chat ");
})
socket.on('messagedetection', (senderNickname,messageContent) => {
//log the message in console
console.log(senderNickname+" : " +messageContent)
//create a message object
let message = {"message":messageContent, "senderNickname":senderNickname}
// send the message to all users including the sender using io.emit()
io.emit('message', message )
})
socket.on('disconnect', function() {
console.log(userNickname +' has left ')
socket.broadcast.emit( "userdisconnect" ,' user has left')
})
})
server.listen(3000,()=>{
console.log('Node app is running on port 3000')
})
Our Android app (Socket client)
To start open android studio and create a new projet with an empty activity , after that open app build.gradle file and add these dependencies then synchronize your project.
compile 'com.android.support:recyclerview-v7:25.3.1'
compile('com.github.nkzawa:socket.io-client:0.5.0') {
exclude group: 'org.json', module: 'json'
}
Now about these lines :
the first one is the recycler view that we will use to display the list of our messages and the second one is the library that will provid us with the implementation of socket.io for the client side so that we can fire or listen to events .
don't forget to enable INTERNET permission in you manifest.xml
<uses-permission android:name="android.permission.INTERNET" ></uses-permission>
In activity_main.xml we will add an EditText for the user to put his nickname and a button that allows him to enter the chatbox
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"xmlns:app="https://schemas.android.com/apk/res-auto"xmlns:tools="https://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context="com.example.aymen.androidchat.MainActivity">
<EditText
android:id="@+id/nickname"android:layout_centerInParent="true"android:textSize="30dp"android:hint="Enter your nickname !"android:layout_width="match_parent"android:layout_height="wrap_content" /><Buttonandroid:layout_below="@+id/nickname"android:id="@+id/enterchat"android:text="Go to chat "android:layout_width="match_parent"android:layout_height="wrap_content" />
</RelativeLayout>
so that the preview will look like this
now your MainActivity.java should look like this
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
public class MainActivity extends AppCompatActivity {
private Button btn;
private EditText nickname;
public static final String NICKNAME = "usernickname";
@Overrideprotected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//call UI components by id
btn = (Button)findViewById(R.id.enterchat) ;
nickname = (EditText) findViewById(R.id.nickname);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//if the nickname is not empty go to chatbox activity and add the nickname to the intent extra
if(!nickname.getText().toString().isEmpty()){
Intent i = new Intent(MainActivity.this,ChatBoxActivity.class);
//retreive nickname from EditText and add it to intent extra
i.putExtra(NICKNAME,nickname.getText().toString());
startActivity(i);
}
}
});
}
}
Now create a second empty activity called ChatBoxActivity and in activity_chat_box.xml add these lines
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"xmlns:app="https://schemas.android.com/apk/res-auto"xmlns:tools="https://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context="com.example.aymen.androidchat.ChatBoxActivity">
<LinearLayout
android:weightSum="3"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerViewandroid:layout_weight="3"android:id="@+id/messagelist"android:layout_width="match_parent"android:layout_height="wrap_content"android:clipToPadding="false"android:scrollbars="vertical"/><Viewandroid:layout_marginTop="5mm"android:id="@+id/separator"android:layout_width="match_parent"android:layout_height="1dp"android:background="@android:color/darker_gray"/>
<LinearLayoutandroid:weightSum="3"android:orientation="horizontal"android:layout_width="match_parent"android:layout_height="wrap_content">
<EditText
android:id="@+id/message"android:layout_weight="3"android:layout_width="wrap_content"android:hint="your message"
android:layout_height="match_parent" />
<Button
android:id="@+id/send"android:layout_width="wrap_content"android:layout_height="wrap_content"android:background="#00000000"android:text="send"
/></LinearLayout>
</LinearLayout>
</RelativeLayout>
your preview should look like this
Now before implementing the socket client we should create an adapter to handle and display our messages for that we need to create a file called item.xml and a java class called message which have two simple string properties (nickname,message) .
In our project directory along side with activites create a file called Message.java :
public class Message {
private String nickname;
private String message ;
public Message(){
}
public Message(String nickname, String message) {
this.nickname = nickname;
this.message = message;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
now create a file called item.xml under layout directory and add these lines
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"android:orientation="horizontal" android:layout_width="match_parent"android:layout_height="match_parent"><TextViewandroid:id="@id/nickname"android:textSize="15dp"android:textStyle="bold"android:text="Nickname : "android:layout_width="wrap_content"android:layout_height="wrap_content" /><TextViewandroid:id="@id/message"android:textSize="15dp"android:text=" message "android:layout_width="wrap_content"android:layout_height="wrap_content" />
</LinearLayout>
create a file called ChatBoxAdapter.java and put these lines
package com.example.aymen.androidchat;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.List;
public class ChatBoxAdapter extends RecyclerView.Adapter<ChatBoxAdapter.MyViewHolder> {
private List<Message> MessageList;
public class MyViewHolder extends RecyclerView.ViewHolder {
public TextView nickname;
public TextView message;
public MyViewHolder(View view) {
super(view);
nickname = (TextView) view.findViewById(R.id.nickname);
message = (TextView) view.findViewById(R.id.message);
}
}
// in this adaper constructor we add the list of messages as a parameter so that
// we will passe it when making an instance of the adapter object in our activity
public ChatBoxAdapter(List<Message>MessagesList) {
this.MessageList = MessagesList;
}
@Overridepublic int getItemCount() {
return MessageList.size();
}
@Overridepublic ChatBoxAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item, parent, false);
return new ChatBoxAdapter.MyViewHolder(itemView);
}
@Overridepublic void onBindViewHolder(final ChatBoxAdapter.MyViewHolder holder, final int position) {
//binding the data from our ArrayList of object to the item.xml using the viewholder
Message m = MessageList.get(position);
holder.nickname.setText(m.getNickname());
holder.message.setText(m.getMessage() );
}
}
now with everything setup we can implement the socket client in our ChatBoxActivity.java so this is how we are going to proceed :
- Get the nickname of the user from the intent extra
- call and implement all the methods relative to recycler view including the adapter instanciation
- declare and define the host for socket client to make connection with the server
- handle all events fired from the server
- emit events when user connect , disconnect or send a message
but before that let's check if everything is ok or not so in our ChatBoxActivity we will declare the socket object and add the socket connection in the method onCreate so that when the activity is called the socket client will directly fire the event connection
public class ChatBoxActivity extends AppCompatActivity {
//declare socket object
private Socket socket;
private String Nickname ;
@Overrideprotected
void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_box);
// get the nickame of the user
Nickname= (String)getIntent().getExtras().getString(MainActivity.NICKNAME);
//connect you socket client to the server
try {
//if you are using a phone device you should connect to same local network as your laptop and disable your pubic firewall as well
socket = IO.socket("https://yourlocalIPaddress:3000");
//create connection
socket.connect()
// emit the event join along side with the nickname
socket.emit('join',Nickname);
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
}
now run your emulator and enter a nickname in the first activity then click go to chat you will see a log in your server console that indicates that a user has successfully made a connection with the server and we can see that the listener for the fired event join in our server is working properly so that it logs the name of the user connected
now with everything working , we should not forget that when our server handle an event it broadcast other costum events as well and so those fired events should be handled in the client side , for that we will make the first listener for the event "userjoinedthechat" which is a custom event fired when the server handle the event "join".
in our ChatBoxActivity we will add these lines
socket.on("userjoinedthechat", new Emitter.Listener() {
@Overridepublic void call(final Object... args) {
runOnUiThread(new Runnable() {
@Overridepublic void run() {
String data = (String) args[0];
// get the extra data from the fired event and display a toast
Toast.makeText(ChatBoxActivity.this,data,Toast.LENGTH_SHORT).show();
}
});
}
now we run 2 emulator in the same time and we enter two different nicknames from both sides and as we can see that one of the two emulator indicated that a user has successfully joined the chat
now comes the best part of our app which is the chat messages :
to display the messages we have to proceed this way
- add onclickListener to the button send and grab the message content from the EditText after that emit the event "messagedetection" using emit() method along side with the nickname of the sender and the message content
- the event will be handled by the server and broadcasted to all users
- adding a socket listener in android to listen for the event "message" fired by the server
- extract the nickname and the message from the extra data and make a new instance of the object Message
- adding the instance to the ArrayList of messages and notify the adapter to update the recycler view
But before that let's setup our recyler view , Adapter , message textfield and the button send.
Add the declarations below in ChatBoxActivity
public RecyclerView myRecylerView ;
public List<Message> MessageList ;
public ChatBoxAdapter chatBoxAdapter;
public EditText messagetxt ;
public Button send ;
in the method onCreate add these lines
messagetxt = (EditText) findViewById(R.id.message) ;
send = (Button)findViewById(R.id.send);
MessageList = new ArrayList<>();
myRecylerView = (RecyclerView) findViewById(R.id.messagelist);
RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext());
myRecylerView.setLayoutManager(mLayoutManager);
myRecylerView.setItemAnimator(new DefaultItemAnimator());
Now in your ChatBoxActivity the button action should look like this
send.setOnClickListener(new View.OnClickListener() {
@Overridepublic void onClick(View v) {
//retrieve the nickname and the message content and fire the event messagedetection
if(!messagetxt.getText().toString().isEmpty()){
socket.emit("messagedetection",Nickname,messagetxt.getText().toString());
messagetxt.setText(" ");
}
}
});
and the listener should look like this
socket.on("message", new Emitter.Listener() {
@Overridepublic void call(final Object... args) {
runOnUiThread(new Runnable() {
@Overridepublic void run() {
JSONObject data = (JSONObject) args[0];
try {
//extract data from fired event
String nickname = data.getString("senderNickname");
String message = data.getString("message");
// make instance of message
Message m = new Message(nickname,message);
//add the message to the messageList
MessageList.add(m);
// add the new updated list to the adapter
chatBoxAdapter = new ChatBoxAdapter(MessageList);
// notify the adapter to update the recycler view
chatBoxAdapter.notifyDataSetChanged();
//set the adapter for the recycler view
myRecylerView.setAdapter(chatBoxAdapter);
} catch (JSONException e) {
e.printStackTrace();
}
}
});
}
});
as we can see in the screenshot below everything is working properly :)) and the messages are displaying from both sides , note that we can connect with many other users but we just have to run other emulators and enter nicknames to join the chatbox
before ending this tutorial we have to make our last functionnality which detects if the user has disconnected from the chatbox .
In our ChatBoxActivity override the method onDestroy() and add these lines
@Override
protected void onDestroy() {
super.onDestroy();
socket.disconnect();
}
and for the listener
socket.on("userdisconnect", new Emitter.Listener() {
@Overridepublic void call(final Object... args) {
runOnUiThread(new Runnable() {
@Overridepublic void run() {
String data = (String) args[0];
Toast.makeText(ChatBoxActivity.this,data,Toast.LENGTH_SHORT).show();
}
});
}
});
finally our ChatBoxActivity will look like this
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import com.github.nkzawa.emitter.Emitter;
import com.github.nkzawa.socketio.client.IO;
import com.github.nkzawa.socketio.client.Socket;
import org.json.JSONException;
import org.json.JSONObject;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
public class ChatBoxActivity extends AppCompatActivity {
public RecyclerView myRecylerView ;
public List<Message> MessageList ;
public ChatBoxAdapter chatBoxAdapter;
public EditText messagetxt ;
public Button send ;
//declare socket objectprivate Socket socket;
public String Nickname ;
@Overrideprotected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_box);
messagetxt = (EditText) findViewById(R.id.message) ;
send = (Button)findViewById(R.id.send);
// get the nickame of the user
Nickname= (String)getIntent().getExtras().getString(MainActivity.NICKNAME);
//connect you socket client to the servertry {
socket = IO.socket("https://yourlocalIPaddress:3000");
socket.connect();
socket.emit("join", Nickname);
} catch (URISyntaxException e) {
e.printStackTrace();
}
//setting up recyler
MessageList = new ArrayList<>();
myRecylerView = (RecyclerView) findViewById(R.id.messagelist);
RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext());
myRecylerView.setLayoutManager(mLayoutManager);
myRecylerView.setItemAnimator(new DefaultItemAnimator());
// message send action
send.setOnClickListener(new View.OnClickListener() {
@Overridepublic void onClick(View v) {
//retrieve the nickname and the message content and fire the event messagedetectionif(!messagetxt.getText().toString().isEmpty()){
socket.emit("messagedetection",Nickname,messagetxt.getText().toString());
messagetxt.setText(" ");
}
}
});
//implementing socket listeners
socket.on("userjoinedthechat", new Emitter.Listener() {
@Overridepublic void call(final Object... args) {
runOnUiThread(new Runnable() {
@Overridepublic void run() {
String data = (String) args[0];
Toast.makeText(ChatBoxActivity.this,data,Toast.LENGTH_SHORT).show();
}
});
}
});
socket.on("userdisconnect", new Emitter.Listener() {
@Overridepublic void call(final Object... args) {
runOnUiThread(new Runnable() {
@Overridepublic void run() {
String data = (String) args[0];
Toast.makeText(ChatBoxActivity.this,data,Toast.LENGTH_SHORT).show();
}
});
}
});
socket.on("message", new Emitter.Listener() {
@Overridepublic void call(final Object... args) {
runOnUiThread(new Runnable() {
@Overridepublic void run() {
JSONObject data = (JSONObject) args[0];
try {
//extract data from fired event
String nickname = data.getString("senderNickname");
String message = data.getString("message");
// make instance of message
Message m = new Message(nickname,message);
//add the message to the messageList
MessageList.add(m);
// add the new updated list to the dapter
chatBoxAdapter = new ChatBoxAdapter(MessageList);
// notify the adapter to update the recycler view
chatBoxAdapter.notifyDataSetChanged();
//set the adapter for the recycler view
myRecylerView.setAdapter(chatBoxAdapter);
} catch (JSONException e) {
e.printStackTrace();
}
}
});
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
socket.disconnect();
}
}
Conclusion
In this exemple we had a great look at the usage of socket.io along side with node js and android , we tried as well to explain some basics and understand the mechanism of socket.io and how to establish a bi-directional communications between a client and a server , note that there are other tools in socket.io like rooms and namespaces which could be very helpfull to make beautifull web and mobile apps .
find in these related links the two projects :
client side : https://gitlab.com/medaymen/AndroidChatClient
server side: https://gitlab.com/medaymen/AndroidChatServer
?? Aspiring DevOps Engineer | ??? AWS | ?? Docker | ?? Jenkins | ? Kubernetes | ?? Terraform | ?? Helm | Prometheus | Grafana | Kibana | ?? AI & MLOps Enthusiast | ?? CI/CD & Automation Advocate
3 年hi sir this is very helpful.. can you please share socket io web and mobile apps..
Software development specialist
3 年Hello do we implemented any video calling app
Android/iOS App/SDK Developer (4 Yrs Exp)@ SecureLayer7 (sensFRX) - Elevating Security Solutions with Code Expertise.
3 年Hello sir need your help
Front End Team Lead | Angular | React | React Native | JS
6 年Thanks man
Principal Application Security Consultant @ AppSec | Product Security | FinTech 3X | DevSecOps Professional ( CDP & CCSK & CEH & More)
6 年Thanks a lot