preface

When it comes to instant messaging, many people are afraid of it, including me before. Long links, automatic reconnection, keepalive, message storage, etc., all seem to be big projects, and I usually turn to the third party platform.

This idea may have been a bit of a hassle a few years ago, when there was no OkHttp and no various packaged databases. A few years ago, I also wrote a chat demo using Netty for Android long connection experience (based on Netty).

Now, with these excellent open source frameworks, we can build a complete instant messaging application on the shoulders of giants.

Hopefully, this article will make people feel that instant messaging is not too difficult, and no longer rely on third-party platforms.

Function of split

  • Long links

As we know, WebSocket has been widely used by clients in recent years, and now OkHttp can also use WebSocket easily and quickly, so we also use WebSocket as a bridge of communication

  • Automatic reconnection

Mobile devices cannot guarantee network quality, so we need to support automatic reconnection after disconnection

  • Keep alive

Today, there is no real meaning of survival, have to say, this is the evolution of the Domestic Android environment, so how to do wechat survival, because the influence of wechat is too big, the major mobile phone manufacturers have opened the back door

The purpose of keepalive is to make the application can still receive messages unimpeded after entering the background. Now major manufacturers have provided system-level push. When the application enters the background, we use the manufacturer’s push to remind us of the message, so there is no need to do keepalive

  • Message storage

Messages on the server are sent in real time. In order to facilitate users to view historical messages, we need to store the messages in a local database, and generally chat supports account switching. Therefore, we need to consider multiple database storage

  • Offline message

When an application is killed by the system or the device is disconnected from the network, messages cannot be received. Therefore, you need to enable offline messages to ensure that the messages received by users are complete

  • Message display

The above function is the basis of our implementation of chat, and chat is ultimately user interaction, here mainly introduces the session list page and chat page

Long links

OkHttp 3.5 is starting to support WebSocket, and you only need a WS link to quickly connect to a server

object WebSocketManager {
    private const val WS_URL = "ws://x.x.x"
    private val httpClient by lazy {
        OkHttpClient().newBuilder()
            .readTimeout(10, TimeUnit.SECONDS)
            .writeTimeout(10, TimeUnit.SECONDS)
            .connectTimeout(10, TimeUnit.SECONDS)
            .pingInterval(40, TimeUnit.SECONDS)
            .retryOnConnectionFailure(true)
            .build()
    }
    private var webSocket: WebSocket? = null

    private fun connect(a) {
        val request = Request.Builder()
            .url(WS_URL)
            .build()
        httpClient.newWebSocket(request, wsListener)
    }

    private val wsListener = object : WebSocketListener() {
        override fun onOpen(webSocket: WebSocket, response: Response) {
            super.onOpen(webSocket, response)
            // The connection is established
        }

        override fun onMessage(webSocket: WebSocket, text: String) {
            super.onMessage(webSocket, text)
            // Received a String message from the server
        }

        override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
            super.onClosing(webSocket, code, reason)
            // The server is ready to CLOSE the connection
        }

        override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
            super.onClosed(webSocket, code, reason)
            // The connection is closed
        }

        override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?). {
            super.onFailure(webSocket, t, response)
            / / make a mistake}}}Copy the code

Is it very simple?

But so far we have only implemented the connection to the server, and now we add automatic reconnection

Automatic reconnection

We know that mobile devices may often experience poor network or mobile/WiFi network switching, in which case the long link will be disconnected and we need to reconnect to the server at the appropriate time

The user login

This depends on the service. Generally, it monitors the login status. If the login succeeds, the connection is made; if the login exits, the connection is disconnected

The network switched from disconnected to connected

This is easy to understand. It happens when a device switches from a netless network to a connected network, from a mobile network to a WiFi network, where you can register the network status monitor

object NetworkStateManager : CoroutineScope by MainScope() {
    private const val TAG = "NetworkStateManager"
    private val _networkState = MutableLiveData(false)
    val networkState: LiveData<Boolean> = _networkState

    @JvmStatic
    fun init(context: Context) {
        _networkState.postValue(NetworkUtils.isNetworkConnected(context))
        val filter = IntentFilter()
        filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION)
        context.registerReceiver(NetworkStateReceiver(), filter)
    }

    class NetworkStateReceiver : BroadcastReceiver() {
        override fun onReceive(context: Context? , intent:Intent?). {
            if (context == null || intent == null) {
                return
            }
            val isConnected =
                intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false).not()
            Log.d(TAG, "network state changed, is connected: $isConnected")
            launch {
                _networkState.postValue(isConnected)
            }
        }
    }
}
Copy the code

