# Getting started: Chat with Android This guide will help you get started with Ably Chat in a new Android Kotlin application built with Jetpack Compose. 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. ![Screenshot of the completed Android Chat application showing a web interface with connection status, a message input field, realtime message display, and a presence indicator showing online users. The interface demonstrates the key features you'll build including publishing messages, subscribing to receive messages in realtime, and tracking which clients are currently present in the channel.](https://raw.githubusercontent.com/ably/docs/main/src/images/content/screenshots/getting-started/chat-kotlin-getting-started-guide.png) ## Prerequisites ### Ably 1. [Sign up](https://ably.com/signup) for an Ably account. 2. Create a [new app](https://ably.com/accounts/any/apps/new), 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 Android project with Jetpack Compose. For detailed instructions, refer to the [Android Studio documentation](https://developer.android.com/jetpack/compose/setup). 1. Create a new Android project in Android Studio. 2. Select **Empty Activity** as the template. 3. Name the project **Chat Example** and place it in the `com.example.chatexample` package. 4. Set the minimum SDK level to API 24 or higher. 5. Select Kotlin as the programming language. 6. Add the Ably dependencies to your app-level `build.gradle.kts` file: ```kotlin implementation("com.ably.chat:chat:") // This package contains extension functions for better Jetpack Compose integration. // It's experimental for now (safe to use, but the API may change later). You can always use its code as a reference. implementation("com.ably.chat:chat-extensions-compose:") ``` ### (Optional) Install Ably CLI Use the [Ably CLI](https://github.com/ably/cli) as an additional client to quickly test chat features. It can simulate other users by sending messages, entering presence, and acting as another user typing a message. 1. Install the Ably CLI: ```shell npm install -g @ably/cli ``` 2. Run the following to log in to your Ably account and set the default app and API key: ```shell ably login ``` ## Step 1: Setting up Ably Replace the contents of your `MainActivity.kt` file with the following code to set up the Ably client. Note that this is for example purposes only. In production, you should use [token authentication](https://ably.com/docs/auth/token.md) to avoid exposing your API keys publicly, the [`clientId`](https://ably.com/docs/auth/identified-clients.md) is used to identify the client: ```kotlin package com.example.chatexample import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* import androidx.compose.material.icons.* import androidx.compose.material.icons.automirrored.filled.Send import androidx.compose.material.icons.filled.Edit import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.* import com.example.chatexample.ui.theme.ChatExampleTheme import com.ably.chat.* import com.ably.chat.extensions.compose.* import io.ably.lib.realtime.AblyRealtime import io.ably.lib.types.ClientOptions import kotlinx.coroutines.launch import java.text.* import java.util.* class MainActivity : ComponentActivity() { private lateinit var realtimeClient: AblyRealtime private lateinit var chatClient: ChatClient override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) realtimeClient = AblyRealtime( ClientOptions().apply { key = "your-api-key" // In production, you should use token authentication to avoid exposing your API keys publicly clientId = "my-first-client" }, ) chatClient = ChatClient(realtimeClient) { logLevel = LogLevel.Info } enableEdgeToEdge() setContent { ChatExampleTheme { App(chatClient) } } } } @Composable fun App(chatClient: ChatClient) { Scaffold { paddingValues -> Column( modifier = Modifier .fillMaxSize() .imePadding() .padding(paddingValues) ) { Text("Hello Chat App") } } } ``` ## Step 2: 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. In your `MainActivity.kt` file, add the following `ConnectionStatusUi` composable component: ```kotlin // This component will display the current connection status @Composable fun ConnectionStatusUi(connection: Connection) { val connectionStatus by connection.collectAsStatus() Text( text = "Connection Status: $connectionStatus", style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(start = 8.dp) ) } ``` Update the `App` component to display the connection status using the new `ConnectionStatusUi` component: ```kotlin @Composable fun App(chatClient: ChatClient) { Scaffold { paddingValues -> Column( modifier = Modifier .fillMaxSize() .imePadding() .padding(paddingValues) ) { ConnectionStatusUi(connection = chatClient.connection) } } } ``` Run your application by pressing **Run** button. ## Step 3: Create a room Now that you have a connection to Ably, you can create a room. Use rooms to separate and organize clients and messages into different topics, or 'chat rooms'. Rooms are the entry point for Chat, providing access to all of its features, such as messages, presence and reactions. In your project, open `MainActivity.kt`, and add a new component called `RoomStatusUi`: ```kotlin @Composable fun RoomStatusUi(roomName: String, room: Room?) { val roomStatus = room?.collectAsStatus()?.value Text( text = "Room Name: ${roomName}, Room Status: ${roomStatus ?: ""}", style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(start = 8.dp) ) } ``` Update your main app component to get and attach to the room and nest the `RoomStatusUi` component inside it: ```kotlin @Composable fun App(chatClient: ChatClient) { val roomName = "my-first-room" var room by remember { mutableStateOf(null) } LaunchedEffect(roomName) { val chatRoom = chatClient.rooms.get(roomName) chatRoom.attach() room = chatRoom } Scaffold { paddingValues -> Column( modifier = Modifier .fillMaxSize() .imePadding() .padding(paddingValues) ) { ConnectionStatusUi(connection = chatClient.connection) RoomStatusUi(roomName = roomName, room = room) } } } ``` The above code creates a room with the ID `my-first-room` and sets up a listener to monitor the room status. It also displays the room ID and current status in the UI. ## Step 4: Send a message Messages are how your clients interact with one another. In your project, open `MainActivity.kt`, and add a new component called `ChatBox`: ```kotlin @Composable fun ChatBox(room: Room?) { val scope = rememberCoroutineScope() var textInput by remember { mutableStateOf(TextFieldValue("")) } var sending by remember { mutableStateOf(false) } val messages = remember { mutableStateListOf() } LaunchedEffect(room) { room?.messages?.asFlow()?.collect { event -> when (event.type) { MessageEventType.Created -> { // Check if the incoming message is correctly ordered if (messages.isNotEmpty() && messages[0].serial > event.message.serial) { // If the message arrived out of order - you should find the correct insertion point based on serial // This is omitted for brevity, but production code should maintain serial order } messages.add(0, event.message) } else -> Unit } } } Column( modifier = Modifier .fillMaxWidth() .padding(8.dp) ) { Card( modifier = Modifier .weight(1f) .fillMaxWidth(), ) { if (messages.isNullOrEmpty()) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Text( text = "No messages yet", color = Color.Gray ) } } else { LazyColumn( modifier = Modifier .fillMaxSize() .padding(8.dp), reverseLayout = true, ) { items(messages.size, key = { messages[it].serial }) { val message = messages[it] Column( modifier = Modifier .fillMaxWidth() .padding(vertical = 4.dp) ) { Text( text = "${message.clientId}: ${message.text}", style = MaterialTheme.typography.bodyMedium ) Text( text = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) .format(Date(message.timestamp)), style = MaterialTheme.typography.labelSmall, color = Color.Gray, modifier = Modifier.align(Alignment.End) ) HorizontalDivider(modifier = Modifier.padding(top = 4.dp)) } } } } } Spacer(modifier = Modifier.height(8.dp)) // Message input Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { OutlinedTextField( value = textInput, onValueChange = { textInput = it }, modifier = Modifier.weight(1f), placeholder = { Text("Type a message") }, maxLines = 3 ) Spacer(modifier = Modifier.width(8.dp)) Button( onClick = { sending = true if (textInput.text.isNotBlank()) { scope.launch { try { room?.messages?.send(textInput.text) } catch (e: Exception) { Log.e("APP", e.message, e) } textInput = TextFieldValue("") sending = false } } }, enabled = textInput.text.isNotBlank() && !sending ) { Icon( imageVector = Icons.AutoMirrored.Filled.Send, contentDescription = "Send" ) } } } } ``` Add the `ChatBox` component to your main app component: ```kotlin @Composable fun App(chatClient: ChatClient) { val roomName = "my-first-room" var room by remember { mutableStateOf(null) } LaunchedEffect(roomName) { val chatRoom = chatClient.rooms.get( roomName, ) chatRoom.attach() room = chatRoom } Scaffold { paddingValues -> Column( modifier = Modifier .fillMaxSize() .imePadding() .padding(paddingValues) ) { ConnectionStatusUi(connection = chatClient.connection) RoomStatusUi(roomName = roomName, room = room) HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) ChatBox(room = room) } } } ``` The UI should automatically render the new component, and you should be able to send messages to the room. Type a message in the input box and click the send button. You'll see the message appear in the chat box. You can also use the Ably CLI to send a message to the room from another environment: ```shell ably rooms messages send my-first-room 'Hello from CLI!' ``` You'll see the message in your app's chat box UI. If you have sent a message via CLI, it should appear in a different color to the one you sent from the app. ## Step 5: Edit a message If your client makes a typo, or needs to update their original message then they can edit it. To do this, you can extend the functionality of the `ChatBox` component to allow updating of messages. 1. We will begin by adding this utility function to facilitate message data updates in the UI: ```kotlin inline fun MutableList.replaceFirstWith(replacement: T, predicate: (T) -> Boolean) { val index = indexOfFirst(predicate) if (index != -1) set(index, replacement) } ``` 2. Add the edited state variable: ```kotlin var edited: Message? by remember { mutableStateOf(null) } ``` 3. Update the message subscription to handle edited messages: ```kotlin LaunchedEffect(room) { room?.messages?.asFlow()?.collect { event -> when (event.type) { MessageEventType.Created -> messages.add(0, event.message) MessageEventType.Updated -> messages.replaceFirstWith(event.message) { it.serial == event.message.serial } else -> Unit } } } ``` 4. Let's enhance the message display. To add an edit button to each message, we'll first need to locate the `Column` composable function within your layout inside the `items(messages.size, key = { messages[it].serial }) {` declarative loop. This is the component responsible for rendering the message content itself, along with the sender's username and the message timestamp. Once we've identified this `Column`, we will integrate the new edit button alongside these existing elements. Replace the `Column` with the following: ```kotlin Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { Column( modifier = Modifier .weight(1f) .padding(vertical = 4.dp) ) { // Message content Text( text = "${message.clientId}: ${message.text}", style = MaterialTheme.typography.bodyMedium ) // Timestamp Text( text = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) .format(Date(message.timestamp)), style = MaterialTheme.typography.labelSmall, color = Color.Gray, modifier = Modifier.align(Alignment.End) ) HorizontalDivider(modifier = Modifier.padding(top = 4.dp)) } // Edit button IconButton(onClick = { edited = message textInput = TextFieldValue(message.text) }) { Icon(Icons.Default.Edit, contentDescription = "Edit") } } ``` 5. Update the send button to handle both new messages and edits: ```kotlin Button( onClick = { sending = true if (textInput.text.isNotBlank()) { scope.launch { try { edited?.let { room?.messages?.update(it.copy(text = textInput.text)) } ?: room?.messages?.send(textInput.text) } catch (e: Exception) { Log.e("APP", e.message, e) } edited = null textInput = TextFieldValue("") sending = false } } }, enabled = textInput.text.isNotBlank() && !sending ) { Icon( imageVector = if (edited != null) Icons.Filled.Edit else Icons.AutoMirrored.Filled.Send, contentDescription = if (edited != null) "Edit" else "Send" ) } ``` When you click on the edit button in the UI, you can modify the text and it will send the updated message contents to the room. ## Step 6: Message history and continuity Ably Chat enables you to retrieve previously sent messages in a room. This is useful for providing conversational context when a user first joins a room, or when they subsequently rejoin it later on. The message subscription object exposes the [`historyBeforeSubscribe()`](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/-messages-subscription/history-before-subscribe.html) method to enable this functionality. This method returns a paginated response, which can be queried further to retrieve the next set of messages. Extend the `ChatBox` component to include a method to retrieve the last 10 messages when the component mounts. In your `MainActivity.kt` file, add the `DisposableEffect` in your `ChatBox` component: ```kotlin fun ChatBox(room: Room?) { /* variables declaration */ var subscription by remember { mutableStateOf(null) } DisposableEffect(room) { subscription = room?.messages?.subscribe { event -> when (event.type) { MessageEventType.Created -> messages.add(0, event.message) MessageEventType.Updated -> messages.replaceFirstWith(event.message) { it.serial == event.message.serial } else -> Unit } } onDispose { subscription?.unsubscribe() } } LaunchedEffect(subscription) { subscription?.let { sub -> val previousMessages = sub.historyBeforeSubscribe(10)?.items ?: emptyList() messages.addAll(previousMessages) } } /* rest of your code */ } ``` The above code will retrieve the last 10 messages when the component mounts, and set them in the state. You also can use `collectAsPagingMessagesState` to automatically subscribe to new messages and lazily load previous messages as you scroll through the message list. ```kotlin @Composable fun ChatBox(room: Room?) { val scope = rememberCoroutineScope() var textInput by remember { mutableStateOf(TextFieldValue("")) } val messagesState = room?.collectAsPagingMessagesState() var sending by remember { mutableStateOf(false) } var edited by remember { mutableStateOf(null) } val messages = messagesState?.loaded Column( modifier = Modifier .fillMaxWidth() .padding(8.dp) ) { Card( modifier = Modifier .weight(1f) .fillMaxWidth(), ) { if (messages.isNullOrEmpty()) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Text( text = "No messages yet", color = Color.Gray ) } } else { LazyColumn( modifier = Modifier .fillMaxSize() .padding(8.dp), reverseLayout = true, state = messagesState.listState, ) { items(messages.size, key = { messages[it].serial }) { val message = messages[it] Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { Column( modifier = Modifier .weight(1f) .padding(vertical = 4.dp) ) { Text( text = "${message.clientId}: ${message.text}", style = MaterialTheme.typography.bodyMedium ) Text( text = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) .format(Date(message.timestamp)), style = MaterialTheme.typography.labelSmall, color = Color.Gray, modifier = Modifier.align(Alignment.End) ) HorizontalDivider(modifier = Modifier.padding(top = 4.dp)) } IconButton(onClick = { edited = message textInput = TextFieldValue(message.text) }) { Icon(Icons.Default.Edit, contentDescription = "Edit") } } } } } } Spacer(modifier = Modifier.height(8.dp)) // Message input Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { OutlinedTextField( value = textInput, onValueChange = { textInput = it }, modifier = Modifier.weight(1f), placeholder = { Text("Type a message") }, maxLines = 3 ) Spacer(modifier = Modifier.width(8.dp)) Button( onClick = { sending = true if (textInput.text.isNotBlank()) { scope.launch { try { edited?.let { room?.messages?.update(it.copy(text = textInput.text)) } ?: room?.messages?.send(textInput.text) } catch (e: Exception) { Log.e("APP", e.message, e) } edited = null textInput = TextFieldValue("") sending = false } } }, enabled = textInput.text.isNotBlank() && !sending ) { Icon( imageVector = if (edited != null) Icons.Default.Edit else Icons.AutoMirrored.Filled.Send, contentDescription = if (edited != null) "Edit" else "Send" ) } } } } ``` Do the following to test this out: 1. Use the ably CLI to simulate sending some messages to the room from another client. 2. Refresh the page, this will cause the `ChatBox` component to mount again and call the `historyBeforeSubscribe()` method. 3. You'll see the last 10 messages appear in the chat box. ## Step 7: Display who is present in the room 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. You can then show clients who else is online, provide a custom status update for each, and notify the room when someone enters it, or leaves it, such as by going offline. In your `MainActivity.kt` file, create a new component called `PresenceStatusUi`: ```kotlin @Composable fun PresenceStatusUi(room: Room?) { val membersState = room?.collectAsPresenceMembers() val members = membersState?.value ?: emptyList() LaunchedEffect(room) { room?.presence?.enter() } Text( text = "Online: ${members.size}", style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(start = 8.dp) ) } ``` Add the `PresenceStatusUi` component to your main app component: ```kotlin @Composable fun App(chatClient: ChatClient) { val roomName = "my-first-room" var room by remember { mutableStateOf(null) } LaunchedEffect(roomName) { val chatRoom = chatClient.rooms.get( roomName, ) chatRoom.attach() room = chatRoom } Scaffold { paddingValues -> Column( modifier = Modifier .fillMaxSize() .imePadding() .padding(paddingValues) ) { ConnectionStatusUi(connection = chatClient.connection) RoomStatusUi(roomName = roomName, room = room) PresenceStatusUi(room = room) HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) ChatBox(room = room) } } } ``` You'll now see your current client ID in the list of present users. You can also use the Ably CLI to enter the room from another client by running the following command: ```shell ably rooms presence enter my-first-room ``` ## Step 8: Send a reaction Clients can send a reaction to a room to show their sentiment for what is happening, such as a point being scored in a sports game. These are short-lived (ephemeral) and are not stored in the room history. In your `MainActivity.kt` file, add a new component called `ReactionBar`: ```kotlin @Composable fun ReactionBar(room: Room?) { val scope = rememberCoroutineScope() val availableReactions = listOf("👍", "❤️", "💥", "🚀", "👎", "💔") val receivedReactions = remember { mutableStateListOf() } LaunchedEffect(room) { room?.reactions?.asFlow()?.collect { receivedReactions.add(it) } } Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { // Reaction send buttons LazyRow( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly, contentPadding = PaddingValues(vertical = 4.dp) ) { items(availableReactions) { emoji -> Button( onClick = { scope.launch { room?.reactions?.send(emoji) } }, colors = ButtonDefaults.buttonColors(containerColor = Color.LightGray) ) { Text( text = emoji, fontSize = 12.sp ) } } } // Display received reactions if (receivedReactions.isNotEmpty()) { Spacer(modifier = Modifier.height(4.dp)) Text( text = "Received Reactions", modifier = Modifier.padding(vertical = 2.dp) ) LazyRow( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, contentPadding = PaddingValues(vertical = 4.dp) ) { items(receivedReactions) { reaction -> Box( contentAlignment = Alignment.Center ) { Column( horizontalAlignment = Alignment.CenterHorizontally ) { Text( text = reaction.reaction.name, fontSize = 12.sp ) } } } } } } } ``` Add the `ReactionBar` component to your main app component: ```kotlin @Composable fun App(chatClient: ChatClient) { val roomName = "my-first-room" var room by remember { mutableStateOf(null) } LaunchedEffect(roomName) { val chatRoom = chatClient.rooms.get( roomName, ) chatRoom.attach() room = chatRoom } Scaffold { paddingValues -> Column( modifier = Modifier .fillMaxSize() .imePadding() .padding(paddingValues) ) { ConnectionStatusUi(connection = chatClient.connection) RoomStatusUi(roomName = roomName, room = room) PresenceStatusUi(room = room) ReactionBar(room = room) HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) ChatBox(room = room) } } } ``` The above code should display a list of reactions that can be sent to the room. When you click on a reaction, it will send it to the room and display it in the UI. You can also send a reaction to the room via the Ably CLI by running the following command: ```shell ably rooms reactions send my-first-room 👍 ``` ## Step 9: Disconnection and clean up resources To gracefully close connection and clean up resources, you can subscribe to activity lifecycle events and close the connection when activity has paused or stopped, and then reconnect when activity resumes. To do this modify `MainActivity` class: ```kotlin class MainActivity : ComponentActivity() { private lateinit var realtimeClient: AblyRealtime private lateinit var chatClient: ChatClient override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) realtimeClient = AblyRealtime( ClientOptions().apply { key = "your-api-key" clientId = "my-first-client" }, ) chatClient = ChatClient(realtimeClient) { logLevel = LogLevel.Info } enableEdgeToEdge() setContent { ChatExampleTheme { App(chatClient) } } } override fun onRestart() { super.onRestart() realtimeClient.connect() } override fun onResume() { super.onResume() realtimeClient.connect() } override fun onPause() { super.onPause() realtimeClient.close() } override fun onStop() { super.onStop() realtimeClient.close() } } ``` ## Next steps Continue to explore the documentation with Kotlin as the selected language: * Understand [token authentication](https://ably.com/docs/auth/token.md) before going to production. * Read more about using [rooms](https://ably.com/docs/chat/rooms.md) and sending [messages](https://ably.com/docs/chat/rooms/messages.md). * Find out more regarding [presence](https://ably.com/docs/chat/rooms/presence.md). * Read into pulling messages from [history](https://ably.com/docs/chat/rooms/history.md) and providing context to new joiners. Explore the [Ably CLI](https://www.npmjs.com/package/@ably/cli) further, or check out the [Chat Kotlin API references](https://sdk.ably.com/builds/ably/ably-chat-kotlin/main/dokka/chat/com.ably.chat/) for additional functionality.