Amazon IVS 챗 클라이언트 메시징 SDK: Android 자습서 2부: 메시지 및 이벤트 - Amazon IVS

Amazon IVS 챗 클라이언트 메시징 SDK: Android 자습서 2부: 메시지 및 이벤트

이 자습서의 두 번째(마지막) 부분은 여러 섹션으로 나뉩니다.

전체 SDK 설명서를 보려면 우선 Amazon IVS 챗 클라이언트 메시징 SDK(Amazon IVS 챗 사용 설명서에서 참조) 및 Chat Client Messaging: SDK for Android Reference(GitHub)를 참조하세요.

전제 조건

이 자습서의 1부인 채팅룸을 완료해야 합니다.

메시지 전송을 위한 UI 만들기

채팅룸 연결을 성공적으로 초기화했으므로 이제 첫 번째 메시지를 보낼 차례입니다. 이 기능에는 UI가 필요합니다. 다음을 추가합니다.

  • connect/disconnect 버튼

  • send 버튼으로 메시지 입력

  • 동적 메시지 목록. 이를 빌드하기 위해 Android Jetpack RecyclerView를 사용합니다.

UI 기본 레이아웃

Android 개발자 문서에서 Android 젯팩 레이아웃을 참조하세요.

XML:

// ./app/src/main/res/layout/activity_main.xml <?xml version="1.0" encoding="utf-8"?> <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/connect_view" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="vertical"> <androidx.cardview.widget.CardView android:id="@+id/connect_button" android:layout_width="match_parent" android:layout_height="48dp" android:layout_gravity="" android:layout_marginStart="16dp" android:layout_marginTop="4dp" android:layout_marginEnd="16dp" android:clickable="true" android:elevation="16dp" android:focusable="true" android:foreground="?android:attr/selectableItemBackground" app:cardBackgroundColor="@color/purple_500" app:cardCornerRadius="10dp"> <TextView android:id="@+id/connect_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentEnd="true" android:layout_gravity="center" android:layout_weight="1" android:paddingHorizontal="12dp" android:text="Connect" android:textColor="@color/white" android:textSize="16sp"/> <ProgressBar android:id="@+id/activity_indicator" android:layout_width="20dp" android:layout_height="20dp" android:layout_gravity="center" android:layout_marginHorizontal="20dp" android:indeterminateOnly="true" android:indeterminateTint="@color/white" android:indeterminateTintMode="src_atop" android:keepScreenOn="true" android:visibility="gone"/> </androidx.cardview.widget.CardView> </LinearLayout> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/chat_view" android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" android:visibility="visible" tools:context=".MainActivity"> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" app:layout_constraintBottom_toTopOf="@+id/layout_message_input" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:clipToPadding="false" android:paddingTop="70dp" android:paddingBottom="20dp"/> </RelativeLayout> <RelativeLayout android:id="@+id/layout_message_input" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@android:color/white" android:clipToPadding="false" android:drawableTop="@android:color/black" android:elevation="18dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent"> <EditText android:id="@+id/message_edit_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_marginStart="16dp" android:layout_toStartOf="@+id/send_button" android:background="@android:color/transparent" android:hint="Enter Message" android:inputType="text" android:maxLines="6" tools:ignore="Autofill"/> <Button android:id="@+id/send_button" android:layout_width="84dp" android:layout_height="48dp" android:layout_alignParentEnd="true" android:background="@color/black" android:foreground="?android:attr/selectableItemBackground" android:text="Send" android:textColor="@color/white" android:textSize="12dp"/> </RelativeLayout> </androidx.constraintlayout.widget.ConstraintLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

텍스트를 일관되게 표시하기 위한 UI 추상화 텍스트 셀

XML:

// ./app/src/main/res/layout/common_cell.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/layout_container" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@color/light_gray" android:minWidth="100dp" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"> <TextView android:id="@+id/card_message_me_text_view" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_marginBottom="8dp" android:maxWidth="260dp" android:paddingLeft="12dp" android:paddingTop="8dp" android:paddingRight="12dp" android:text="This is a Message" android:textColor="#ffffff" android:textSize="16sp"/> <TextView android:id="@+id/failed_mark" android:layout_width="40dp" android:layout_height="match_parent" android:paddingRight="5dp" android:src="@drawable/ic_launcher_background" android:text="!" android:textAlignment="viewEnd" android:textColor="@color/white" android:textSize="25dp" android:visibility="gone"/> </LinearLayout> </LinearLayout>

