DEV Community

Karim Abdallah
Karim Abdallah

Posted on

Create Websocket Android Application Using Ktor

In this article i will show you the simple way to create webscocket android app (client side) and create websocket server side with Kotlin Ktor.

project review

First, Let's Create Server Side (websocket server)
Open Intellij Ide or any ide you prefer then create a new project and select Ktor from left side then click next.

Image 2

now you will see project plugins page, Search for 'Routing' then add it & search for 'WebSockets' and add it then click Create

Image 3

after project builds you must have these files in your project:

Image 4

Now open build.gradle.kts and add these dependencies

dependencies {
    implementation("io.ktor:ktor-server-netty:$ktor_version")
    implementation("io.ktor:ktor-server-core-jvm:$ktor_version")
    implementation("io.ktor:ktor-server-websockets-jvm:$ktor_version")
    implementation("io.ktor:ktor-server-netty-jvm:$ktor_version")
    implementation("ch.qos.logback:logback-classic:$logback_version")
    testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
}
Enter fullscreen mode Exit fullscreen mode

build.gradle.kts file

Now we want to create application.conf inside resources folder and add inside it these lines.

Ktor uses this file to determine the port on which it should run, and it also defines the entry point of our application.

ktor {
    deployment {
        port = 8080
        port = ${?PORT}
    }
    application {
        modules = [ com.example.ApplicationKt.module ]
    }
}
Enter fullscreen mode Exit fullscreen mode

now we have set the application entry point at com.example.ApplicationKt.module so let's go to Application.kt file, and this is our entry point.

you don't have to create application.conf file you can use fun main() to put your port & host (ip) instead of it.

Image 6

then, go to Sockets.kt file and put this code in it