Provides LiveData to listen to network status

Applications switch from background to foreground

Some manufacturers may restrict the background networking of applications after the energy-saving mode is enabled, that is, applications cannot connect to the network when they enter the background. However, the device is not disconnected from the network, so the network status monitoring fails. In this scenario, we can try to reconnect the applications after they are cut back to the foreground

object AppForeground : Application.ActivityLifecycleCallbacks {
    private var foregroundActivityCount = 0
    private val appForegroundInternal = MutableLiveData(false)

    val appForeground: LiveData<Boolean> = appForegroundInternal

    fun init(application: Application) {
        application.registerActivityLifecycleCallbacks(this)}override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?).{}override fun onActivityStarted(activity: Activity) {
        foregroundActivityCount++
        if (appForegroundInternal.value == false) {
            appForegroundInternal.value = true}}override fun onActivityResumed(activity: Activity){}override fun onActivityPaused(activity: Activity){}override fun onActivityStopped(activity: Activity) {
        foregroundActivityCount--
        if (foregroundActivityCount == 0 && appForegroundInternal.value == true) {
            appForegroundInternal.value = false}}override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle){}override fun onActivityDestroyed(activity: Activity){}}Copy the code

Timing reconnection

There is a scenario where the user is connected to an invalid WiFi network, that is, the network is connected but cannot connect to the Internet, or the server simply goes down and fails to connect, so we need a timed reconnection mechanism

In order to avoid repeated invalid connections, we can use the Fibonacci sequence as the reconnection time, but it cannot be infinitely larger and requires a maximum reconnection time

At the same time, in order to avoid the server downtime, each device uses the same reconnection interval, so that all devices are connected at the same time after the server is restored, and the number of connections reaches the peak instantly, which is likely to lead to the server downtime again, we need to use a random reconnection time

private const val MAX_INTERVAL = DateUtils.HOUR_IN_MILLIS
private var lastInterval = 0L
private var currInterval = 1000L

private fun getReconnectInterval(a): Long {
    if (currInterval >= MAX_INTERVAL) {
        return MAX_INTERVAL
    }
    val interval = lastInterval + currInterval
    lastInterval = currInterval
    currInterval = interval
    return interval
}

private fun resetReconnectInterval(a) {
    lastInterval = 0
    // Use random numbers to avoid server downtime
    currInterval = Random.nextLong(1000.2000)}Copy the code

Clean up the long link and auto reconnect part of the complete code

object WebSocketManager {
    private const val WS_URL = "ws://x.x.x"
    private lateinit var threadHandler: Handler
    private val httpClient by lazy {
        OkHttpClient().newBuilder()
            .readTimeout(10, TimeUnit.SECONDS)
            .writeTimeout(10, TimeUnit.SECONDS)
            .connectTimeout(10, TimeUnit.SECONDS)
            .pingInterval(40, TimeUnit.SECONDS)
            .retryOnConnectionFailure(true)
            .build()
    }
    private var webSocket: WebSocket? = null

    private const val MAX_INTERVAL = DateUtils.HOUR_IN_MILLIS
    private var lastInterval = 0L
    private var currInterval = 1000L

    private val _connectState = MutableLiveData(ConnectState.DISCONNECT)
    val connectState: LiveData<ConnectState> = _connectState

    enum class ConnectState {
        CONNECTING,
        CONNECTED,
        DISCONNECT
    }

    fun init(context: Context) {
        val handlerThread = HandlerThread(TAG)
        handlerThread.start()
        threadHandler = Handler(handlerThread.looper)

        NetworkStateManager.networkState.observeForever { isConnected ->
            if (isConnected && connectState.value == ConnectState.DISCONNECT) {
                resetReconnectInterval()
                // The network status is delayed. Delay reconnection
                connect(1000)}}// APP goes back to the foreground and tries to reconnect
        AppForeground.appForeground.observeForever { foreground ->
            Log.d(TAG, "app foreground state changed, is foreground: $foreground")
            if (foreground && connectState.value == ConnectState.DISCONNECT) {
                connect(1000)}}}private fun connect(delay: Long = 0) {
        removeCallbacks(connectRunnable)
        runInThread(connectRunnable, delay)
    }

