Amazon IVS Chat Client Messaging SDK: Kotlin Coroutines Tutorial Part 1: Chat Rooms - Amazon Interactive Video Service

Amazon IVS Chat Client Messaging SDK: Kotlin Coroutines Tutorial Part 1: Chat Rooms

This is the first of a two-part tutorial. You will learn the essentials of working with the Amazon IVS Chat Messaging SDK by building a fully functional Android app using the Kotlin programming language and coroutines. We call the app Chatterbox.

Before you start the module, take a few minutes to familiarize yourself with the prerequisites, key concepts behind chat tokens, and the backend server necessary for creating chat rooms.

These tutorials are created for experienced Android developers who are new to the IVS Chat Messaging SDK. You will need to be comfortable with the Kotlin programming language and creating UIs on the Android platform.

This first part of the tutorial is broken up into several sections:

For full SDK documentation, start with Amazon IVS Chat Client Messaging SDK (here in the Amazon IVS User Guide) and the Chat Client Messaging: SDK for Android Reference (on GitHub).


Set Up a Local Authentication/Authorization Server

Your backend server will be responsible for both creating chat rooms and generating the chat tokens needed for the IVS Chat Android SDK to authenticate and authorize your clients for your chat rooms.

See Create a Chat Token in Getting Started with Amazon IVS Chat. As shown in the flowchart there, your server-side code is responsible for creating a chat token. This means your app must provide its own means of generating a chat token by requesting one from your server-side application.

We use the Ktor framework to create a live local server that manages the creation of chat tokens using your local AWS environment.

At this point, we expect you have your AWS credentials set up correctly. For step-by-step instructions, see Set up AWS Credentials and Region for Development.

Create a new directory and call it chatterbox and inside it, another one, called auth-server.

Our server folder will have the following structure:

- auth-server - src - main - kotlin - com - chatterbox - authserver - Application.kt - resources - application.conf - logback.xml - build.gradle.kts

Note: you can directly copy/paste the code here into the referenced files.

Next, we add all the necessary dependencies and plugins for our auth server to work:

Kotlin Script:

// ./auth-server/build.gradle.kts plugins { application kotlin("jvm") kotlin("plugin.serialization").version("1.7.10") } application { mainClass.set("io.ktor.server.netty.EngineMain") } dependencies { implementation("") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.20") implementation("io.ktor:ktor-server-core:2.1.3") implementation("io.ktor:ktor-server-netty:2.1.3") implementation("io.ktor:ktor-server-content-negotiation:2.1.3") implementation("io.ktor:ktor-serialization-kotlinx-json:2.1.3") implementation("ch.qos.logback:logback-classic:1.4.4") }

Now we need to set up logging functionality for the auth server. (For more information, see Configure logger.)


// ./auth-server/src/main/resources/logback.xml <configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="trace"> <appender-ref ref="STDOUT"/> </root> <logger name="org.eclipse.jetty" level="INFO"/> <logger name="io.netty" level="INFO"/> </configuration>

The Ktor server requires configuration settings, which it automatically loads from the application.* file in the resources directory, so we add that as well. (For more information, see Configuration file.)


// ./auth-server/src/main/resources/application.conf ktor { deployment { port = 3000 } application { modules = [ com.chatterbox.authserver.ApplicationKt.main ] } }

Finally, let's implement our server:


// ./auth-server/src/main/kotlin/com/chatterbox/authserver/Application.kt package com.chatterbox.authserver import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* import io.ktor.server.plugins.contentnegotiation.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import import @Serializable data class ChatTokenParams(var userId: String, var roomIdentifier: String) @Serializable data class ChatToken( val token: String, val sessionExpirationTime: String, val tokenExpirationTime: String, ) fun Application.main() { install(ContentNegotiation) { json(Json) } routing { post("/create_chat_token") { val callParameters = call.receive<ChatTokenParams>() val request = CreateChatTokenRequest.builder().roomIdentifier(callParameters.roomIdentifier) .userId(callParameters.userId).build() val token = IvschatClient.create() .createChatToken(request) call.respond( ChatToken( token.token(), token.sessionExpirationTime().toString(), token.tokenExpirationTime().toString() ) ) } } }

Create a Chatterbox Project

To create an Android project, install and open Android Studio.

Follow the steps listed in the official Android Create a Project guide.

  • In Configure your project, choose the following values for configuration fields:

    • Name: My App

    • Package name: com.chatterbox.myapp

    • Save location: point at the created chatterbox directory created in the previous step

    • Language: Kotlin

    • Minimum API level: API 21: Android 5.0 (Lollipop)

