Getting started: Chat with JVM (Kotlin/Java)
This guide will help you get started with Ably Chat in a new JVM application using Kotlin.
You'll learn how to create chat rooms, send and edit messages, and implement realtime features like typing indicators and presence. You'll also cover message history, reactions, and proper connection management.
Prerequisites
Ably
-
Sign up for an Ably account.
-
Create a new app, and get your first API key. You can use the root API key that is provided by default, within the API Keys tab to get started.
Create a new project
Create a new JVM project using Gradle or Maven. We'll use Gradle with Kotlin DSL for this guide:
- Create a new directory for your project:
mkdir chat-jvm-example
cd chat-jvm-example
- Initialize a new Gradle project:
gradle init --type kotlin-application
- Update your
build.gradle.kts
file to include the Ably dependencies:
1
2
3
4
5
6
7
implementation("com.ably.chat:chat:<latest-version>")
// We will need Kotlin coroutines for this demo
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
// Required so that slf4j has an implementation to log to
implementation("org.slf4j:slf4j-nop:2.0.17")
(Optional) Install Ably CLI
The Ably CLI provides a command-line interface for managing your Ably applications directly from your terminal.
- Install the Ably CLI:
npm install -g @ably/cli
- Run the following to log in to your Ably account and set the default app and API key:
ably login
Step 1: Connect to Ably
Clients establish a connection with Ably when they instantiate an SDK. This enables them to send and receive messages in realtime across channels.
Add the following code to the App.kt
file created by gradle init
set up the Ably client.
Note that this is for example purposes only. In production, you should use token authentication to avoid exposing your API keys publicly. The clientId
is used to identify the client when using an API key:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.example
import com.ably.chat.*
import io.ably.lib.realtime.AblyRealtime
import io.ably.lib.types.ClientOptions
import kotlinx.coroutines.*
import com.google.gson.JsonObject
import java.text.SimpleDateFormat
import java.util.*
val ABLY_KEY = "demokey:*****"
suspend fun main() {
demonstrateMessages()
}
suspend fun demonstrateMessages() {
val realtimeClient = AblyRealtime(
ClientOptions().apply {
// In production, you should use token authentication to avoid exposing your API keys publicly
key = ABLY_KEY
clientId = "my-first-client"
}
)
val chatClient = ChatClient(realtimeClient) { logLevel = LogLevel.Info }
// Monitor connection status
val subscription = chatClient.connection.onStatusChange { change ->
println("Connection status: ${change.current}")
}
println("Chat client initialized. Connection status: ${chatClient.connection.status}")
// Keep the application running
delay(5000)
subscription.unsubscribe()
realtimeClient.close()
}
Run the application with ./gradlew run
. You should see the connection status logged to the console.
Step 2: Create a room and send a message
Messages are how your clients interact with one another. Use rooms to separate and organize clients and messages into different topics, or 'chat rooms'. Rooms are the entry object into Chat, providing access to all of its features, such as messages, presence and reactions.
Update your demonstrateMessages
function to create a room, attach to it, and send a message:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
suspend fun demonstrateMessages() {
val realtimeClient = AblyRealtime(
ClientOptions().apply {
key = ABLY_KEY
clientId = "my-first-client"
}
)
val chatClient = ChatClient(realtimeClient) { logLevel = LogLevel.Info }
// Monitor connection status
chatClient.connection.onStatusChange { change ->
println("Connection status: ${change.current}")
}
// Get and attach to a room
val room = chatClient.rooms.get("my-first-room")
room.attach()
println("Room '${room.name}' status: ${room.status}")
// Subscribe to messages
val subscription = room.messages.subscribe { messageEvent ->
val message = messageEvent.message
val timestamp = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date(message.timestamp))
println("[$timestamp] ${message.clientId}: ${message.text}")
}
// Send a message
val myMessage = room.messages.send("Hello from JVM!")
println("Sent message with serial: ${myMessage.serial}")
// Keep listening for messages
println("Listening for messages... (press Ctrl+C to exit)")
delay(30000) // Keep running for 30 seconds
// Clean up
subscription.unsubscribe()
room.detach()
realtimeClient.close()
}
Run the application again with ./gradlew run
. You'll see your message appear in the console.
You can also use the Ably CLI to send a message to the room from another environment:
ably rooms messages send my-first-room 'Hello from CLI!'
You should see the CLI message appear in your application's console output.
Step 3: Edit a message
If your client makes a typo, or needs to update their original message then they can edit it.
Update the code from Step 2 for creating a subscription and sending a message to also handle message edits:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Subscribe to message updates
val subscription = room.messages.subscribe { messageEvent ->
val message = messageEvent.message
val timestamp = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date(message.timestamp))
when (messageEvent.type) {
ChatMessageEventType.Created -> {
println("[$timestamp] NEW: ${message.clientId}: ${message.text}")
}
ChatMessageEventType.Updated -> {
println("[$timestamp] EDITED: ${message.clientId}: ${message.text}")
}
else -> Unit
}
}
// Send and then edit a message
val myMessage = room.messages.send("Hello from JVM!")
println("Sent message with serial: ${myMessage.serial}")
delay(1000) // Wait a bit
// Edit the message
val editedMessage = myMessage.copy(text = "Hello from JVM! (edited)")
room.messages.update(editedMessage)
println("Updated message with serial: ${myMessage.serial}")
// Wait a bit to receive messages
delay(5000)
When you run the application, you'll see both the original message and the edited version in the console.
Step 4: Message history and continuity
Ably Chat provides a method for retrieving messages that have been previously sent in a room, up until the point that a client joins (attaches) to it. This enables clients joining a room part way through a conversation to receive the context of what has happened, and what is being discussed.
Use the Ably CLI to send some additional messages to your room, for example:
ably rooms messages send my-first-room 'Historical message 1'
ably rooms messages send my-first-room 'Historical message 2'
ably rooms messages send my-first-room 'Historical message 3'
Create a new function to demonstrate retrieving message history:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
suspend fun demonstrateHistory() {
val realtimeClient = AblyRealtime(
ClientOptions().apply {
key = ABLY_KEY
clientId = "history-client"
}
)
val chatClient = ChatClient(realtimeClient) { logLevel = LogLevel.Info }
val room = chatClient.rooms.get("my-first-room")
room.attach()
// Subscribe to new messages and get historical messages
val subscription = room.messages.subscribe { messageEvent ->
println("NEW MESSAGE: ${messageEvent.message.clientId}: ${messageEvent.message.text}")
}
// Retrieve the last 10 messages
val historicalMessages = subscription.historyBeforeSubscribe(limit = 10)
println("=== HISTORICAL MESSAGES ===")
historicalMessages.items.reversed().forEach { message ->
val timestamp = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date(message.timestamp))
println("[$timestamp] ${message.clientId}: ${message.text}")
}
println("=== END HISTORY ===")
delay(10000)
subscription.unsubscribe()
room.detach()
realtimeClient.close()
}
Update your main function to call this new function:
1
2
3
suspend fun main() {
demonstrateHistory()
}
You should now see the message that you have previously sent in this chat room.
Step 5: Show who is typing a message
Typing indicators enable you to display messages to clients when someone is currently typing. An event is emitted when someone starts typing, when they press a keystroke, and then another event is emitted after a configurable amount of time has passed without a key press.
Add typing functionality to your main application:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
suspend fun demonstrateTyping() {
val realtimeClient = AblyRealtime(
ClientOptions().apply {
key = ABLY_KEY
clientId = "typing-client"
}
)
val chatClient = ChatClient(realtimeClient) { logLevel = LogLevel.Info }
val room = chatClient.rooms.get("my-first-room")
room.attach()
// Subscribe to typing events
val subscription = room.typing.subscribe { typingEvent ->
if (typingEvent.currentlyTyping.isEmpty()) {
println("No one is currently typing")
} else {
println("${typingEvent.currentlyTyping.joinToString(", ")} is typing...")
}
}
// Simulate typing
println("Starting to type...")
room.typing.keystroke()
delay(2000)
println("Stopping typing...")
room.typing.stop()
delay(5000)
subscription.unsubscribe()
room.detach()
realtimeClient.close()
}
Update your main function to call this new function:
1
2
3
suspend fun main() {
demonstrateTyping()
}
You can also use the Ably CLI to simulate typing events:
ably rooms typing subscribe my-first-room
Step 6: Display online status
Display the online status of clients using the presence feature. This enables clients to be aware of one another if they are present in the same room.
Add presence functionality:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
suspend fun demonstratePresence() {
val realtimeClient = AblyRealtime(
ClientOptions().apply {
key = ABLY_KEY
clientId = "presence-client"
}
)
val chatClient = ChatClient(realtimeClient) { logLevel = LogLevel.Info }
val room = chatClient.rooms.get("my-first-room")
room.attach()
// Subscribe to presence events
val subscription = room.presence.subscribe { presenceEvent ->
val member = presenceEvent.member
when (presenceEvent.type) {
PresenceEventType.Enter -> {
println("${member.clientId} entered the room with data: ${member.data}")
}
PresenceEventType.Leave -> {
println("${member.clientId} left the room with data: ${member.data}")
}
PresenceEventType.Update -> {
println("${member.clientId} updated their presence: ${member.data}")
}
PresenceEventType.Present -> {
println("${member.clientId} is present with data: ${member.data}")
}
}
}
// Enter the presence set
room.presence.enter(
JsonObject().apply {
addProperty("status", "Online")
},
)
println("Entered presence set")
// Get current presence members
val members = room.presence.get()
println("Currently online (${members.size} members):")
members.forEach { member ->
println(" - ${member.clientId}: ${member.data}")
}
delay(10000)
// Leave presence before closing
room.presence.leave(
JsonObject().apply {
addProperty("status", "Offline")
},
)
delay(1000)
subscription.unsubscribe()
room.detach()
realtimeClient.close()
}
Update your main function to call this new function:
1
2
3
suspend fun main() {
demonstratePresence()
}
Use the Ably CLI to join the presence set from another client:
ably rooms presence enter my-first-room --data '{"status":"learning about Ably!"}'
Step 7: Send a room reaction
Clients can send an ephemeral reaction to a room to show their sentiment for what is happening, such as a point being scored in a sports game.
Add reaction functionality:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
suspend fun demonstrateReactions() {
val realtimeClient = AblyRealtime(
ClientOptions().apply {
key = ABLY_KEY
clientId = "reaction-client"
}
)
val chatClient = ChatClient(realtimeClient) { logLevel = LogLevel.Info }
val room = chatClient.rooms.get("my-first-room")
room.attach()
// Subscribe to reactions
val subscription = room.reactions.subscribe { event ->
println("${event.reaction.clientId} reacted with: ${event.reaction.name}")
}
// Send some reactions
val reactions = listOf("👍", "❤️", "🚀", "🎉")
for (reaction in reactions) {
room.reactions.send(reaction)
println("Sent reaction: $reaction")
delay(1000)
}
delay(5000)
subscription.unsubscribe()
room.detach()
realtimeClient.close()
}
Update your main function to call this new function:
1
2
3
suspend fun main() {
demonstrateReactions()
}
Use the Ably CLI to send reactions to the room:
ably rooms reactions send my-first-room 👍
ably rooms reactions send my-first-room 🎉
Step 8: Close the connection
Connections are automatically closed approximately 2 minutes after no heartbeat is detected by Ably. Explicitly closing connections when they are no longer needed is good practice to help save costs. It will also remove all listeners that were registered by the client.
Add proper connection management to your application:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
suspend fun fullChatDemo() {
val realtimeClient = AblyRealtime(
ClientOptions().apply {
key = ABLY_KEY
clientId = "demo-client"
}
)
val chatClient = ChatClient(realtimeClient) { logLevel = LogLevel.Info }
val room = chatClient.rooms.get("my-first-room")
try {
// Connection monitoring
chatClient.connection.onStatusChange { change ->
println("Connection status changed to: ${change.current}")
}
// Attach the room
room.attach()
println("=== Full Chat Demo Started ===")
// Subscribe to all events
room.messages.subscribe { messageEvent ->
val message = messageEvent.message
val timestamp = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date(message.timestamp))
val eventType = if (messageEvent.type == ChatMessageEventType.Created) "NEW" else "EDIT"
println("[$timestamp] $eventType: ${message.clientId}: ${message.text}")
}
room.presence.subscribe { presenceEvent ->
println("PRESENCE: ${presenceEvent.member.clientId} ${presenceEvent.type}")
}
room.reactions.subscribe { event ->
println("${event.reaction.clientId} reacted with: ${event.reaction.name}")
}
room.typing.subscribe { typingEvent ->
val typingUsers = typingEvent.currentlyTyping.joinToString(", ")
if (typingUsers.isNotEmpty()) {
println("TYPING: $typingUsers is typing...")
}
}
// Enter presence
room.presence.enter(
JsonObject().apply {
addProperty("status", "Online")
},
)
// Send a message
room.messages.send("Hello everyone! This is a JVM chat client.")
// Send a reaction
room.reactions.send("🎉")
// Demonstrate typing
room.typing.keystroke()
delay(2000)
room.typing.stop()
println("Demo running... Send messages via CLI or wait for auto-close")
delay(30000) // Run for 30 seconds
} catch (e: Exception) {
println("Error: ${e.message}")
} finally {
// Always close the connection
println("=== Closing connection ===")
realtimeClient.close()
room.detach()
println("Connection closed")
}
}
suspend fun main() {
fullChatDemo()
}
This demonstrates proper resource management and graceful shutdown of the chat client.
Next steps
Continue to explore the documentation with Kotlin as the selected language:
- Understand token authentication before going to production.
- Read more about using rooms and sending messages.
- Find out more regarding presence.
- Read into pulling messages from history and providing context to new joiners.
Explore the Ably CLI further, or check out the Chat Kotlin API references for additional functionality.