    private fun autoReconnect(a) {
        val interval = getReconnectInterval()
        removeCallbacks(connectRunnable)
        runInThread(connectRunnable, interval)
    }

    private val connectRunnable = Runnable {
        if(connectState.value ! = ConnectState.DISCONNECT) { Log.w(TAG,"connect cancel cause state error")
            return@Runnable
        }
        if(! NetworkUtils.isNetworkConnected(context)) { Log.w(TAG,"connect cancel cause network disconnect")
            return@Runnable
        }
        removeBindTimeoutRunnable()
        realConnect()
    }

    private fun realConnect(a) {
        _connectState.postValue(ConnectState.CONNECTING)
        val request = Request.Builder()
            .url(WS_URL)
            .build()
        httpClient.newWebSocket(request, wsListener)
    }

    private val wsListener = object : WebSocketListener() {
        override fun onOpen(webSocket: WebSocket, response: Response) {
            super.onOpen(webSocket, response)
            // The connection is established
            runInThread {
                this@WebSocketManager.webSocket = webSocket
            }
        }

        override fun onMessage(webSocket: WebSocket, text: String) {
            super.onMessage(webSocket, text)
            // Received a String message from the server
            runInThread {
                handleMessage(text)
            }
        }

        override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
            super.onClosing(webSocket, code, reason)
            // The server is ready to CLOSE the connection
        }

        override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
            super.onClosed(webSocket, code, reason)
            // The connection is closed
            onFailure(webSocket, IllegalStateException("web socket closed unexpected"), null)}override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?). {
            super.onFailure(webSocket, t, response)
            / / make a mistake
            runInThread {
                this@WebSocketManager.webSocket = null
                _connectState.postValue(ConnectState.DISCONNECT)
                autoReconnect()
            }
        }
    }

    private fun release(a){ webSocket? .cancel() }private fun getReconnectInterval(a): Long {
        if (currInterval >= MAX_INTERVAL) {
            return MAX_INTERVAL
        }
        val interval = lastInterval + currInterval
        lastInterval = currInterval
        currInterval = interval
        return interval
    }

    private fun resetReconnectInterval(a) {
        lastInterval = 0
        // Use random numbers to avoid server downtime
        currInterval = Random.nextLong(1000.2000)}private fun runInThread(r: Runnable) {
        runInThread(r, 0)}private fun runInThread(r: Runnable, delay: Long) {
        threadHandler.postDelayed(r, delay)
    }

    private fun removeCallbacks(r: Runnable) {
        threadHandler.removeCallbacks(r)
    }
}
Copy the code

Message storage

Let’s first comb through the table structure

Chat collation is divided into two parts, session and message, we can divide database table by this, divided into two tables, one store session, one store message

Some of you may wonder, do we need to distinguish between private and private conversations?

In my opinion, private chat and single chat are just two types of chat, and there is no difference in the data stored, so there is no need to distinguish

  • The sessions table

A session can have only one chat object. A private chat is a peer and a group chat is a group. Therefore, you can use the ID of the chat object as the primary key

  • The message list

In the message table, messages and chat objects are many-to-many, and we can use message IDS or increment ids as the primary key

For account switching, we need to support switching between different databases. The user ID can be used as the database name, and the user can switch to the corresponding data after successful login

Google provides Room in JetPack to help us easily navigate SqlLite, but we chose Guo Shen’s LitePal because of the ease of switching between databases quickly

That multi-database feature you asked for is finally here

The sessions table

data class ConversionBean(
    // ID of the chat object
    @Column(unique = true, index = true, nullable = false)
    private val chat_id: String? = null.// Type of conversation, private or group
    @Column(nullable = false, defaultValue = ChatType.PERSON)
    private val chat_type: String? = null.// Session name
    @Column(nullable = true)
    var name: String? = null.// Session avatar
    @Column(nullable = true)
    var avatar: String? = null.// Last message
    @Column(nullable = true)
    var last_message: String? = null./ / not reading
    @Column(nullable = false, defaultValue = "0")
    var unread_count: Int? = null.// The time of the last message
    @Column(nullable = false, defaultValue = "0")
    var update_time: Long? = null,
) : LitePalSupport()
Copy the code

The message list