UI 왼쪽 채팅 메시지

XML:

// ./app/src/main/res/layout/card_view_left.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginBottom="12dp" android:orientation="vertical"> <TextView android:id="@+id/username_edit_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="UserName"/> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <androidx.cardview.widget.CardView android:id="@+id/card_message_other" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="left" android:layout_marginBottom="4dp" android:foreground="?android:attr/selectableItemBackground" app:cardBackgroundColor="@color/light_gray_2" app:cardCornerRadius="10dp" app:cardElevation="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent"> <include layout="@layout/common_cell"/> </androidx.cardview.widget.CardView> <TextView android:id="@+id/dateText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="4dp" android:layout_marginBottom="4dp" android:text="10:00" app:layout_constraintBottom_toBottomOf="@+id/card_message_other" app:layout_constraintLeft_toRightOf="@+id/card_message_other"/> </androidx.constraintlayout.widget.ConstraintLayout> </LinearLayout>

UI 오른쪽 채팅 메시지

XML:

// ./app/src/main/res/layout/card_view_right.xml <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginEnd="8dp"> <androidx.cardview.widget.CardView android:id="@+id/card_message_me" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right" android:layout_marginBottom="10dp" android:foreground="?android:attr/selectableItemBackground" app:cardBackgroundColor="@color/purple_500" app:cardCornerRadius="10dp" app:cardElevation="0dp" app:cardPreventCornerOverlap="false" app:cardUseCompatPadding="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent"> <include layout="@layout/common_cell"/> </androidx.cardview.widget.CardView> <TextView android:id="@+id/dateText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="12dp" android:layout_marginBottom="4dp" android:text="10:00" app:layout_constraintBottom_toBottomOf="@+id/card_message_me" app:layout_constraintRight_toLeftOf="@+id/card_message_me"/> </androidx.constraintlayout.widget.ConstraintLayout>

UI 추가 색상 값

XML:

// ./app/src/main/res/values/colors.xml <?xml version="1.0" encoding="utf-8"?> <resources> <!-- ...--> <color name="dark_gray">#4F4F4F</color> <color name="blue">#186ED3</color> <color name="dark_red">#b30000</color> <color name="light_gray">#B7B7B7</color> <color name="light_gray_2">#eef1f6</color> </resources>

뷰 결합 적용

Android 뷰 결합 기능을 활용하여 XML 레이아웃의 결합 클래스를 참조할 수 있습니다. 이 기능을 사용하려면 ./app/build.gradleviewBinding 빌드 옵션을 true로 설정합니다.

Kotlin 스크립트:

// ./app/build.gradle android { // ... buildFeatures { viewBinding = true } // ... }

이제 UI를 Kotlin 코드와 연결할 차례입니다.

Kotlin:

// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt package com.chatterbox.myapp // ... const val TAG = "Chatterbox-MyApp" class MainActivity : AppCompatActivity() { // ... private fun sendMessage(request: SendMessageRequest) { try { room?.sendMessage( request, object : SendMessageCallback { override fun onRejected(request: SendMessageRequest, error: ChatError) { runOnUiThread { entries.addFailedRequest(request) scrollToBottom() Log.e(TAG, "Message rejected: ${error.errorMessage}") } } } ) entries.addPendingRequest(request) binding.messageEditText.text.clear() scrollToBottom() } catch (error: Exception) { Log.e(TAG, error.message ?: "Unknown error occurred") } } private fun scrollToBottom() { binding.recyclerView.smoothScrollToPosition(entries.size - 1) } private fun sendButtonClick(view: View) { val content = binding.messageEditText.text.toString() if (content.trim().isEmpty()) { return } val request = SendMessageRequest(content) sendMessage(request) } }

또한 채팅 메시지 컨텍스트 메뉴를 사용하여 호출할 수 있는 메시지를 삭제하고 채팅에서 사용자의 연결을 끊는 메서드를 추가합니다.