After specifying all the configuration parameters correctly, our file structure inside the chatterbox folder should look like the following:

- app - build.gradle ... - gradle - .gitignore - build.gradle - - gradlew - gradlew.bat - - settings.gradle - auth-server - src - main - kotlin - com - chatterbox - authserver - Application.kt - resources - application.conf - logback.xml - build.gradle.kts

Now that we have a working Android project, we can add com.amazonaws:ivs-chat-messaging and org.jetbrains.kotlinx:kotlinx-coroutines-core to our build.gradle dependencies. (For more information on the Gradle build toolkit, see Configure your build.)

Note: At the top of every code snippet, there is a path to the file where you should be making changes in your project. The path is relative to the project’s root.


// ./app/build.gradle plugins { // ... } android { // ... } dependencies { implementation 'com.amazonaws:ivs-chat-messaging:1.1.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4' // ... }

After the new dependency is added, run Sync Project with Gradle Files in Android Studio to synchronize the project with the new dependency. (For more information, see Add build dependencies.)

To conveniently run our auth server (created in the previous section) from the project root, we include it as a new module in settings.gradle. (For more information, see Structuring and Building a Software Component with Gradle.)

Kotlin Script:

// ./settings.gradle // ... = "My App" include ':app' include ':auth-server'

From now on, as auth-server is included in the Android project, you can run the auth server with the following command from the project's root:


./gradlew :auth-server:run

Connect to a Chat Room and Observe Connection Updates

To open a chat-room connection, we use the onCreate() activity lifecycle callback, which fires when the activity is first created. The ChatRoom constructor requires us to provide region and tokenProvider to instantiate a room connection.

Note: The fetchChatToken function in the snippet below will be implemented in the next section.


// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt package com.chatterbox.myapp // ... // AWS region of the room that was created in Getting Started with Amazon IVS Chat const val REGION = "us-west-2" class MainActivity : AppCompatActivity() { private var room: ChatRoom? = null // ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // Create room instance room = ChatRoom(REGION, ::fetchChatToken) } // ... }

Displaying and reacting to changes in a chat room's connection are essential parts of making a chat app like chatterbox. Before we can start interacting with the room, we must subscribe to chat-room connection-state events, to get updates.

In the Chat SDK for coroutine, ChatRoom expects us to handle room lifecycle events in Flow. For now, the functions will log only confirmation messages, when invoked:


// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt package com.chatterbox.myapp // ... const val TAG = "Chatterbox-MyApp" class MainActivity : AppCompatActivity() { // ... override fun onCreate(savedInstanceState: Bundle?) { // ... // Create room instance room = ChatRoom(REGION, ::fetchChatToken).apply { lifecycleScope.launch { stateChanges().collect { state -> Log.d(TAG, "state change to $state") } } lifecycleScope.launch { receivedMessages().collect { message -> Log.d(TAG, "messageReceived $message") } } lifecycleScope.launch { receivedEvents().collect { event -> Log.d(TAG, "eventReceived $event") } } lifecycleScope.launch { deletedMessages().collect { event -> Log.d(TAG, "messageDeleted $event") } } lifecycleScope.launch { disconnectedUsers().collect { event -> Log.d(TAG, "userDisconnected $event") } } } } }

After this, we need to provide the ability to read the room-connection state. We will keep it in the MainActivity.kt property and initialize it to the default DISCONNECTED state for rooms (see ChatRoom state in the IVS Chat Android SDK Reference). To be able to keep local state up to date, we need to implement a state-updater function; let’s call it updateConnectionState:


// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt package com.chatterbox.myapp // ... class MainActivity : AppCompatActivity() { private var connectionState = ChatRoom.State.DISCONNECTED // ... private fun updateConnectionState(state: ChatRoom.State) { connectionState = state when (state) { ChatRoom.State.CONNECTED -> { Log.d(TAG, "room connected") } ChatRoom.State.DISCONNECTED -> { Log.d(TAG, "room disconnected") } ChatRoom.State.CONNECTING -> { Log.d(TAG, "room connecting") } } }

Next, we integrate our state-updater function with the ChatRoom.listener property:


// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt package com.chatterbox.myapp // ... class MainActivity : AppCompatActivity() { // ... override fun onCreate(savedInstanceState: Bundle?) { // ... // Create room instance room = ChatRoom(REGION, ::fetchChatToken).apply { lifecycleScope.launch { stateChanges().collect { state -> Log.d(TAG, "state change to $state") updateConnectionState(state) } } // ... } } }