data class MessageBean(
    / / message ID
    @Column(index = true, nullable = false, defaultValue = "0")
    var msg_id: Long? = null.// ID of the chat object
    @Column(index = true, nullable = false)
    private val chat_id: String? = null.// Type of conversation, private or group
    @Column(nullable = false, defaultValue = ChatType.PERSON)
    private val chat_type: String? = null.// Message type
    @Column(nullable = false, defaultValue = MsgType.TEXT)
    private val msg_type: String? = null.// Message sender ID
    @Column(nullable = false)
    val from_uuid: String? = null.// The sender's nickname
    @Column(nullable = true)
    val from_nickname: String? = null.// Picture of the sender
    @Column(nullable = true)
    val from_avatar: String? = null.// Message content
    @Column(nullable = true)
    val content: String? = null.// Whether it has been read
    @Column(index = true, nullable = false, defaultValue = "0")
    var is_read: Int? = null.// Message sending status
    @Column(defaultValue = MsgStatus.SUCCESS)
    var status: String? = null.// Message sending time
    @Column(index = true, nullable = false, defaultValue = "0")
    val time: Long? = null,
) : LitePalSupport()
Copy the code

Database operations

object IMDatabase {
    fun init(context: Context) {
        LitePal.initialize(context)
        loginState.observeForever { isLogin ->
            if (isLogin) {
                onLogin()
            } else {
                onLogout()
            }
        }
    }

    /** * Open database */
    fun onLogin(a) {
        if (uuid.isNotEmpty()) {
            val litePalDB = LitePalDB.fromDefault("im#${uuid}") LitePal.use(litePalDB!!) }}/** * Log out and close the database */
    fun onLogout(a) {
        LitePal.useDefault()
    }

    /** * Query the session list */
    fun queryConversionList(a): List<ConversionBean> {
        return LitePal.order("update_time desc").find(ConversionBean::class.java)
    }

    /** * Get the session object */
    fun getConversion(chatId: String): ConversionBean? {
        return LitePal.where("chat_id = ?", chatId).findFirst(ConversionBean::class.java)
    }

    /** * Saves the session for local new session */
    fun saveConversion(
        chatId: String,
        chatType: String,
        name: String? = null,
        avatar: String? = null,
        lastMsg: String? = null,
        unreadCount: Int? = null,
        updateTime: Long? = null
    ): ConversionBean? {
        valconversion = ConversionBean( chat_id = chatId, chat_type = chatType, name = name, avatar = avatar, last_message = lastMsg, unread_count = unreadCount, update_time = updateTime ? : System.currentTimeMillis() ) conversion.save()return conversion
    }

    /** * Updates session information */
    fun updateConversion(
        chatId: String,
        name: String? = null,
        avatar: String? = null,
        lastMsg: String? = null,
        unreadCount: Int? = null,
        unreadCountAdd: Int? = null,
        updateTime: Long? = null
    ) {
        valconversion = getConversion(chatId) ? :return
        if(name ! =null) {
            conversion.name = name
        }
        
        // ...

        conversion.save()
    }

    /**
     * 删除会话
     */
    fun deleteConversion(chatId: String) {
        LitePal.deleteAll(ConversionBean::class.java, "chat_id = ?", chatId)
    }

    /** * the session is set to read */
    fun setRead(chatId: String) {
        LitePal.where("chat_id = ?", chatId)
            .findFirst(ConversionBean::class.java)? .apply { unread_count =0
                save()
            }
        MessageBean(is_read = 1).updateAllAsync("chat_id = ? AND is_read = ?", chatId, "0")}/** * query message */
    fun queryMessageList(chatId: String, offset: Int, limit: Int): MutableList<MessageBean> {
        return LitePal.where("chat_id = ?", chatId)
            .order("time desc")
            .offset(offset)
            .limit(limit)
            .find(MessageBean::class.java)
    }

    /** * save the message */
    fun saveMessage(message: MessageBean) {
        message.save()
    }

    /** * Batch save messages */
    fun saveMessageList(msgList: List<MessageBean>) {
        LitePal.saveAll(msgList)
    }

    /** * Update message sending status */
    fun updateMessageStatus(id: Long, status: String, msg_id: Long? = null) {
        val bean = MessageBean(status = status, msg_id = msg_id)
        bean.update(id)
    }