Kotlin:

// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt package com.chatterbox.myapp // ... class MainActivity : AppCompatActivity() { // ... private fun deleteMessage(request: DeleteMessageRequest) { room?.deleteMessage( request, object : DeleteMessageCallback { override fun onRejected(request: DeleteMessageRequest, error: ChatError) { runOnUiThread { Log.d(TAG, "Delete message rejected: ${error.errorMessage}") } } } ) } private fun disconnectUser(request: DisconnectUserRequest) { room?.disconnectUser( request, object : DisconnectUserCallback { override fun onRejected(request: DisconnectUserRequest, error: ChatError) { runOnUiThread { Log.d(TAG, "Disconnect user rejected: ${error.errorMessage}") } } } ) } }

채팅 메시지 요청 관리

가능한 모든 상태를 통해 채팅 메시지 요청을 관리할 수 있는 방법이 필요합니다.

  • 보류 중(Pending) - 메시지가 채팅룸에 전송되었지만 아직 확인 또는 거부되지 않았습니다.

  • 확인됨(Confirmed) - 우리를 포함한 모든 사용자에게 채팅방에 메시지를 보냈습니다.

  • 거부됨(Rejected) - 채팅룸에서 오류 객체가 포함된 메시지를 거부했습니다.

확인되지 않은 채팅 요청과 채팅 메시지는 목록에 보관됩니다. 이 목록에는 ChatEntries.kt라는 별도의 클래스가 필요합니다.

Kotlin:

// ./app/src/main/java/com/chatterbox/myapp/ChatEntries.kt package com.chatterbox.myapp import com.amazonaws.ivs.chat.messaging.entities.ChatMessage import com.amazonaws.ivs.chat.messaging.requests.SendMessageRequest sealed class ChatEntry() { class Message(val message: ChatMessage) : ChatEntry() class PendingRequest(val request: SendMessageRequest) : ChatEntry() class FailedRequest(val request: SendMessageRequest) : ChatEntry() } class ChatEntries { /* This list is kept in sorted order. ChatMessages are sorted by date, while pending and failed requests are kept in their original insertion point. */ val entries = mutableListOf<ChatEntry>() var adapter: ChatListAdapter? = null val size get() = entries.size /** * Insert pending request at the end. */ fun addPendingRequest(request: SendMessageRequest) { val insertIndex = entries.size entries.add(insertIndex, ChatEntry.PendingRequest(request)) adapter?.notifyItemInserted(insertIndex) } /** * Insert received message at proper place based on sendTime. This can cause removal of pending requests. */ fun addReceivedMessage(message: ChatMessage) { /* Skip if we have already handled that message. */ val existingIndex = entries.indexOfLast { it is ChatEntry.Message && it.message.id == message.id } if (existingIndex != -1) { return } val removeIndex = entries.indexOfLast { it is ChatEntry.PendingRequest && it.request.requestId == message.requestId } if (removeIndex != -1) { entries.removeAt(removeIndex) } val insertIndexRaw = entries.indexOfFirst { it is ChatEntry.Message && it.message.sendTime > message.sendTime } val insertIndex = if (insertIndexRaw == -1) entries.size else insertIndexRaw entries.add(insertIndex, ChatEntry.Message(message)) if (removeIndex == -1) { adapter?.notifyItemInserted(insertIndex) } else if (removeIndex == insertIndex) { adapter?.notifyItemChanged(insertIndex) } else { adapter?.notifyItemRemoved(removeIndex) adapter?.notifyItemInserted(insertIndex) } } fun addFailedRequest(request: SendMessageRequest) { val removeIndex = entries.indexOfLast { it is ChatEntry.PendingRequest && it.request.requestId == request.requestId } if (removeIndex != -1) { entries.removeAt(removeIndex) entries.add(removeIndex, ChatEntry.FailedRequest(request)) adapter?.notifyItemChanged(removeIndex) } else { val insertIndex = entries.size entries.add(insertIndex, ChatEntry.FailedRequest(request)) adapter?.notifyItemInserted(insertIndex) } } fun removeMessage(messageId: String) { val removeIndex = entries.indexOfFirst { it is ChatEntry.Message && it.message.id == messageId } entries.removeAt(removeIndex) adapter?.notifyItemRemoved(removeIndex) } fun removeFailedRequest(requestId: String) { val removeIndex = entries.indexOfFirst { it is ChatEntry.FailedRequest && it.request.requestId == requestId } entries.removeAt(removeIndex) adapter?.notifyItemRemoved(removeIndex) } fun removeAll() { entries.clear() } }

