

# IVS 聊天功能客户端消息收发 SDK：Kotlin 协同教程第 1 部分：聊天室
<a name="chat-sdk-kotlin-tutorial-chat-rooms"></a>

这是一个由两部分组成的教程的第 1 部分。通过使用 [Kotlin](https://kotlinlang.org/) 编程语言和[协同例程](https://kotlinlang.org/docs/coroutines-overview.html)构建功能齐全的 Android 应用程序，您将了解使用 Amazon IVS 聊天功能消息收发 SDK 的基本知识。我们把这个应用程序称为 *Chatterbox*。

在开始本模块之前，请花几分钟时间熟悉先决条件、聊天令牌背后的关键概念以及创建聊天室所需的后端服务器。

这些教程是为刚接触 IVS 聊天功能消息收发 SDK 但经验丰富的 Android 开发人员创建的。您需要熟悉 Kotlin 编程语言并在 Android 平台上创建 UI。

本教程的第 1 部分分为几个章节：

1. [设置本地身份验证/授权服务器](#chat-kotlin-rooms-auth-server)

1. [创建 Chatterbox 项目](#chat-kotlin-rooms-chatterbox)

1. [连接到聊天室和观察连接更新](#chat-kotlin-rooms-connect)

1. [构建令牌提供程序](#chat-kotlin-rooms-token-provider)

1. [后续步骤](#chat-kotlin-rooms-next-steps)

要学习完整的 SDK 文档，请从[亚马逊 IVS 聊天功能客户端消息收发 SDK](chat-sdk.md)（在《Amazon IVS 聊天功能用户指南**》中）和 [Chat Client Messaging: SDK for Android Reference](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/latest/)（在 GitHub 上）开始。

## 先决条件
<a name="chat-kotlin-rooms-prerequisites"></a>
+ 熟悉 Kotlin 并在 Android 平台上创建应用程序。如果您对创建 Android 应用程序不熟悉，请学习面向 Android 开发者的[构建您的第一个应用程序](https://developer.android.com/codelabs/basic-android-kotlin-compose-first-app#0)指南中的基础知识。
+ 阅读并理解 [Amazon IVS Chat 入门](getting-started-chat.md)。
+ 使用现有的 IAM policy 中定义的 `CreateChatToken` 和 `CreateRoom` 功能创建 AWS IAM 用户。（请参见 [Amazon IVS Chat 入门](getting-started-chat.md)。）
+ 确保该用户的私有密钥/访问密钥存储在 Amazon 凭证文件中。有关说明，请参阅 [Amazon CLI 用户指南](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html)（尤其是[配置和凭证文件设置](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html)部分）。
+ 创建聊天室并保存其 ARN。请参阅[Amazon IVS Chat 入门](getting-started-chat.md)。（如果不保存 ARN，您可以稍后使用控制台或 Chat API 查找 ARN。）

## 设置本地身份验证/授权服务器
<a name="chat-kotlin-rooms-auth-server"></a>

您的后端服务器负责创建聊天室和生成 IVS 聊天功能 Android SDK 所需的聊天令牌，以便在您的客户端连接聊天室时进行身份验证和授权。

请参阅*《Amazon IVS Chat 入门》*中的[创建聊天令牌](getting-started-chat-auth.md)。如其中的流程图所示，您的服务器端代码负责创建聊天令牌。这意味着您的应用程序必须通过向服务器端应用程序请求聊天令牌，来提供自己生成聊天令牌的方法。

我们使用 [Ktor](https://ktor.io/) 框架创建一个实时本地服务器，该服务器使用您的本地 Amazon 环境管理聊天令牌的创建。

此时，我们希望您已正确设置 AWS 凭证。有关分步说明，请参阅 [Set up AWS temporary credentials and AWS Region for development](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/setup-credentials.html)。

创建一个新目录并命名为 `chatterbox`，在其中再创建一个，然后命名为 `auth-server`*。*

我们的服务器文件夹将具有如下结构：

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

*注意：您可以直接将此处的代码复制/粘贴到引用文件中。*

接下来，我们添加所有必要的依赖项和插件，以便身份验证服务器正常工作：

**Kotlin 脚本：**

```
// ./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("software.amazon.awssdk:ivschat:2.18.1")
   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")
}
```

现在我们需要为身份验证服务器设置日志记录功能。（有关更多信息，请参阅[配置记录器](https://ktor.io/docs/logging.html#configure-logger)。）

**XML：**

```
// ./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>
```

[Ktor](https://ktor.io/docs/welcome.html) 服务器需要自动从 `resources` 目录的 `application.*` 文件中加载的配置设置，所以我们也添加了这些设置。（有关更多信息，请参阅[文件中的配置](https://ktor.io/docs/configurations.html#configuration-file)。）

**HOCON**：

```
// ./auth-server/src/main/resources/application.conf

ktor {
   deployment {
      port = 3000
   }
   application {
      modules = [ com.chatterbox.authserver.ApplicationKt.main ]
   }
}
```

最后，让我们实施我们的服务器：

**Kotlin：**

```
// ./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 software.amazon.awssdk.services.ivschat.IvschatClient
import software.amazon.awssdk.services.ivschat.model.CreateChatTokenRequest

@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()
            )
         )
      }
   }
}
```

## 创建 Chatterbox 项目
<a name="chat-kotlin-rooms-chatterbox"></a>

要创建 Android 项目，请安装并打开 [Android Studio](https://developer.android.com/studio)。

按照 Android 官方在[创建项目指南](https://developer.android.com/studio/projects/create-project)中列出的步骤进行操作。
+ 在[选择您的项目](https://developer.android.com/studio/projects/create-project)中，为我们的 Chatterbox 应用程序选择**空活动**项目模板。
+ 在[配置您的项目](https://developer.android.com/studio/projects/create-project#configure)中，为配置字段选择以下值：
  + **名称**：My App
  + **程序包名称**：com.chatterbox.myapp
  + **保存位置**：指向在上一步中创建的 `chatterbox` 目录
  + **语言**：Kotlin
  + **最低 API 级别**：API 21：Android 5.0（Lollipop）

正确指定所有配置参数后，我们在 `chatterbox` 文件夹内的文件结构应如下所示：

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

现在我们有一个正在运行的 Android 项目，可以将 [com.amazonaws:ivs-chat-messaging](https://mvnrepository.com/artifact/com.amazonaws/ivs-chat-messaging) 和 [org.jetbrains.kotlinx:kotlinx-coroutines-core](https://github.com/Kotlin/kotlinx.coroutines) 添加到 `build.gradle` 依赖项中。（有关 [Gradle](https://gradle.org/) 构建工具包的更多信息，请参阅[配置您的构建](https://developer.android.com/build)。） 

**注意**：在每个代码段的顶部，都有一个文件路径，您应该在其中对项目进行更改。路径相对于项目的根目录。

**Kotlin：**

```
// ./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'

// ...
}
```

添加新依赖项后，在 Android Studio 中运行**将项目与 Gradle 文件同步**，将项目与新依赖项同步。（有关更多信息，请参阅[添加构建依赖项](https://developer.android.com/build/dependencies)。）

为了方便从项目根目录运行我们的身份验证服务器（在上一部分中创建），我们将其作为新模块包含在 `settings.gradle` 中。（有关更多信息，请参阅[使用 Gradle 构建软件组件](https://docs.gradle.org/current/userguide/multi_project_builds.html)。）

**Kotlin 脚本：**

```
// ./settings.gradle

// ...

rootProject.name = "My App"
include ':app'
include ':auth-server'
```

从现在起，由于 `auth-server` 已包含在 Android 项目中，您可以从项目的根目录使用以下命令运行身份验证服务器：

**Shell：**

```
./gradlew :auth-server:run         
```

## 连接到聊天室和观察连接更新
<a name="chat-kotlin-rooms-connect"></a>

为了打开聊天室连接，我们使用了 [onCreate() 活动生命周期回调](https://developer.android.com/guide/components/activities/activity-lifecycle)，该回调在首次创建活动时触发。[ChatRoom 构造函数](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/-amazon%20-i-v-s%20-chat%20-messaging%20-s-d-k%20for%20-android/com.amazonaws.ivs.chat.messaging/-chat-room/index.html)要求我们提供 `region` 和 `tokenProvider` 以实例化聊天室连接。

**注意：**以下代码段中的 `fetchChatToken` 功能将在[下一部分](#chat-kotlin-rooms-token-provider)中实现。

**Kotlin：**

```
// ./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)
   }

// ...
}
```

显示聊天室连接的变化并对其做出反应是制作 `chatterbox` 等聊天应用程序的重要组成部分。在我们开始与聊天室互动之前，必须订阅聊天室连接状态事件以获取更新。

在协同例程的聊天功能 SDK 中，[https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/-amazon%20-i-v-s%20-chat%20-messaging%20-s-d-k%20for%20-android/com.amazonaws.ivs.chat.messaging/-chat-room/index.html](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/-amazon%20-i-v-s%20-chat%20-messaging%20-s-d-k%20for%20-android/com.amazonaws.ivs.chat.messaging/-chat-room/index.html) 希望我们在[流](https://kotlinlang.org/docs/flow.html)中处理聊天室生命周期事件。目前，函数在调用时只会记录确认消息：

**Kotlin：**

```
// ./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")
                }
            }
        }
    }
}
```

此后，我们需要提供读取聊天室连接状态的功能。我们会将其保留在 `MainActivity.kt` [属性](https://kotlinlang.org/docs/properties.html)中并将其初始化为聊天室连接默认断开状态（参阅 [IVS 聊天功能 Android SDK 参考](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/latest/)中的 `ChatRoom` `state`）。为了保持最新的本地状态，我们需要实现一个状态更新器函数，我们称之为 `updateConnectionState`：

**Kotlin：**

```
// ./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")
          }
      }
}
```

接下来，我们将状态更新器函数与 [ChatRoom.listener](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/-amazon%20-i-v-s%20-chat%20-messaging%20-s-d-k%20for%20-android/com.amazonaws.ivs.chat.messaging/-chat-room/listener.html) 属性集成：

**Kotlin：**

```
// ./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)

                }
            }

      // ...

      }
   }
}
```

现在我们能够保存、侦听和响应 [ChatRoom](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/-amazon%20-i-v-s%20-chat%20-messaging%20-s-d-k%20for%20-android/com.amazonaws.ivs.chat.messaging/-chat-room/index.html) 状态更新，接下来可以初始化连接了：

**Kotlin：**

```
// ./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)
      }
   }

   // ...
}
```

## 构建令牌提供程序
<a name="chat-kotlin-rooms-token-provider"></a>

现在可以创建一个函数，负责在我们的应用程序中创建和管理聊天令牌。在此示例中，我们使用 [Android 版 Retrofit HTTP 客户端](https://square.github.io/retrofit/)。

在发送任何网络流量之前，我们必须设置适用于 Android 的网络安全配置。（有关更多信息，请参阅[网络安全配置](https://developer.android.com/privacy-and-security/security-config)。） 我们首先向[应用程序清单](https://developer.android.com/guide/topics/manifest/manifest-intro)文件添加网络权限。请注意，添加的 `user-permission` 标签和 `networkSecurityConfig` 属性将指向我们的新网络安全配置。*在下面的代码中，将 *`<version>`* 替换为聊天功能 Android SDK 的当前版本号（例如 1.1.0）。*

**XML：**

```
// ./app/src/main/AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/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")
}
```

声明您的本地 IP 地址（例如 `10.0.2.2` 和 `localhost`）为可信域，开始与我们的后端交换消息：

**XML：**

```
// ./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">10.0.2.2</domain>
        <domain includeSubdomains="true">localhost</domain>
    </domain-config>
</network-security-config>
```

接下来，我们需要添加一个新的依赖项以及用于解析 HTTP 响应的 [Gson 转换器](https://github.com/square/retrofit/tree/trunk/retrofit-converters/gson)。*在下面的代码中，将 *`<version>`* 替换为聊天功能 Android SDK 的当前版本号（例如 1.1.0）。*

**Kotlin 脚本：**

```
// ./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")
}
```

要检索聊天令牌，我们需要从 `chatterbox` 应用程序发出 POST HTTP 请求。我们在 Retrofit 接口中定义请求，以便实现。（请参阅 [Retrofit 文档](https://square.github.io/retrofit/)，也请熟悉 [CreateChatToken](https://docs.aws.amazon.com/ivs/latest/ChatAPIReference/API_CreateChatToken.html#API_CreateChatToken_RequestBody) 操作规范。）

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/network/ApiService.kt

package com.chatterbox.myapp.network

import com.amazonaws.ivs.chat.messaging.ChatToken
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 com.chatterbox.myapp.network

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitFactory {
   private const val BASE_URL = "http://10.0.2.2:3000"

   fun makeRetrofitService(): ApiService {
       return Retrofit.Builder()
           .baseUrl(BASE_URL)
           .addConverterFactory(GsonConverterFactory.create())
           .build().create(ApiService::class.java)
   }
}
```

现在，网络设置完成后，可以添加一个函数，负责创建和管理我们的聊天令牌。我们将其添加到 `MainActivity.kt`，它是在项目[生成](#chat-kotlin-rooms-chatterbox)时自动创建的：

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import com.amazonaws.ivs.chat.messaging.*
import com.amazonaws.ivs.chat.messaging.coroutines.*
import com.chatterbox.myapp.network.CreateTokenParams
import com.chatterbox.myapp.network.RetrofitFactory
import retrofit2.Call
import java.io.IOException
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)
         }
      })
   }
}
```

## 后续步骤
<a name="chat-kotlin-rooms-next-steps"></a>

现在，您已经建立聊天室连接，请继续阅读本 Kotlin 协同教程的第 2 部分：[消息和事件](chat-sdk-kotlin-tutorial-messages-events.md)。