    /** * Delete session message */
    fun deleteMessages(chatId: String) {
        LitePal.deleteAll(MessageBean::class.java, "chat_id = ?", chatId)
    }
}
Copy the code

Offline message

After the long link is successfully established, the offline message can be obtained through the API interface

You can provide the server with the ID of the last message to retrieve all offline messages

Since this is business specific, only implementation ideas are provided

Message display

To facilitate the interaction between the logic layer and the UI layer, we abstracted the uI-related logic out of the interface and provided it to the UI

According to function division, we can provide session service, message receiving service, message sending service

Session services

interface ConversionService : IMService {
    // All readings are not displayed at the entrance
    val totalUnreadCount: LiveData<Int>
    // Session list
    val conversionList: LiveData<List<ConversionBean>>

    /** * get session */
    fun getConversion(chatId: String): ConversionBean?

    /** * Start a new session */
    fun newConversion(
        chatId: String,
        chatType: String,
        name: String? , avatar:String?).: ConversionBean?

    /** * Enter the session, no new message */
    fun onEnterConversion(chatId: String)

    /** * Leaves the session and continues with a new message */
    fun onExitConversion(chatId: String)

    /** * Updates session information */
    suspend fun updateConversionInfo(
        chatId: String,
        name: String? , avatar:String?).

    /**
     * 删除会话
     */
    suspend fun deleteConversion(chatId: String)

    /** * Clears session messages */
    suspend fun deleteMessages(chatId: String)
}
Copy the code

Message receiving service

typealias MessageObserver = (msgList: List<MessageBean>) -> Unit

interface MessageReceiveService : IMService {
    /** * Add new message listener */
    fun addMessageObserve(observer: MessageObserver)

    /** * remove new message listener */
    fun removeMessageObserve(observer: MessageObserver)

    /** * query message */
    suspend fun queryMessageList(chatId: String, offset: Int, limit: Int): List<MessageBean>
}
Copy the code

Message sending service

typealias SendMessageCallback = (result: Result<MessageBean>) -> Unit

typealias MessageStatusObserver = (msg: MessageBean) -> Unit

interface MessageSendService : IMService {
    /** * Add message status listener */
    fun addMessageStatusObserve(observer: MessageStatusObserver)

    /** * remove message status listener */
    fun removeMessageStatusObserve(observer: MessageStatusObserver)

    /** * Send text message */
    fun sendTextMessage(uuid: String, chatType: String, text: String, callback: SendMessageCallback)

    /** * send picture message */
    fun sendImageMessage(uuid: String, chatType: String, file: File, callback: SendMessageCallback)

    /** * Retry sending */
    fun resendMessage(msg: MessageBean, callback: SendMessageCallback?).
}
Copy the code

Currently only text and image messages are implemented, and more message types can be extended

The implementation class of the service is also relatively simple, mainly the message sending, receiving logic processing, and the invocation of the database interface

With these service interfaces, it’s easy for the UI to be implementation-aware

The session list

Monitor ConversionService#conversionList for data updates

IM.getService<ConversionService>().conversionList.observe(this) {
    adapter.refresh(it)
    if (it.isEmpty()) {
        showEmpty()
    } else {
        showSuccess()
    }
}
Copy the code

The chat page

Get history messages from the database

lifecycleScope.launch {
    val list = IM.getService<MessageReceiveService>()
        .queryMessageList(conversion.getChatId(), messageList.size, QUERY_MSG_COUNT)
    messageList.clear()
    messageList.addAll(list)
    adapter.notifyDataSetChanged()
    scrollToBottom()
}
Copy the code

Add new message listener and add it to message list after receiving new message

IM.getService<MessageReceiveService>().addMessageObserve(messageObserver)

private val messageObserver: MessageObserver = { list ->
    list.forEach { msg ->
        if (msg.getChatId() == conversion.getChatId()) {
            onNewMessage(msg)
        }
    }
}

private fun onNewMessage(msg: MessageBean) {
    messageList.add(msg)
    adapter.notifyDataSetChanged()
    scrollToBottom()
}
Copy the code

Listen to the input box and send button, call the API interface to send a message

private fun sendTextMsg(a) {
    val text = viewBinding.etInput.text.toString()
    viewBinding.btnSend.isEnabled = false
    IM.getService<MessageSendService>()
        .sendTextMessage(conversion.getChatId(), conversion.getChatType(), text) { result ->
            viewBinding.btnSend.isEnabled = true
            if (result.isSuccess) {
                onNewMessage(result.getOrNull()!!)
                viewBinding.etInput.text = null
            } else {
                "Sending failed. Please try again later.".toast()
            }
        }
}
Copy the code

