Getting started: Chat with Kotlin

This guide will help you get started with Ably Chat in a new Android Kotlin application built with Jetpack Compose.

It will take you through the following steps:

  • Creating a client and establishing a realtime connection to Ably
  • Creating a room and subscribing to its messages
  • Sending messages to the room and editing messages
  • Retrieving historical messages to provide context for new joiners
  • Displaying online status of clients in the room
  • Subscribing to and sending reactions
  • Disconnecting and resource cleanup

Prerequisites

Ably

  1. Sign up for an Ably account.

  2. Create a new app and get your API key. You can use the root API key that is provided by default to get started.

  3. Install the Ably CLI:

npm install -g @ably/cli
  1. Run the following to log in to your Ably account and set the default app and API key:
ably login

ably apps switch
ably auth keys switch

Create a new project

Create a new Android project with Jetpack Compose. For detailed instructions, refer to the Android Studio documentation.

  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

1

2

3

4

implementation("com.ably.chat:chat-android:<latest-version>")
// 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:<latest-version>")

Step 1: Setting up Ably

Replace the contents of your MainActivity.kt file with the following code to set up the Ably client.

Replace {{API_KEY}} with your Ably API key from the dashboard. Note that this is for example purposes only. In production, you should use token authentication to avoid exposing your API keys publicly:

Kotlin

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

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 = "{{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

1

2

3

4

5

6

7

8

9

10

// This component will display the current connection status
@Composable
fun ConnectionStatusUi(connection: Connection) {
    val connectionStatus = 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

1

2

3

4

5

6

7

8

9

10

11

12

13

@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

1

2

3

4

5

6

7

8

9

@Composable
fun RoomStatusUi(roomName: String, room: Room?) {
    val roomStatus = room?.collectAsStatus()
    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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

@Composable
fun App(chatClient: ChatClient) {
    val roomName = "my-first-room"
    var room by remember { mutableStateOf<Room?>(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

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

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

@Composable
fun ChatBox(room: Room?) {
    val scope = rememberCoroutineScope()
    var textInput by remember { mutableStateOf(TextFieldValue("")) }
    var sending by remember { mutableStateOf(false) }
    val messages = remember { mutableStateListOf<Message>() }

    DisposableEffect(room) {
        val subscription = room?.messages?.subscribe { event ->
            when (event.type) {
                MessageEventType.Created -> messages.add(0, event.message)
                else -> Unit
            }
        }

        onDispose {
            subscription?.unsubscribe()
        }
    }

    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

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

@Composable
fun App(chatClient: ChatClient) {
    val roomName = "my-first-room"
    var room by remember { mutableStateOf<Room?>(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:

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

1

2

3

4

inline fun <T> MutableList<T>.replaceFirstWith(replacement: T, predicate: (T) -> Boolean) {
    val index = indexOfFirst(predicate)
    if (index != -1) set(index, replacement)
}
  1. Add the edited state variable:
Kotlin

1

var edited: Message? by remember { mutableStateOf(null) }
  1. Update the message subscription to handle edited messages:
Kotlin

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

DisposableEffect(room) {
    val 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()
    }
}
  1. 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

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

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")
    }
}
  1. Update the send button to handle both new messages and edits:
Kotlin

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

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 getPreviousMessages() 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

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

fun ChatBox(room: Room?) {
    /* variables declaration */

    DisposableEffect(room) {
      val 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
          }
      }

      scope.launch {
          val previousMessages = subscription?.getPreviousMessages(10)?.items ?: emptyList()
          messages.addAll(previousMessages)
      }

      onDispose {
          subscription?.unsubscribe()
      }
  }
  /* 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

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

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

@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<Message?>(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 getPreviousMessages() 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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

@Composable
fun PresenceStatusUi(room: Room?) {
    val members = room?.collectAsPresenceMembers()

    LaunchedEffect(room) {
        room?.presence?.enter()
    }

    Text(
        text = "Online: ${members?.size ?: 0}",
        style = MaterialTheme.typography.bodyMedium,
        modifier = Modifier.padding(start = 8.dp)
    )
}

Add the PresenceStatusUi component to your main app component:

Kotlin

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

@Composable
fun App(chatClient: ChatClient) {
    val roomName = "my-first-room"
    var room by remember { mutableStateOf<Room?>(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:

ably rooms presence enter my-first-room --client-id "my-cli"

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

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

@Composable
fun ReactionBar(room: Room?) {
    val scope = rememberCoroutineScope()
    val availableReactions = listOf("👍", "❤️", "💥", "🚀", "👎", "💔")
    val receivedReactions = remember { mutableStateListOf<Reaction>() }

    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.name,
                                fontSize = 12.sp
                            )
                        }
                    }
                }
            }
        }
    }
}

Add the ReactionBar component to your main app component:

Kotlin

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

@Composable
fun App(chatClient: ChatClient) {
    val roomName = "my-first-room"
    var room by remember { mutableStateOf<Room?>(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:

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

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

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 = "{{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 exploring Ably Chat with Kotlin:

Read more about the concepts covered in this guide:

Explore the Ably CLI further, or check out the Chat Kotlin API references for additional functionality.

Select...