목록을 UI와 연결하기 위해 어댑터를 사용합니다. 자세한 정보는 AdapterView를 사용하여 데이터에 결합생성된 결합 클래스를 참조하세요.

Kotlin:

// ./app/src/main/java/com/chatterbox/myapp/ChatListAdapter.kt package com.chatterbox.myapp import android.content.Context import android.graphics.Color import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.LinearLayout import android.widget.TextView import androidx.core.content.ContextCompat import androidx.core.view.isGone import androidx.recyclerview.widget.RecyclerView import com.amazonaws.ivs.chat.messaging.requests.DisconnectUserRequest import java.text.DateFormat class ChatListAdapter( private val entries: ChatEntries, private val onDisconnectUser: (request: DisconnectUserRequest) -> Unit, ) : RecyclerView.Adapter<ChatListAdapter.ViewHolder>() { var context: Context? = null var userId: String? = null class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val container: LinearLayout = view.findViewById(R.id.layout_container) val textView: TextView = view.findViewById(R.id.card_message_me_text_view) val failedMark: TextView = view.findViewById(R.id.failed_mark) val userNameText: TextView? = view.findViewById(R.id.username_edit_text) val dateText: TextView? = view.findViewById(R.id.dateText) } override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder { if (viewType == 0) { val rightView = LayoutInflater.from(viewGroup.context).inflate(R.layout.card_view_right, viewGroup, false) return ViewHolder(rightView) } val leftView = LayoutInflater.from(viewGroup.context).inflate(R.layout.card_view_left, viewGroup, false) return ViewHolder(leftView) } override fun getItemViewType(position: Int): Int { // Int 0 indicates to my message while Int 1 to other message val chatMessage = entries.entries[position] return if (chatMessage is ChatEntry.Message && chatMessage.message.sender.userId != userId) 1 else 0 } override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { return when (val entry = entries.entries[position]) { is ChatEntry.Message -> { viewHolder.textView.text = entry.message.content val bgColor = if (entry.message.sender.userId == userId) { R.color.purple_500 } else { R.color.light_gray_2 } viewHolder.container.setBackgroundColor(ContextCompat.getColor(context!!, bgColor)) if (entry.message.sender.userId != userId) { viewHolder.textView.setTextColor(Color.parseColor("#000000")) } viewHolder.failedMark.isGone = true viewHolder.itemView.setOnCreateContextMenuListener { menu, _, _ -> menu.add("Kick out").setOnMenuItemClickListener { val request = DisconnectUserRequest(entry.message.sender.userId, "Some reason") onDisconnectUser(request) true } } viewHolder.userNameText?.text = entry.message.sender.userId viewHolder.dateText?.text = DateFormat.getTimeInstance(DateFormat.SHORT).format(entry.message.sendTime) } is ChatEntry.PendingRequest -> { viewHolder.container.setBackgroundColor(ContextCompat.getColor(context!!, R.color.light_gray)) viewHolder.textView.text = entry.request.content viewHolder.failedMark.isGone = true viewHolder.itemView.setOnCreateContextMenuListener(null) viewHolder.dateText?.text = "Sending" } is ChatEntry.FailedRequest -> { viewHolder.textView.text = entry.request.content viewHolder.container.setBackgroundColor(ContextCompat.getColor(context!!, R.color.dark_red)) viewHolder.failedMark.isGone = false viewHolder.dateText?.text = "Failed" } } } override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { super.onAttachedToRecyclerView(recyclerView) context = recyclerView.context } override fun getItemCount() = entries.entries.size }

최종 단계

이제 ChatEntries 클래스를 MainActivity에 결합하여 새 어댑터를 연결할 차례입니다.