routing {
    webSocket("/chat") {
        send("You are connected!")
        for(frame in incoming) {
            frame as? Frame.Text ?: continue
            val receivedText = frame.readText()
            send("You said: $receivedText")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

final code

Now Our server is ready, Let's run it by opening application.kt file and press on the green play button and you must see this at the terminal

start server

to check if our server is running properly or not, just open Postman & open new file & choose websocket request then put your server ws://url:port/chat in that scheme: ws://127.0.0.1:8080/chat and click connect

postman

now if you send any message you will see it at realtime so our server is running correctly that's awesome.
But this is so simple and not a chat, Let's create more advanced websocket chat
first create new file in your main project package called Connections.kt then add this code in it

package com.example

import io.ktor.websocket.*
import java.util.concurrent.atomic.*

class Connection(val session: DefaultWebSocketSession) {
    companion object {
        val lastId = AtomicInteger(0)
    }
    val name = "user${lastId.getAndIncrement()}"
}
Enter fullscreen mode Exit fullscreen mode

Image 8

Go back to Sockets.kt and modify routing by adding these new codes

routing {
    val connections = Collections.synchronizedSet<Connection?>(LinkedHashSet())
    webSocket("/chat") {
        println("Adding user!")
        val thisConnection = Connection(this)
        connections += thisConnection
        try {
            send("There are ${connections.count()} users here.")
            for (frame in incoming) {
                frame as? Frame.Text ?: continue
                val receivedText = frame.readText()
                val textWithUsername = "[${thisConnection.name}]: $receivedText"
                connections.forEach {
                    it.session.send(textWithUsername)
                    println(textWithUsername)
                }
            }
        } catch (e: Exception) {
            println(e.localizedMessage)
        } finally {
            println("Removing $thisConnection!")
            connections -= thisConnection
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

re-run the server and try sending messages with postman again to see the results.

postman

now you see that the chat is little advanced and every user joins the chat takes unique username

Now Our Server Is Ready To Be Linked With Our Android App :)


Okay, Let's Create Client Side (Android Application)
Open Android studio or any Ide you have, then create a new project & select Kotlin as a primary language.

create project

Then, open build.gradle app level file and put this implementation at dependencies.

dependencies {
    implementation 'org.java-websocket:Java-WebSocket:1.4.0'
}
Enter fullscreen mode Exit fullscreen mode

we will use View Binding in this project so don't forget to activate it at build.gradle file.

After gradle sync complete, open AndroidManifest.xml and put the internet permission.

<uses-permission android:name="android.permission.INTERNET" />
Enter fullscreen mode Exit fullscreen mode

then we will store server url at strings.xml go to 'res > values > strings.xml' and add this line & modify your server url as you want

<string name="server_url" translatable="false">ws://127.0.0.1:8080</string>
Enter fullscreen mode Exit fullscreen mode

Now we need to create chat ui so open activity_main.xml file and add this codes to add ListView, EditText and Button

<?xml version="1.0" encoding="utf-8"?>
<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">

    <LinearLayout
        android:id="@+id/linear2"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:orientation="vertical"
        android:padding="4dp">

        <ListView
            android:id="@+id/listview1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:choiceMode="none"
            android:padding="8dp"/>
    </LinearLayout>

    <LinearLayout
        android:id="@+id/linear1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="2dp">

        <EditText
            android:id="@+id/edit_msg"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:hint="Enter Message"
            android:importantForAutofill="no"
            android:padding="8dp"
            android:textColor="#000000"
            android:textColorHint="#607D8B"
            android:textSize="12sp"
            tools:ignore="TextFields" />

        <Button
            android:id="@+id/btnSend"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="8dp"
            android:text="Send"
            android:textColor="#FFFFFF"
            android:textSize="16sp" />
    </LinearLayout>

</LinearLayout>
Enter fullscreen mode Exit fullscreen mode

after that Create new kotlin class called ChatWebSocketClient.kt and add these line of code

this class handle every action that happen like when connection open, close, error and when receive a message from server.

package com.encept.websocket_client

import org.java_websocket.client.WebSocketClient
import org.java_websocket.handshake.ServerHandshake
import java.net.URI

// initialize websocket client
class ChatWebSocketClient(serverUri: URI, private val messageListener: (String) -> Unit) : WebSocketClient(serverUri) {

    override fun onOpen(handshakedata: ServerHandshake?) {
        // When WebSocket connection opened
    }

    override fun onClose(code: Int, reason: String?, remote: Boolean) {
        // When WebSocket connection closed
    }

    override fun onMessage(message: String?) {
        // When Receive a message we handle it at MainActivity
        messageListener.invoke(message ?: "")
    }

    override fun onError(ex: Exception?) {
        // When An error occurred
    }

    fun sendMessage(message: String) {
        send(message)
    }
}
Enter fullscreen mode Exit fullscreen mode

Now open MainActivity.kt and put these variables after class declaration

private lateinit var binding: ActivityMainBinding
private lateinit var webSocketClient: ChatWebSocketClient
private var AL: ArrayList<HashMap<String, Any>> = ArrayList()
Enter fullscreen mode Exit fullscreen mode

Then, Add this onCreate function with these codes that initialize

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)

    //declare the ui widgets using binding
    val btnSend = binding.btnSend
    val editMsg = binding.editMsg
    val listview1 = binding.listview1

    // load server url from strings.xml
    val serverUri = URI(getString(R.string.server_url))

    webSocketClient = ChatWebSocketClient(serverUri) { message ->
        // display incoming message in ListView
        runOnUiThread {
            run {
                val _item = HashMap<String, Any>()
                _item["message"] = message
                AL.add(_item)
            }
            listview1.adapter = ListviewAdapter(AL)
        }
    }
    // connect to websocket server
    webSocketClient.connect()

    btnSend.setOnClickListener {
        try {
            // send message to websocket server
            webSocketClient.sendMessage(editMsg.text.toString())
            editMsg.setText("")
        } catch (e: Exception) {
            e.printStackTrace()
            Toast.makeText(this, e.toString(), Toast.LENGTH_SHORT).show()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  • in case of you're using IDE that does not support auto import just add these imports to your MainActivity.kt
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import com.encept.websocketchat.databinding.ActivityMainBinding
import java.net.URI
import android.widget.*
import java.util.*
Enter fullscreen mode Exit fullscreen mode

Now we need to close Websocket connection when activity destroy.

override fun onDestroy() {
    super.onDestroy()
    // close websocket connection
    webSocketClient.close()
}
Enter fullscreen mode Exit fullscreen mode

now we need to create listview custom view, so create new xml file called custom.xml and add this code we will just add TextView to display any message.

<?xml version="1.0" encoding="utf-8"?>
<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">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="8dp"
        android:orientation="horizontal"
        tools:ignore="UselessParent">

        <TextView
            android:id="@+id/text2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="text"
            android:textSize="16sp"
            android:textStyle="bold"
            android:textColor="#000000"/>
    </LinearLayout>

</LinearLayout>
Enter fullscreen mode Exit fullscreen mode

After onCreate fun Add The ListView Adapter

class ListviewAdapter(private val _data: ArrayList<HashMap<String, Any>>) : BaseAdapter() {
    override fun getCount(): Int {
        return _data.size
    }

    override fun getItem(_index: Int): HashMap<String, Any> {
        return _data[_index]
    }

    override fun getItemId(_index: Int): Long {
        return _index.toLong()
    }

    override fun getView(_position: Int, _v: View?, _container: ViewGroup?): View? {
        val _inflater = LayoutInflater.from(_container?.context)
        var _view = _v
        if (_view == null) {
            _view = _inflater.inflate(R.layout.cul, _container, false)
        }

        val text2 = _view?.findViewById<TextView>(R.id.text2)

        if (text2 != null) {
            text2.text = _data[_position]["message"].toString()
        }

        return _view
    }

}
Enter fullscreen mode Exit fullscreen mode

That's all for today i hope this article helped you :)

Sources and References:
Ktor docs
websocket client
websocket server

Top comments (1)

Collapse
 
karim_abdallah profile image
Karim Abdallah • Edited

if you have any question please contact me here.