For different types of messages, you can use the viewType of RecyclerView to distinguish display, in fact, many attributes of messages are universal, such as avatar, nickname, time, etc., so we can encapsulate a message base class, messages of different types inherit the base class, only need to care about the rendering of message content

Message Item base class

abstract class MessageBaseViewHolder(
    private val binding: ItemChatMessageBaseBinding,
    private val listener: OnMessageEventListener? = null
) : RecyclerView.ViewHolder(binding.root) {
    protected val content: View
    protected lateinit var msg: MessageBean

    init {
        content = LayoutInflater.from(binding.root.context)
            .inflate(getContentResId(), binding.content, false)
        binding.content.addView(content)
        val onClickListener = ClickListener()
        binding.ivPortraitRight.setOnClickListener(onClickListener)
        binding.ivPortraitLeft.setOnClickListener(onClickListener)
        binding.ivMessageStatus.setOnClickListener(onClickListener)
    }

    @LayoutRes
    protected abstract fun getContentResId(a): Int

    fun onBind(msg: MessageBean) {
        this.msg = msg
        setGravity()
        setPortrait()
        refreshContent()
        setNickname()
        setTime()
        setStatus()
    }

    protected fun isReceivedMessage(a): Boolean {
        return msg.isFromMe().not()
    }

    protected abstract fun refreshContent(a)

    protected open fun isCenterMessage(a): Boolean {
        return false
    }

    protected open fun isShowNick(a): Boolean {
        return msg.getChatType() == ChatType.GROUP && isReceivedMessage() && isCenterMessage().not()
    }

    protected open fun isShowTime(a): Boolean {
        return msg.isShowTime == true
    }

    private fun setGravity(a) {
        val gravity = if (isCenterMessage()) {
            Gravity.CENTER_HORIZONTAL
        } else if (isReceivedMessage()) {
            Gravity.LEFT
        } else {
            Gravity.RIGHT
        }

        binding.contentWithStatus.gravity = gravity
    }

    private fun setPortrait(a) {
        binding.ivPortraitRight.visibleOrGone(false)
        binding.ivPortraitLeft.visibleOrGone(false)
        var show: ImageView? = null
        if(isReceivedMessage() && ! isCenterMessage()) { binding.ivPortraitLeft.visibleOrGone(true)
            show = binding.ivPortraitLeft
        } else if(! isReceivedMessage() && ! isCenterMessage()) { binding.ivPortraitRight.visibleOrGone(true) show = binding.ivPortraitRight } show? .loadAvatar(getPortraitUrl()) }private fun setNickname(a) {
        binding.tvNickname.text = if (isShowNick()) this.msg.getFromNickname() else null
    }

    private fun setTime(a) {
        binding.tvTime.visibleOrGone(isShowTime())
        binding.tvTime.text = this.msg.time.dateFriendly()
    }

    private fun setStatus(a) {
        binding.ivMessageStatus.visibleOrGone(msg.isFromMe() && msg.status == MsgStatus.FAIL)
    }

    private fun getPortraitUrl(a): String? {
        if (isReceivedMessage()) {
            return msg.from_avatar
        } else {
            returnUserCenter.userInfoState.value? .avatar } } }Copy the code

Text message Item

open class MessageTextViewHolder(
    binding: ItemChatMessageBaseBinding,
    listener: OnMessageEventListener? = null
) : MessageBaseViewHolder(binding, listener) {
    protected val tvMessageContent: TextView by lazy {
        content.findViewById(R.id.tvMessageContent)
    }

    override fun getContentResId(a): Int {
        return R.layout.item_chat_message_text
    }

    override fun refreshContent(a){ tvMessageContent.isSelected = ! isReceivedMessage()val content = msg.content
        tvMessageContent.text = content
    }
}
Copy the code

At this point, a simple instant messaging function is almost complete.

conclusion

This article with personal experience to take you to sort out the main function of APP instant messaging split, and simple implementation, because it involves project code, so it is inconvenient to post the source code.

If it is helpful to you, please like and support. If you have any questions in the development process, you can also leave a comment on the article. I will try my best to help you answer them.