

# IVS 聊天用戶端傳訊 SDK：Kotlin Coroutines 版教學課程第 1 部分：聊天室
<a name="chat-sdk-kotlin-tutorial-chat-rooms"></a>

這是由兩部分組成的教學課程的第一部分。透過使用 [Kotlin](https://kotlinlang.org/) 程式設計語言和 [coroutines](https://kotlinlang.org/docs/coroutines-overview.html) 建置功能完整的 Android 應用程式，您將學習使用 Amazon IVS 聊天功能傳訊 SDK 的基礎知識。我們稱呼該應用程式為 *Chatterbox*。

在開始該模組之前，請花幾分鐘時間熟悉先決條件、聊天權杖背後的重要概念以及建立聊天室所需的後端伺服器。

這些教學課程專為經驗豐富的 Android 開發人員而建立，他們不熟悉 IVS 聊天功能傳訊 SDK。您將需要熟悉 Kotlin 程式設計語言並在 Android 平台上建立 UI。

本教學課程的第一部分分為幾個部分：

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 文件，請先閱讀 [Amazon 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/) (聊天用戶端傳訊：Android 版 SDK 參考) (位於 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 聊天功能入門](getting-started-chat.md)。
+ 使用現有 IAM 政策中定義的 `CreateChatToken` 和 `CreateRoom` 功能建立 AWS IAM 使用者。(請參閱 [Amazon IVS 聊天功能入門](getting-started-chat.md))。
+ 確保將此使用者的私密/存取金鑰儲存在 AWS 憑證檔案中。如需指示，請參閱《[AWS 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 聊天功能入門](getting-started-chat.md)。(如果您未保存該 ARN，稍後可以使用主控台或 Chat API 來查詢。)

## 設定本機身分驗證/授權伺服器
<a name="chat-kotlin-rooms-auth-server"></a>

後端伺服器負責建立聊天室並產生 IVS 聊天功能 Android SDK 需要的聊天權杖，以便對聊天室的用戶端執行身分驗證和授權。

請參閱 *Amazon IVS 聊天功能入門*中的[建立聊天字符](getting-started-chat-auth.md)。如流程圖所示，您的伺服器端程式碼會負責建立聊天權杖。這意味著應用程式必須透過從伺服器端應用程式請求聊天字符，來提供自己產生聊天字符的方法。

我們使用 [Ktor](https://ktor.io/) 架構建立即時本機伺服器，以管理使用本機 AWS 環境建立聊天權杖的作業。

此時，希望您已正確設定 AWS 憑證。如需逐步說明，請參閱[設定適用於開發的 AWS 暫時憑證和 AWS 區域](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
```

*注意：您可以直接將這裡的程式碼複製/貼上到參考的檔案中。*

接下來，我們會新增所有必要的相依性和外掛程式，以使 auth 伺服器正常工作：

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

現在，我們需要為 auth 伺服器設定記錄功能。(如需詳細資訊，請參閱[設定記錄器](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)。)

為了方便地從專案根目錄中執行我們的 auth 伺服器 (在上一節中建立)，我們將其作為新模組包含在 `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 專案中，所以您可以使用下列命令從專案的根目錄中執行 auth 伺服器：

**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` 來執行個體化聊天室連線。

**注意：**將在[下一節](#chat-kotlin-rooms-token-provider)中實作下面程式碼片段中的 `fetchChatToken` 函數。

**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` 聊天應用程式的重要部分。在開始與聊天室互動之前，我們必須訂閱聊天室連線狀態事件，以獲取更新。

在適用於 coroutine 的聊天功能 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)中，並將其初始化為聊天室的預設 DISCONNECTED 狀態 (請參閱《[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)。) 我們首先向 [App Manifest](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>
```

接下來，我們需要新增一個新的相依性，並[新增 Gson 轉換器](https://github.com/square/retrofit/tree/trunk/retrofit-converters/gson)以用於解析 HTTP 回應。*在下面的程式碼中，將 *`<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 Coroutines 教學課程的第 2 部分：[訊息和事件](chat-sdk-kotlin-tutorial-messages-events.md)。