SDK per la messaggistica per client di IVS Chat: Tutorial per le coroutine di Kotlin, parte 2: messaggi ed eventi
Questa seconda e ultima parte del tutorial è suddivisa in diverse sezioni:
Per la documentazione completa dell'SDK, inizia con l'SDK di messaggistica per client di chat Amazon IVS (qui nella Guida per l'utente di Chat Amazon IVS) e la Documentazione di riferimento dell'SDK di messaggistica per client di chat per Android
Prerequisito
Assicurati di aver completato la prima parte di questo tutorial, Chat room.
Creazione di un'interfaccia utente per l'invio di messaggi
Ora che abbiamo inizializzato correttamente la connessione alla chat room, è il momento di inviare il primo messaggio. Per questa funzionalità è necessaria un'interfaccia utente. Aggiungeremo:
-
Pulsante
connect
/disconnect
-
Inserimento di messaggi con il pulsante
send
-
Elenco dei messaggi dinamici. Per realizzarlo, utilizziamo RecyclerView
di Android Jetpack.
Layout principale dell'interfaccia utente
Consulta la pagina Layout
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>
Cella di testo astratta dell'interfaccia utente per visualizzare il testo in modo coerente
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>
Messaggio a sinistra dell'interfaccia utente di chat
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>
Messaggio a destra dell'interfaccia utente
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>
Valori di colore aggiuntivi dell'interfaccia utente
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>
Applicazione dell'associazione di visualizzazione
Sfruttiamo la funzione di Android Visualizzazione associazioneviewBinding
su true
in ./app/build.gradle
:
Script Kotlin:
// ./app/build.gradle android { // ... buildFeatures { viewBinding = true } // ... }
Ora è il momento di connettere l'interfaccia utente con il codice Kotlin:
Kotlin:
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt package com.chatterbox.myapp // ... class MainActivity : AppCompatActivity() { // ... private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) // Create room instance room = ChatRoom(REGION, ::fetchChatToken).apply { // ... } binding.sendButton.setOnClickListener(::sendButtonClick) binding.connectButton.setOnClickListener {connect()} setUpChatView() updateConnectionState(ChatRoom.State.DISCONNECTED) } private fun sendMessage(request: SendMessageRequest) { lifecycleScope.launch { try { binding.messageEditText.text.clear() room?.awaitSendMessage(request) } catch (exception: ChatException) { Log.e(TAG, "Message rejected: ${exception.message}") } catch (exception: Exception) { Log.e(TAG, exception.message ?: "Unknown error occurred") } } } private fun sendButtonClick(view: View) { val content = binding.messageEditText.text.toString() if (content.trim().isEmpty()) { return } val request = SendMessageRequest(content) sendMessage(request) } // ... }
Aggiungiamo anche dei metodi per eliminare i messaggi e disconnettere gli utenti dalla chat, che possono essere richiamati utilizzando il menu contestuale dei messaggi di chat:
Kotlin:
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt package com.chatterbox.myapp // ... class MainActivity : AppCompatActivity() { // ... private fun deleteMessage(request: DeleteMessageRequest) { lifecycleScope.launch { try { room?.awaitDeleteMessage(request) } catch (exception: ChatException) { Log.e(TAG, "Delete message rejected: ${exception.message}") } catch (exception: Exception) { Log.e(TAG, exception.message ?: "Unknown error occurred") } } } private fun disconnectUser(request: DisconnectUserRequest) { lifecycleScope.launch { try { room?.awaitDisconnectUser(request) } catch (exception: ChatException) { Log.e(TAG, "Disconnect user rejected: ${exception.message}") } catch (exception: Exception) { Log.e(TAG, exception.message ?: "Unknown error occurred") } } } }
Gestione delle richieste di messaggi di chat
Abbiamo bisogno di un modo per gestire le nostre richieste di messaggi di chat in tutti i loro stati possibili:
-
In sospeso: un messaggio è stato inviato a una chat room ma non è stato ancora confermato o rifiutato.
-
Confermato: un messaggio è stato inviato dalla chat room a tutti gli utenti, inclusi noi.
-
Rifiutato: un messaggio è stato rifiutato dalla chat room con un oggetto di errore.
Conserveremo le richieste di chat e i messaggi di chat non risolti in un elencoChatEntries.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() } }
Per collegare l'elenco all'interfaccia utente, utilizziamo un adattatore
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 }
Passaggi finali
È ora di collegare il nuovo adattatore, vincolando la classe ChatEntries
a 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 fun setUpChatView() { 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 } } }
Poiché abbiamo già una classe responsabile di tenere traccia delle richieste di chat (ChatEntries
), siamo pronti a implementare il codice per la manipolazione delle entries
in RoomListener. Aggiorneremo entries
e connectionState
in base all'evento a cui stiamo rispondendo:
Kotlin:
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt package com.chatterbox.myapp // ... class MainActivity : AppCompatActivity() { // ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) // Create room instance room = ChatRoom(REGION, ::fetchChatToken).apply { lifecycleScope.launch { stateChanges().collect { state -> Log.d(TAG, "state change to $state") updateConnectionState(state) if (state == ChatRoom.State.DISCONNECTED) { entries.removeAll() } } } lifecycleScope.launch { receivedMessages().collect { message -> Log.d(TAG, "messageReceived $message") entries.addReceivedMessage(message) } } lifecycleScope.launch { receivedEvents().collect { event -> Log.d(TAG, "eventReceived $event") } } lifecycleScope.launch { deletedMessages().collect { event -> Log.d(TAG, "messageDeleted $event") entries.removeMessage(event.messageId) } } lifecycleScope.launch { disconnectedUsers().collect { event -> Log.d(TAG, "userDisconnected $event") } } } binding.sendButton.setOnClickListener(::sendButtonClick) binding.connectButton.setOnClickListener {connect()} setUpChatView() updateConnectionState(ChatRoom.State.DISCONNECTED) } // ... }
Ora dovresti essere in grado di eseguire la tua applicazione. Consulta la pagina Costruzione ed esecuzione dell'app./gradlew :auth-server:run
oppure eseguendo l'attività auth-server:run
di Gradle direttamente da Android Studio.