Kotlin:

// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt package com.chatterbox.myapp // ... import com.chatterbox.myapp.databinding.ActivityMainBinding import com.chatterbox.myapp.ChatListAdapter import com.chatterbox.myapp.ChatEntries class MainActivity : AppCompatActivity() { // ... private var entries = ChatEntries() private lateinit var adapter: ChatListAdapter private lateinit var binding: ActivityMainBinding /* see https://developer.android.com/topic/libraries/data-binding/generated-binding#create */ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) /* Create room instance. */ room = ChatRoom(REGION, ::fetchChatToken).apply { listener = roomListener } binding.sendButton.setOnClickListener(::sendButtonClick) binding.connectButton.setOnClickListener { connect() } setUpChatView() updateConnectionState(ConnectionState.DISCONNECTED) } private fun setUpChatView() { /* Setup Android Jetpack RecyclerView - see https://developer.android.com/develop/ui/views/layout/recyclerview.*/ adapter = ChatListAdapter(entries, ::disconnectUser) entries.adapter = adapter val recyclerViewLayoutManager = LinearLayoutManager(this@MainActivity, LinearLayoutManager.VERTICAL, false) binding.recyclerView.layoutManager = recyclerViewLayoutManager binding.recyclerView.adapter = adapter binding.sendButton.setOnClickListener(::sendButtonClick) binding.messageEditText.setOnEditorActionListener { _, _, event -> val isEnterDown = (event.action == KeyEvent.ACTION_DOWN) && (event.keyCode == KeyEvent.KEYCODE_ENTER) if (!isEnterDown) { return@setOnEditorActionListener false } sendButtonClick(binding.sendButton) return@setOnEditorActionListener true } } }

채팅 요청을 계속 추적하는 클래스(ChatEntries)가 이미 있으므로 roomListenerentries 조작을 위한 코드를 구현할 준비가 되었습니다. 대응 중인 이벤트에 따라 entriesconnectionState를 업데이트합니다.

Kotlin:

// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt package com.chatterbox.myapp // ... class MainActivity : AppCompatActivity() { //... private fun sendMessage(request: SendMessageRequest) { //... } private fun scrollToBottom() { binding.recyclerView.smoothScrollToPosition(entries.size - 1) } private val roomListener = object : ChatRoomListener { override fun onConnecting(room: ChatRoom) { Log.d(TAG, "[${Thread.currentThread().name}] onConnecting") runOnUiThread { updateConnectionState(ConnectionState.LOADING) } } override fun onConnected(room: ChatRoom) { Log.d(TAG, "[${Thread.currentThread().name}] onConnected") runOnUiThread { updateConnectionState(ConnectionState.CONNECTED) } } override fun onDisconnected(room: ChatRoom, reason: DisconnectReason) { Log.d(TAG, "[${Thread.currentThread().name}] onDisconnected") runOnUiThread { updateConnectionState(ConnectionState.DISCONNECTED) entries.removeAll() } } override fun onMessageReceived(room: ChatRoom, message: ChatMessage) { Log.d(TAG, "[${Thread.currentThread().name}] onMessageReceived $message") runOnUiThread { entries.addReceivedMessage(message) scrollToBottom() } } override fun onEventReceived(room: ChatRoom, event: ChatEvent) { Log.d(TAG, "[${Thread.currentThread().name}] onEventReceived $event") } override fun onMessageDeleted(room: ChatRoom, event: DeleteMessageEvent) { Log.d(TAG, "[${Thread.currentThread().name}] onMessageDeleted $event") } override fun onUserDisconnected(room: ChatRoom, event: DisconnectUserEvent) { Log.d(TAG, "[${Thread.currentThread().name}] onUserDisconnected $event") } } }

이제 애플리케이션을 실행할 수 있을 것입니다! (앱 빌드 및 실행을 참조하세요.) 앱을 사용할 때는 반드시 백엔드 서버가 실행 중이어야 합니다. 터미널에서 ./gradlew :auth-server:run 명령어를 프로젝트 루트에서 사용하거나 Android 스튜디오에서 auth-server:run Gradle 작업을 직접 실행하여 시작할 수 있습니다.