Ably Chat is a set of purpose-built APIs for a host of chat features enabling you to create 1:1, 1:Many, Many:1 and Many:Many chat rooms for any scale. It is designed to meet a wide range of chat use cases, such as livestreams, in-game communication, customer support, or social interactions in SaaS products. Built on Ably's core service, it abstracts complex details to enable efficient chat architectures.
Important
This SDK is currently under development. If you are interested in being an early adopter and providing feedback then you can sign up to the private beta and are welcome to provide us with feedback. Coming soon: chat moderation, editing and deleting messages.
Get started using the 📚 documentation.
This SDK works on Android 7.0+ (API level 24+) and Java 8+.
This project is under development so we will be incrementally adding new features. At this stage, you'll find APIs for the following chat features:
- Chat rooms for 1:1, 1:many, many:1 and many:many participation.
- Sending and receiving chat messages.
- Online status aka presence of chat participants.
- Chat room occupancy, i.e total number of connections and presence members.
- Typing indicators
- Room-level reactions (ephemeral at this stage)
If there are other features you'd like us to prioritize, please let us know.
You will need the following prerequisites:
- An Ably account
- You can sign up to the generous free tier.
- An Ably API key
- Use the default or create a new API key in an app within your Ably account dashboard.
- Make sure your API key has the
following capabilities:
publish
,subscribe
,presence
,history
andchannel-metadata
.
The Ably client library follows Semantic Versioning. See https://github.com/ably/ably-chat-kotlin/tags for a list of tagged releases.
To instantiate the Chat SDK, create an Ably client and pass it into the Chat constructor:
import com.ably.chat.ChatClient
import io.ably.lib.realtime.AblyRealtime
import io.ably.lib.types.ClientOptions
val realtimeClient = AblyRealtime(
ClientOptions().apply {
key = "<api-key>"
clientId = "<client-id>"
},
)
val chatClient = ChatClient(realtimeClient)
You can use basic authentication i.e. the API Key directly for testing purposes, however it is strongly recommended that you use token authentication in production environments.
To use Chat you must also set a clientId
so that clients are
identifiable. If you are prototyping, you can use java.util.UUID
to generate an ID.
In most cases, clientId
can be set to userId
, the user’s application-specific identifier, provided that userId
is unique within the context of your application.
The Chat SDK uses a single connection to Ably, which is exposed via the ChatClient#connection
property. You can use this
property to observe the connection state and take action accordingly.
You can view the current connection status at any time:
val connectionStatus = chat.connection.status
val connectionError = chat.connection.error
You can subscribe to connection status changes by registering a listener, like so:
val subscription = chat.connection.onStatusChange { statusChange: ConnectionStatusChange ->
println(statusChange.toString())
}
To stop listening to changes, call the unsubscribe
method on the returned subscription instance:
subscription.unsubscribe()
You can create or retrieve a chat room with name "basketball-stream"
this way:
val room = chat.rooms.get("basketball-stream", RoomOptions(reactions = RoomReactionsOptions()))
The second argument to rooms.get
is a RoomOptions
argument, which tells the Chat SDK what features you would like your room to use and
how they should be configured.
You can also use RoomOptions.default
to enable all room features with the default configuration.
For example, you can set the timeout between keystrokes for typing events as part of the room options. Sensible defaults for each of the features are provided for your convenience:
- A typing timeout (time of inactivity before typing stops) of 5 seconds.
- Entry into, and subscription to, presence.
The defaults options for each feature may be viewed here.
Here’s an example demonstrating how to specify a custom typing timeout:
val room = chat.rooms.get(
"basketball-stream",
RoomOptions(typing = TypingOptions(timeoutMs = 3_000)),
)
In order to use the same room but with different options, you must first release
the room before requesting an instance with the changed
options (see below for more information on releasing rooms).
Note that:
- If a
release
call is currently in progress for the room (see below), then a call toget
will wait for that to resolve before resolving itself. - If a
get
call is currently in progress for the room andrelease
is called, theget
call will reject.
To start receiving events on a room, it must first be attached. This can be done using the attach
method.
// Add a listener so it's ready at attach time (see below for more information on listeners)
room.messages.subscribe { message: Message ->
println(message.toString())
}
room.attach()
To stop receiving events on a room, it must be detached, which can be achieved by using the detach
method.
room.detach()
Note: This does not remove any event listeners you have registered and they will begin to receive events again in the event that the room is re-attached.
Depending on your application, you may have multiple rooms that come and go over time (e.g. if you are running 1:1 support chat). When you
are completely finished with a room, you may release
it which allows the underlying resources to be collected.
rooms.release("basketball-stream")
Once release
is called, the room will become unusable and you will need to get a new instance using rooms.get
should you wish to
re-start the room.
Note
Releasing a room may be optional for many applications. If release is not called, the server will automatically tidy up connections and other resources associated with the room after a period of time.
Monitoring the status of the room is key to a number of common chat features. For example, you might want to display a warning when the room has become detached.
To get the current status, you can use the status
property:
val roomStatus = room.status
val roomError = room.error
You can also subscribe to changes in the room status and be notified whenever they happen by registering a listener:
val subscription = room.onStatusChange { statusChange: RoomStatusChange ->
println(statusChange.toString())
}
To stop listening to changes, unsubscribe
method on returned subscription instance:
subscription.unsubscribe()
There may be instances where the connection to Ably is lost for a period of time, for example, when the user enters a tunnel. In many circumstances, the connection will recover and operation will continue with no discontinuity of messages. However, during extended periods of disconnection, continuity cannot be guaranteed and you'll need to take steps to recover messages you might have missed.
Each feature of the Chat SDK provides an onDiscontinuity
handler. Here you can register a listener that will be notified whenever a
discontinuity in that feature has been observed.
Taking messages as an example, you can listen for discontinuities like so:
val subscription = room.messages.onDiscontinuity { reason: ErrorInfo? ->
// Recover from the discontinuity
}
To stop listening to discontinuities, unsubscribe
method on returned subscription instance.
To send a message, simply call send
on the room.messages
property, with the message you want to send.
val message = room.messages.send(text = "text")
When you're done with the listener, call unsubscribe
to remove that listeners subscription and prevent it from receiving
any more events.
val subscription = room.messages.subscribe { message: Message ->
println(message.toString())
}
// Time passes...
subscription.unsubscribe()
The messages object also exposes the get
method which can be used to request historical messages in the chat room according
to the given criteria. It returns a paginated response that can be used to request more messages.
var historicalMessages = room.messages.get(orderBy = NewestFirst, limit = 50)
println(historicalMessages.items.toString())
while (historicalMessages.hasNext()) {
historicalMessages = historicalMessages.next()
println(historicalMessages.items.toString())
}
println("End of messages")
In addition to being able to unsubscribe from messages, the return value from messages.subscribe
also includes the getPreviousMessages
method. It can be used to request
historical messages in the chat room that were sent up to the point that a particular listener was subscribed. It returns a
paginated response that can be used to request for more messages.
val subscription = room.messages.subscribe {
println("New message received")
}
var historicalMessages = subscription.getPreviousMessages(limit = 50)
println(historicalMessages.items.toString())
while (historicalMessages.hasNext()) {
historicalMessages = historicalMessages.next()
println(historicalMessages.items.toString())
}
println("End of messages")
You can get the complete list of currently online or present members, their state and data, by calling the presence#get
method.
// Retrieve the entire list of present members
val presentMembers = room.presence.get()
// You can supply a clientId to retrieve the presence of a specific member with the given clientId
val presentMember = room.presence.get(clientId = "client-id")
// You can call this to get a simple boolean value of whether a member is present or not
val isPresent = room.presence.isUserPresent("client-id")
Calls to presence#get()
will return a list of the presence messages, where each message contains the most recent
data for a member.
To appear online for other users, you can enter the presence set of a chat room. While entering presence, you can provide optional data that will be associated with the presence message.
room.presence.enter(
JsonObject().apply {
addProperty("status", "online")
},
)
Updates allow you to make changes to the custom data associated with a present user. Common use-cases include updating the users' status or profile picture.
room.presence.update(
JsonObject().apply {
addProperty("status", "busy")
},
)
Ably automatically triggers a presence leave if a client goes offline. But you can also manually leave the presence set as a result of a UI action. While leaving presence, you can provide optional data that will be associated with the presence message.
room.presence.leave(
JsonObject().apply {
addProperty("status", "Be back later!")
},
)
You can provide a single listener, if so, the listener will be subscribed to receive all presence event types.
val subscription = room.presence.subscribe { event: PresenceEvent ->
when (event.action) {
Action.enter -> println("${event.clientId} entered with data: ${event.data}")
Action.leave -> println("${event.clientId} left")
Action.update -> println("${event.clientId} updated with data: ${event.data}")
}
}
To unsubscribe a specific listener from presence events, you can call the unsubscribe
method provided in the subscription object returned
by the subscribe
call.
val subscription = room.presence.subscribe { event: PresenceEvent ->
// Handle events
}
// Unsubscribe
subscription.unsubscribe()
Note
You should be attached to the room to enable this functionality.
Typing events allow you to inform others that a client is typing and also subscribe to others' typing status.
You can get the complete set of the current typing clientId
s, by calling the typing.get
method.
// Retrieve the entire list of currently typing clients
val currentlyTypingClientIds = room.typing.get()
To inform other users that you are typing, you can call the start method. This will begin a timer that will automatically stop typing after a set amount of time.
room.typing.start()
Repeated calls to start will reset the timer, so the clients typing status will remain active.
room.typing.start()
// Some short delay - still typing
room.typing.start()
// Some short delay - still typing
room.typing.start()
// Some long delay - timer expires, stopped typing event emitted and listeners are notified
You can immediately stop typing without waiting for the timer to expire.
room.typing.start()
// Some short delay - timer not yet expired
room.typing.stop()
// Timer cleared and stopped typing event emitted and listeners are notified
To subscribe to typing events, provide a listener to the subscribe
method.
val subscription = room.typing.subscribe { event: TypingEvent ->
println("currently typing: ${event.currentlyTyping}")
}
To unsubscribe the listener, you can call the unsubscribe
method on the subscription object returned by the subscribe
call:
val subscription = room.typing.subscribe { event: TypingEvent ->
println("currently typing: ${event.currentlyTyping}")
}
// Time passes
subscription.unsubscribe()
Occupancy tells you how many users are connected to the chat room.
To subscribe to occupancy updates, subscribe a listener to the chat rooms occupancy
member:
val subscription = room.occupancy.subscribe { event: OccupancyEvent ->
println(event.toString())
}
To unsubscribe the listener, you can call the unsubscribe
method on the subscription object returned by the subscribe
call:
val subscription = room.occupancy.subscribe { event: OccupancyEvent ->
println(event.toString())
}
// Time passes...
subscription.unsubscribe()
Occupancy updates are delivered in near-real-time, with updates in quick succession batched together for performance.
You can request the current occupancy of a chat room using the occupancy.get
method:
val occupancy = room.occupancy.get()
You can subscribe to and send ephemeral room-level reactions by using the room.reactions
objects.
To send room-level reactions, you must be attached to the room.
To send a reaction such as "like"
:
room.reactions.send(type = "like")
You can also add any metadata and headers to reactions:
room.reactions.send(
type ="like",
metadata = JsonObject().apply { addProperty("effect", "fireworks") },
headers = mapOf("streamId" to "basketball-stream"),
)
Subscribe to receive room-level reactions:
val subscription = room.reactions.subscribe { reaction: ReactionEvent ->
println("received a ${reaction.type} with metadata ${reaction.metadata}")
}
To unsubscribe the listener, you can call the unsubscribe
method on the subscription object returned by the subscribe
call:
val subscription = room.reactions.subscribe { reaction: ReactionEvent ->
println("received a ${reaction.type} with metadata ${reaction.metadata}")
}
// Time passes...
subscription.unsubscribe()
It might be useful to know that each feature is backed by an underlying Pub/Sub channel. You can use this information to enable interoperability with other platforms by subscribing to the channels directly using the Ably Pub/Sub SDKs for those platforms.
The channel for each feature can be obtained via the channel
property
on that feature.
val messagesChannel = room.messages.channel
Warning: You should not attempt to change the state of a channel directly. Doing so may cause unintended side-effects in the Chat SDK.
For a given chat room, the channels used for features are as follows:
Feature | Channel |
---|---|
Messages | <roomId>::$chat::$chatMessages |
Presence | <roomId>::$chat::$chatMessages |
Occupancy | <roomId>::$chat::$chatMessages |
Reactions | <roomId>::$chat::$reactions |
Typing | <roomId>::$chat::$typingIndicators |
For guidance on how to contribute to this project, see the contributing guidelines.
Please visit http://support.ably.com/ for access to our knowledge base and to ask for any assistance. You can also view the community reported Github issues or raise one yourself.
To see what has changed in recent versions, see the changelog.
- See a simple chat example in this repo.
- Sign up to the private beta and get started.
- Share feedback or request a new feature.