Client-Messaging-SDK für IVS Chat: Tutorial Teil 2: Nachrichten und Ereignisse - Amazon IVS

Client-Messaging-SDK für IVS Chat: Tutorial Teil 2: Nachrichten und Ereignisse

Der vorliegende zweite (und letzte) Teil des Tutorials ist in mehrere Abschnitte unterteilt:

Umfassende Informationen zum SDK finden Sie im Client-Messaging-SDK für Amazon IVS Chat (im vorliegenden Benutzerhandbuch zu Amazon IVS Chat) und unter Chat Client Messaging: SDK für Android-Referenz auf GitHub.

Voraussetzung

Absolvieren Sie unbedingt Teil 1 dieses Tutorials: Chatrooms.

Erstellen Sie eine Benutzeroberfläche für das Senden von Nachrichten.

Nachdem wir die Chatraum-Verbindung erfolgreich initialisiert haben, ist es an der Zeit, unsere erste Nachricht zu senden. Für dieses Feature wird eine Benutzeroberfläche benötigt. Wir werden folgendes hinzufügen:

  • connect/disconnect-Schaltfläche

  • Nachrichteneingabe mit send-Schaltfläche

  • Dynamische Nachrichtenliste. Um dies zu erstellen, verwenden wir Android Jetpack RecyclerView.

Benutzeroberfläche Hauptlayout

Siehe Android Jetpack-Layouts in der Android-Entwicklerdokumentation.

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>

Abstrahierte Benutzeroberflächen-Textzelle zur konsistenten Anzeige von Text

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>

Benutzeroberfläche linke Chat-Nachricht

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>

Benutzeroberfläche rechte Chat-Nachricht

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>

Benutzeroberfläche zusätzliche Farbwerte

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>

View Binding anwenden

Wir nutzen das Android View-Binding-Feature, um Bindungsklassen für unser XML-Layout referenzieren zu können. Um das Feature zu aktivieren, setzen Sie die viewBinding-Build-Option auf true in ./app/build.gradle:

Kotlin-Skript:

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

Jetzt ist es an der Zeit, die Benutzeroberfläche mit unserem Kotlin-Code zu verbinden:

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) } }

Wir fügen auch Methoden hinzu, um Nachrichten zu löschen und Benutzer vom Chat zu trennen. Diese können über das Kontextmenü von Chat-Nachrichten aufgerufen werden:

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

Chat-Nachrichtenanfragen verwalten

Wir benötigen eine Möglichkeit, unsere Chat-Nachrichtenanfragen in all ihren möglichen Zuständen zu verwalten:

  • Ausstehend – Eine Nachricht wurde an einen Chatraum gesendet, wurde aber noch nicht bestätigt oder abgelehnt.

  • Bestätigt – Der Chatraum hat eine Nachricht an alle Benutzer (einschließlich uns) gesendet.

  • Abgelehnt – Eine Nachricht wurde vom Chatraum mit einem Fehlerobjekt abgelehnt.

Wir werden noch nicht aufgelöste Chat-Anfragen und Chat-Nachrichten in einer Liste speichern. Die Liste benötigt eine separate Klasse, die wir ChatEntries.kt nennen:

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

Um unsere Liste mit der Benutzeroberfläche zu verbinden, verwenden wir einen Adapter. Weitere Informationen finden Sie unter Binden an Daten mit AdapterView und Generierte Bindungsklassen.

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 }

Letzte Schritte

Es ist Zeit, unseren neuen Adapter zu verbinden, der die ChatEntries-Klasse an MainActivity bindet:

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 } } }

Da wir bereits eine Klasse haben, die dafür verantwortlich ist, unsere Chat-Anfragen zu verfolgen (ChatEntries), sind wir bereit, Code für die Manipulation von entries zu roomListener zu implementieren. Wir werden entries und connectionState entsprechend des Ereignisses, auf das wir reagieren, aktualisieren:

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

Jetzt sollten Sie Ihre Anwendung ausführen können! (Siehe Erstellen und starten Ihrer App.) Denken Sie daran, dass Ihr Backend-Server in Betrieb sein muss, wenn Sie die App verwenden. Sie können ihn mit diesem Befehl vom Terminal im Stammverzeichnis unseres Projekts aus starten: ./gradlew :auth-server:run oder indem Sie die auth-server:run-Gradle-Aufgabe direkt von Android Studio aus ausführen.