Now that we have the ability to save, listen, and react to ChatRoom state updates, it's time to initialize a connection:


// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt package com.chatterbox.myapp // ... class MainActivity : AppCompatActivity() { // ... private fun connect() { try { room?.connect() } catch (ex: Exception) { Log.e(TAG, "Error while calling connect()", ex) } } // ... }

Build a Token Provider

It's time to create a function responsible for creating and managing chat tokens in our application. In this example we use the Retrofit HTTP client for Android.

Before we can send any network traffic, we must set up a network-security configuration for Android. (For more information, see Network security configuration.) We begin with adding network permissions to the App Manifest file. Note the added user-permission tag and networkSecurityConfig attribute, which will point to our new network-security configuration. In the code below, replace <version> with the current version number of the Chat Android SDK (e.g., 1.1.0).


// ./app/src/main/AndroidManifest.xml <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="" xmlns:tools="" package="com.chatterbox.myapp"> <uses-permission android:name="android.permission.INTERNET" /> <application android:allowBackup="true" android:fullBackupContent="@xml/backup_rules" android:label="@string/app_name" android:networkSecurityConfig="@xml/network_security_config" // ... // ./app/build.gradle dependencies { implementation("com.amazonaws:ivs-chat-messaging:<version>") // ... implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:converter-gson:2.9.0") }

Declare your local IP address, e.g. and localhost domains as trusted, to start exchanging messages with our backend:


// ./app/src/main/res/xml/network_security_config.xml <?xml version="1.0" encoding="utf-8"?> <network-security-config> <domain-config cleartextTrafficPermitted="true"> <domain includeSubdomains="true"></domain> <domain includeSubdomains="true">localhost</domain> </domain-config> </network-security-config>

Next, we need to add a new dependency, along with Gson converter addition for parsing HTTP responses. In the code below, replace <version> with the current version number of the Chat Android SDK (e.g., 1.1.0).

Kotlin Script:

// ./app/build.gradle dependencies { implementation("com.amazonaws:ivs-chat-messaging:<version>") // ... implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:converter-gson:2.9.0") }

To retrieve a chat token, we need to make a POST HTTP request from our chatterbox app. We define the request in an interface for Retrofit to implement. (See Retrofit documentation. Also familiarize yourself with the CreateChatToken endpoint specification.)


// ./app/src/main/java/com/chatterbox/myapp/network/ApiService.kt package import import retrofit2.Call import retrofit2.http.Body import retrofit2.http.POST data class CreateTokenParams(var userId: String, var roomIdentifier: String) interface ApiService { @POST("create_chat_token") fun createChatToken(@Body params: CreateTokenParams): Call<ChatToken> } // ./app/src/main/java/com/chatterbox/myapp/network/RetrofitFactory.kt package import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory object RetrofitFactory { private const val BASE_URL = "" fun makeRetrofitService(): ApiService { return Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .build().create( } }

Now, with networking set up, it's time to add a function responsible for creating and managing our chat token. We add it to MainActivity.kt, which was automatically created when the project was generated:


// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt package com.chatterbox.myapp import import android.os.Bundle import android.util.Log import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.launch import* import* import import import retrofit2.Call import import retrofit2.Callback import retrofit2.Response // custom tag for logging purposes const val TAG = "Chatterbox-MyApp" // any ID to be associated with auth token const val USER_ID = "test user id" // ID of the room the app wants to access. Must be an ARN. See Amazon Resource Names(ARNs) const val ROOM_ID = "arn:aws:..." // AWS region of the room that was created in Getting Started with Amazon IVS Chat const val REGION = "us-west-2" class MainActivity : AppCompatActivity() { private val service = RetrofitFactory.makeRetrofitService() private var userId: String = USER_ID // ... private fun fetchChatToken(callback: ChatTokenCallback) { val params = CreateTokenParams(userId, ROOM_ID) service.createChatToken(params).enqueue(object : Callback<ChatToken> { override fun onResponse(call: Call<ChatToken>, response: Response<ChatToken>) { val token = response.body() if (token == null) { Log.e(TAG, "Received empty token response") callback.onFailure(IOException("Empty token response")) return } Log.d(TAG, "Received token response $token") callback.onSuccess(token) } override fun onFailure(call: Call<ChatToken>, throwable: Throwable) { Log.e(TAG, "Failed to fetch token", throwable) callback.onFailure(throwable) } }) } }

Next Steps

Now that you’ve established a chat-room connection, proceed to Part 2 of this Kotlin Coroutines tutorial, Messages and Events.