• Android Socket通讯 之 表情列表优化、业务逻辑优化


    前言

      本文将对Socket通讯进行进一步的优化,并增加新的功能,具体改变了那些,一起来看。效果如下图所示:

    正文

      本文的优化,从逻辑、UI和功能三个方向上进行,之前的代码实际上是有一些逻辑问题。

    一、增加线程池

      之前在使用的过程中,每一次发送一条消息就会新建一个线程,这无疑是不可取的,而现在我们通过一个线程池来管理,对多个线程进行统一地管理,避免资源竞争中出现的问题,对线程进行复用,线程在执行完任务后不会立刻销毁,而会等待另外的任务,这样就不会频繁地创建、销毁线程和调用GC。

      那么问题又来了,既然线程池有这么多的好处,为什么作者一开始不用呢?

      emm… 我一开始没想那么多,没有想过这个Socket会去写系列文章,现在写也不晚嘛!嗯,就是这样!

    ① 增加服务端线程池

    打开SocketServer,在里面声明线程池,代码如下:

    private var serverThreadPool: ExecutorService? = null
    
    • 1

    在发送消息到客户端的时候对这个线程池进行初始化,并且执行子线程,修改sendToClient()函数,代码如下:

    	fun sendToClient(msg: String) {
            if (serverThreadPool == null) {
                serverThreadPool = Executors.newCachedThreadPool()
            }
            serverThreadPool?.execute {
                if (socket == null) {
                    mCallback.otherMsg("客户端还未连接")
                    return@execute
                }
                if (socket!!.isClosed) {
                    mCallback.otherMsg("Socket已关闭")
                    return@execute
                }
                outputStream = socket!!.getOutputStream()
                try {
                    outputStream.write(msg.toByteArray())
                    outputStream.flush()
                } catch (e: IOException) {
                    e.printStackTrace()
                    mCallback.otherMsg("向客户端发送消息: $msg 失败")
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

      在发送消息之前,先检查socket 是否为null,因为有可能你在还没有客户端连接的时候就给客户端发送消息,不做处理的话,会导致空指针异常,程序闪退。同时将异常消息通过otherMsg()回调到页面上,页面上可以使用showMsg()函数告知用户。

    而当我们停止服务的时候也需要关闭线程池,修改stopServer()函数,代码如下:

    	fun stopServer() {
            socket?.apply {
                shutdownInput()
                shutdownOutput()
                close()
            }
            serverSocket?.close()
    
            //关闭线程池
            serverThreadPool?.shutdownNow()
            serverThreadPool = null
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    ② 增加客户端线程池

    打开SocketClient,在里面声明线程池,代码如下:

        private var clientThreadPool: ExecutorService? = null
    
    • 1

    在发送消息到服务端的时候对这个线程池进行初始化,并且执行子线程,修改sendToServer()函数,代码如下:

    	fun sendToServer(msg: String) {
            if (clientThreadPool == null) {
                clientThreadPool = Executors.newSingleThreadExecutor()
            }
            clientThreadPool?.execute {
                if (socket == null) {
                    mCallback.otherMsg("客户端还未连接")
                    return@execute
                }
                if (socket!!.isClosed) {
                    mCallback.otherMsg("Socket已关闭")
                    return@execute
                }
                outputStream = socket?.getOutputStream()
                try {
                    outputStream?.write(msg.toByteArray())
                    outputStream?.flush()
                } catch (e: IOException) {
                    e.printStackTrace()
                    mCallback.otherMsg("向服务端发送消息: $msg 失败")
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

      在发送消息之前,先检查socket 是否为null,因为有可能你在客户端没连接到服务端的时候就给服务端发送消息,不做处理的话,会导致空指针异常,程序闪退。同时将异常消息通过otherMsg()回调到页面上,页面上可以使用showMsg()函数告知用户,这里和服务端的处理类似。

    而当我们关闭客户端连接的时候也需要关闭线程池,修改closeConnect()函数,代码如下:

    	fun closeConnect() {
            inputStreamReader?.close()
            outputStream?.close()
            socket?.close()
            //关闭线程池
            clientThreadPool?.shutdownNow()
            clientThreadPool = null
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    写完这些,建议你运行一下,说不定就会报错,运行之后效果和之前是一样的,但是我们避免了一些问题的出现,虽然你感觉不到,但是这很有必要。

    二、修改表情出现布局

      在修改之前,我们先来看看之前的是什么效果,点击表情的时候出现了底部弹窗,弹窗覆盖了布局布局,同时页面上有阴影,如下图所示:

    在这里插入图片描述

    我们再来看看QQ的:

    在这里插入图片描述
    QQ的会将输入框布局顶上去,我们现在是覆盖了,那么我们怎么做到顶上去呢?

    ① BottomSheet使用

      Android中的布局可以实现这样的功能,因为底部是一样的,所以可以写在一起,目前我们先这么来写,后续可能会有改动。在layout下新建一个bottom_sheet_edit.xml,里面的代码如下:

    
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/bottom_sheet"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:background="@color/white"
        android:orientation="vertical"
        app:behavior_hideable="true"
        app:behavior_peekHeight="50dp"
        app:layout_behavior="@string/bottom_sheet_behavior">
    
        
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:gravity="center_vertical"
            android:paddingStart="8dp"
            android:paddingEnd="8dp">
    
            <androidx.appcompat.widget.AppCompatImageView
                android:id="@+id/iv_emoji"
                android:layout_width="36dp"
                android:layout_height="36dp"
                android:layout_marginEnd="8dp"
                android:src="@drawable/ic_emoji" />
    
            <androidx.appcompat.widget.AppCompatEditText
                android:id="@+id/et_msg"
                android:layout_width="0dp"
                android:layout_height="40dp"
                android:layout_weight="1"
                android:background="@drawable/shape_et_bg"
                android:gravity="center_vertical"
                android:hint="发送给客户端"
                android:padding="10dp"
                android:textSize="14sp" />
    
            <com.google.android.material.button.MaterialButton
                android:id="@+id/btn_send_msg"
                android:layout_width="80dp"
                android:layout_height="wrap_content"
                android:layout_marginStart="8dp"
                android:text="发送"
                app:cornerRadius="8dp" />
        LinearLayout>
    
        
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_emoji"
            android:overScrollMode="never"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    
    LinearLayout>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    这个布局是这样的效果。
    在这里插入图片描述
    这里的50dp是指底部显示的高度,底部的列表就用来装载表情。然后我们需要使用CoordinatorLayout(协调布局)来进行配置。

    ② CoordinatorLayout使用

    在修改之前,先在colors.xml中增加一个颜色,代码如下:

    <color name="bg_color">#F8F8F8color>
    
    • 1

    这个颜色作为页面的背景色,然后我们修改activity_server.xml布局,代码如下:

    
    <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"
        android:background="@color/bg_color"
        tools:context=".ui.ServerActivity">
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginBottom="50dp"
            android:orientation="vertical">
    
            <com.google.android.material.appbar.MaterialToolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="@color/purple_500"
                app:navigationIcon="@drawable/ic_back_black"
                app:navigationIconTint="@color/white"
                app:subtitleTextColor="@color/white"
                app:title="服务端"
                app:titleTextColor="@color/white">
    
                <TextView
                    android:id="@+id/tv_start_service"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="end"
                    android:padding="16dp"
                    android:text="开启服务"
                    android:textColor="@color/white"
                    android:textSize="14sp" />
            com.google.android.material.appbar.MaterialToolbar>
    
            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/rv_msg"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />
        LinearLayout>
        
        <include
            android:id="@+id/lay_bottom_sheet_edit"
            layout="@layout/bottom_sheet_edit" />
    
    androidx.coordinatorlayout.widget.CoordinatorLayout>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    再来修改activity_client.xml,代码如下:

    
    <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"
        android:background="@color/bg_color"
        tools:context=".ui.ClientActivity">
    
        <LinearLayout
            app:layout_behavior="@string/appbar_scrolling_view_behavior"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginBottom="50dp"
            android:orientation="vertical">
    
            <com.google.android.material.appbar.MaterialToolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="@color/purple_500"
                app:navigationIcon="@drawable/ic_back_black"
                app:navigationIconTint="@color/white"
                app:title="客户端"
                app:titleTextColor="@color/white">
    
                <TextView
                    android:id="@+id/tv_connect_service"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="end"
                    android:padding="16dp"
                    android:text="连接服务"
                    android:textColor="@color/white"
                    android:textSize="14sp" />
            com.google.android.material.appbar.MaterialToolbar>
    
            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/rv_msg"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />
        LinearLayout>
        
        <include
            android:id="@+id/lay_bottom_sheet_edit"
            layout="@layout/bottom_sheet_edit" />
    
    androidx.coordinatorlayout.widget.CoordinatorLayout>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    在这里插入图片描述
      底部插入的这个布局就是我们刚才写的bottom_sheet_edit.xml,50dp此时可以在页面上显示出来。其余的部分我们需要在点击表情的使用再显示出来。

    ③ Activity中修改

      因为布局有修改,那么对应的ServerActivity和ClientActivity也会有修改,下面这个函数在两个Activity中都需要调用,代码如下:

    	//是否显示表情
        private var isShowEmoji = false
    
        private var bottomSheetBehavior: BottomSheetBehavior<LinearLayout>? = null
    
    	private fun initBottomSheet() {
            //Emoji布局
            bottomSheetBehavior =
                BottomSheetBehavior.from(binding.layBottomSheetEdit.bottomSheet).apply {
                    state = BottomSheetBehavior.STATE_HIDDEN
                    isHideable = false
                    isDraggable = false
                }
            binding.layBottomSheetEdit.rvEmoji.apply {
                layoutManager = GridLayoutManager(context, 6)
                adapter = EmojiAdapter(SocketApp.instance().emojiList).apply {
                    setOnItemClickListener(object : EmojiAdapter.OnClickListener {
                        override fun onItemClick(position: Int) {
                            val charSequence = SocketApp.instance().emojiList[position]
                            checkedEmoji(charSequence)
                        }
                    })
                }
            }
            //显示emoji
            binding.layBottomSheetEdit.ivEmoji.setOnClickListener {
                bottomSheetBehavior!!.state =
                    if (isShowEmoji) BottomSheetBehavior.STATE_COLLAPSED else BottomSheetBehavior.STATE_EXPANDED
            }
    
            bottomSheetBehavior!!.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
                override fun onStateChanged(bottomSheet: View, newState: Int) {
                    when (newState) {
                        BottomSheetBehavior.STATE_EXPANDED -> {//显示
                            isShowEmoji = true
                            binding.layBottomSheetEdit.ivEmoji.setImageDrawable(
                                ContextCompat.getDrawable(
                                    this@ServerActivity,
                                    R.drawable.ic_emoji_checked
                                )
                            )
                        }
                        BottomSheetBehavior.STATE_COLLAPSED -> {//隐藏
                            isShowEmoji = false
                            binding.layBottomSheetEdit.ivEmoji.setImageDrawable(
                                ContextCompat.getDrawable(
                                    this@ServerActivity,
                                    R.drawable.ic_emoji
                                )
                            )
                        }
                        else -> isShowEmoji = false
                    }
                    Log.e(TAG, "onStateChanged: $newState")
                }
    
                override fun onSlide(bottomSheet: View, slideOffset: Float) {
    
                }
    
            })
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    这里的代码需要说明一下,我们从上往下进行说明:

    		//Emoji布局
            bottomSheetBehavior =
                BottomSheetBehavior.from(binding.layBottomSheetEdit.bottomSheet).apply {
                    state = BottomSheetBehavior.STATE_HIDDEN
                    isHideable = false
                    isDraggable = false
                }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

      这段代码就是在配置BottomSheetBehavior,这里我们通过from()函数获取与布局所绑定的BottomSheetBehavior,然后配置BottomSheetBehavior,设置默认状态为隐藏,当前不显示,不可上下拖动。

    		binding.layBottomSheetEdit.rvEmoji.apply {
                layoutManager = GridLayoutManager(context, 6)
                adapter = EmojiAdapter(SocketApp.instance().emojiList).apply {
                    setOnItemClickListener(object : EmojiAdapter.OnClickListener {
                        override fun onItemClick(position: Int) {
                            val charSequence = SocketApp.instance().emojiList[position]
                            checkedEmoji(charSequence)
                        }
                    })
                }
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

      这段代码你之前应该见过,就是那个表情弹窗中的列表适配器部分代码,这里我把它抽离到这个函数中。

    		//显示emoji
            binding.layBottomSheetEdit.ivEmoji.setOnClickListener {
                bottomSheetBehavior!!.state =
                    if (isShowEmoji) BottomSheetBehavior.STATE_COLLAPSED else BottomSheetBehavior.STATE_EXPANDED
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5

      这里就是在点击那个表情图标的时候,根据isShowEmoji的状态,来判断当前是显示还是隐藏。

    		//BottomSheet显示隐藏的相关处理
            bottomSheetBehavior!!.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
                override fun onStateChanged(bottomSheet: View, newState: Int) {
                    when (newState) {
                        BottomSheetBehavior.STATE_EXPANDED -> {//显示
                            isShowEmoji = true
                            binding.layBottomSheetEdit.ivEmoji.setImageDrawable(
                                ContextCompat.getDrawable(
                                    this@ServerActivity,
                                    R.drawable.ic_emoji_checked
                                )
                            )
                        }
                        BottomSheetBehavior.STATE_COLLAPSED -> {//隐藏
                            isShowEmoji = false
                            binding.layBottomSheetEdit.ivEmoji.setImageDrawable(
                                ContextCompat.getDrawable(
                                    this@ServerActivity,
                                    R.drawable.ic_emoji
                                )
                            )
                        }
                        else -> isShowEmoji = false
                    }
                    Log.e(TAG, "onStateChanged: $newState")
                }
    
                override fun onSlide(bottomSheet: View, slideOffset: Float) {
    
                }
    
            })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

      这里我们给bottomSheetBehavior添加了一个回调,主要是实现onStateChanged的方法,另一个方法适用于滑动的,用不上,这里我们在状态改变的时候修改isShowEmoji 的值,然后切换图标,这里的ic_emoji_checked图标需要补充一下,在drawable文件夹下新建一个ic_emoji_checked.xml,里面的代码如下:

    <vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="36dp"
        android:height="36dp"
        android:tint="@color/purple_500"
        android:viewportWidth="24"
        android:viewportHeight="24">
        <path
            android:fillColor="@color/purple_500"
            android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM15.5,11c0.83,0 1.5,-0.67 1.5,-1.5S16.33,8 15.5,8 14,8.67 14,9.5s0.67,1.5 1.5,1.5zM8.5,11c0.83,0 1.5,-0.67 1.5,-1.5S9.33,8 8.5,8 7,8.67 7,9.5 7.67,11 8.5,11zM12,17.5c2.03,0 3.8,-1.11 4.75,-2.75 0.19,-0.33 -0.05,-0.75 -0.44,-0.75L7.69,14c-0.38,0 -0.63,0.42 -0.44,0.75 0.95,1.64 2.72,2.75 4.75,2.75z" />
    </vector>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    最后记得在initView()中调用,ServerActivity和ClientActivity一样改动。
    在这里插入图片描述
    之前在initView()中所写的如下代码:

    		//显示emoji
            binding.ivEmoji.setOnClickListener {
                //显示底部弹窗
                showEmojiDialog(this,this)
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    可以删掉了,最后将

    binding.btnSendMsg
    
    • 1

    改成

    binding.layBottomSheetEdit.btnSendMsg
    
    • 1
    binding.etMsg
    
    • 1

    改成

    binding.layBottomSheetEdit.etMsg
    
    • 1

    确保当前的Activity没有报错,然后运行。下面我们通过一个GIF来看看效果。
    在这里插入图片描述

    三、业务层优化

      通过上面的修改,你有没有觉得很麻烦呢?ServerActivitty和ClientActivity中写了很多一样的代码,改动也一样,一些布局也一样,那么可不可以写到一起,然后又有区分呢?

      这其实的编程的思想不断进步有关系,第一篇文章,我们就是服务端和客户端写到一起的,然后在第二篇的时候觉得可以分开写,各自做各自的事情,但是会产生一些重复的代码和布局。到了现在我们看到这一点越来越明显了,那么我们就需要进一步细分,所以一些设计模式还是很有效果的,这些设计模式就是为了解决开发中实际的问题而出现的。

      下面开始我们的优化之路,为了减少重复的布局,我们可以写一个基类,然后我们的客户端和服务端都继承自这个基类,各自的类里面再去根据自己的差异进行设计,这样会更合理一些,类似顶层接口设计。

    ① 创建基类Activity

    在ui包下新建一个BaseSocketActivity,对应的布局为activity_base_socket.xml,布局代码如下:

    
    <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"
        android:background="@color/bg_color"
        tools:context=".ui.BaseSocketActivity">
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginBottom="50dp"
            android:orientation="vertical">
    
            <com.google.android.material.appbar.MaterialToolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="@color/purple_500"
                app:navigationIcon="@drawable/ic_back_black"
                app:navigationIconTint="@color/white"
                app:subtitleTextColor="@color/white"
                app:title="通用页面"
                app:titleTextColor="@color/white">
    
                <TextView
                    android:id="@+id/tv_func"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="end"
                    android:padding="16dp"
                    android:text="功能按钮"
                    android:textColor="@color/white"
                    android:textSize="14sp" />
            com.google.android.material.appbar.MaterialToolbar>
    
            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/rv_msg"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />
        LinearLayout>
    
        <include
            android:id="@+id/lay_bottom_sheet_edit"
            layout="@layout/bottom_sheet_edit" />
    
    androidx.coordinatorlayout.widget.CoordinatorLayout>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    然后我们修改BaseSocketActivity中的代码:

    open class BaseSocketActivity : BaseActivity() {
    
        lateinit var binding: ActivityBaseSocketBinding
    
        private val TAG = BaseSocketActivity::class.java.simpleName
    
    	lateinit var etMsg: EditText
        lateinit var btnSendMsg: Button
        lateinit var ivMore: ImageView
    
        //Socket服务是否打开
        var openSocket = false
        
        //Socket服务是否连接
        var connectSocket = false
    
        //消息列表
        private val messages = ArrayList<Message>()
    
        //消息适配器
        private lateinit var msgAdapter: MsgAdapter
    
        //是否显示表情
        private var isShowEmoji = false
    
        private var bottomSheetBehavior: BottomSheetBehavior<LinearLayout>? = null
        
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            binding = ActivityBaseSocketBinding.inflate(layoutInflater)
            setContentView(binding.root)
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    ② 标题设置

      这是基本的页面,创建了一些变量,下面我们对这个基类进行丰富,首先是设置标题,在BaseSocketActivity 中新增setTitle()函数,代码如下:

    	private fun setTitle(
            mTitle: String, mSubtitle: String = "", funcTitle: String,
            click: View.OnClickListener) {
            binding.toolbar.apply {
                title = mTitle
                subtitle = mSubtitle
                setNavigationOnClickListener { onBackPressed() }
            }
            binding.tvFunc.text = funcTitle
            binding.tvFunc.setOnClickListener(click)
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这里就是设置Toolbar的标题和子标题,已经Navigation图标点击事件处理,最后设置标题栏右侧的点击事件,我们可以再写两个函数分别处理客户端和服务端的标题,代码如下:

    	fun setServerTitle(startService: View.OnClickListener) =
            setTitle("服务端", "IP:${getIp()}", "开启服务", startService)
    
        fun setClientTitle(connectService: View.OnClickListener) =
            setTitle(mTitle = "客户端", funcTitle = "连接服务", click = connectService)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ③ 开启服务和停止服务

    下面我们再写一些函数,开始服务,代码如下:

    	fun startServer() {
            openSocket = true
            SocketServer.startServer(this)
            showMsg("开启服务")
            binding.tvFunc.text = "关闭服务"
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    停止服务,代码如下:

    	fun stopServer() {
            openSocket = false
            SocketServer.stopServer()
            showMsg("关闭服务")
            binding.tvFunc.text = "开启服务"
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    ④ 连接服务和关闭连接

    连接服务,代码如下:

    	fun connectServer(ipAddress: String) {
            connectSocket = true
            SocketClient.connectServer(ipAddress, this)
            showMsg("连接服务")
            binding.tvFunc.text = "关闭连接"
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    关闭连接,代码如下:

    	fun closeConnect() {
            connectSocket = false
            SocketClient.closeConnect()
            showMsg("关闭连接")
            binding.tvFunc.text = "连接服务"
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    ⑤ 实现接口回调

    在实现接口回调之前,我需要先修改一下ServerCallback,代码如下:

    interface ServerCallback {
        //接收客户端的消息
        fun receiveClientMsg(ipAddress: String, msg: String)
        //其他消息
        fun otherMsg(msg: String)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    修改一下ClientCallback,代码如下:

    interface ClientCallback {
        //接收服务端的消息
        fun receiveServerMsg(ipAddress: String, msg: String)
        //其他消息
        fun otherMsg(msg: String)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这里主要就是在接收消息的时候将IP地址也传递过来,下面我们修改SocketClient中的ClientThread类中的代码:

    	class ClientThread(private val socket: Socket, private val callback: ClientCallback) : Thread() {
            override fun run() {
                val inputStream: InputStream?
                try {
                    inputStream = socket.getInputStream()
                    val buffer = ByteArray(1024)
                    var len: Int
                    var receiveStr = ""
                    if (inputStream.available() == 0) {
                        Log.e(TAG, "inputStream.available() == 0")
                    }
                    while (inputStream.read(buffer).also { len = it } != -1) {
                        receiveStr += String(buffer, 0, len, Charsets.UTF_8)
                        if (len < 1024) {
                            socket.inetAddress.hostAddress?.let { callback.receiveServerMsg(it, receiveStr) }
                            receiveStr = ""
                        }
                    }
                } catch (e: IOException) {
                    e.printStackTrace()
                    e.message?.let { Log.e("socket error", it) }
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    实际上主要是这一行代码:

    socket.inetAddress.hostAddress?.let { callback.receiveServerMsg(it, receiveStr) }
    
    • 1

    然后修改SocketServer中的ServerThread类中的代码:

    	class ServerThread(private val socket: Socket, private val callback: ServerCallback) :Thread() {
    
            override fun run() {
                val inputStream: InputStream?
                try {
                    inputStream = socket.getInputStream()
                    val buffer = ByteArray(1024)
                    var len: Int
                    var receiveStr = ""
                    if (inputStream.available() == 0) {
                        Log.e(TAG, "inputStream.available() == 0")
                    }
                    while (inputStream.read(buffer).also { len = it } != -1) {
                        receiveStr += String(buffer, 0, len, Charsets.UTF_8)
                        if (len < 1024) {
                            socket.inetAddress.hostAddress?.let { callback.receiveClientMsg(it, receiveStr) }
                            receiveStr = ""
                        }
                    }
                } catch (e: IOException) {
                    e.printStackTrace()
                    e.message?.let { Log.e("socket error", it) }
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    主要是这一行代码:

    socket.inetAddress.hostAddress?.let { callback.receiveClientMsg(it, receiveStr) }
    
    • 1

    然后BaseSocketActivity实现三个接口,ServerCallback、ClientCallback和EmojiCallback。

    在这里插入图片描述

    实现里面的回调,代码如下:

    	override fun receiveClientMsg(ipAddress: String, msg: String) {
    
    	}
    
        override fun receiveServerMsg(ipAddress: String, msg: String) {
    
    	}
    	
        override fun otherMsg(msg: String) {
            
        }
    
        override fun checkedEmoji(charSequence: CharSequence) {
            
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这几个函数,需要在里面再写代码,稍等一会儿。

    ⑥ 消息处理

      之前的消息是分服务端和客户端的,而实际上是需要,发送消息的一方在右边,收到的消息在左边,这里我们首先修改一下Message类,代码如下:

    data class Message(val isMyself: Boolean, val msg: String)
    
    • 1

    然后我们修改一下item_rv_msg.xml,代码如下:

    
    <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:paddingTop="8dp"
        android:paddingBottom="8dp"
        android:orientation="vertical">
    
        <RelativeLayout
            android:id="@+id/lay_other"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
    
            <com.google.android.material.imageview.ShapeableImageView
                android:id="@+id/iv_other"
                android:layout_width="60dp"
                android:layout_height="60dp"
                android:layout_marginStart="16dp"
                android:src="@drawable/icon_server"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:shapeAppearanceOverlay="@style/circleImageStyle" />
    
            <TextView
                android:id="@+id/tv_other_msg"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="16dp"
                android:layout_marginEnd="16dp"
                android:background="@drawable/shape_left_msg_bg"
                android:text="123"
                android:textColor="@color/black"
                android:layout_toEndOf="@id/iv_other" />
        RelativeLayout>
    
        <RelativeLayout
            android:id="@+id/lay_myself"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
    
            <TextView
                android:id="@+id/tv_myself_msg"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="16dp"
                android:layout_marginEnd="16dp"
                android:layout_toStartOf="@id/iv_myself"
                android:background="@drawable/shape_right_msg_bg"
                android:text="123"
                android:textColor="@color/white"
                app:layout_constraintEnd_toStartOf="@+id/iv_myself"
                app:layout_constraintTop_toTopOf="@+id/iv_myself" />
    
            <com.google.android.material.imageview.ShapeableImageView
                android:id="@+id/iv_myself"
                android:layout_width="60dp"
                android:layout_alignParentEnd="true"
                android:layout_marginEnd="16dp"
                android:layout_height="60dp"
                android:src="@drawable/icon_client"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:shapeAppearanceOverlay="@style/circleImageStyle" />
        RelativeLayout>
    LinearLayout>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    修改适配器MsgAdapter,代码如下:

    class MsgAdapter(private val messages: ArrayList<Message>) : RecyclerView.Adapter<MsgAdapter.ViewHolder>() {
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
            ViewHolder(ItemRvMsgBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        
        override fun onBindViewHolder(holder: ViewHolder, position: Int) {
            val message = messages[position]
            if (message.isMyself) {
                holder.mView.tvMyselfMsg.text = message.msg
            } else {
                holder.mView.tvOtherMsg.text = message.msg
            }
    
            holder.mView.layOther.visibility = if (message.isMyself) View.GONE else View.VISIBLE
            holder.mView.layMyself.visibility = if (message.isMyself) View.VISIBLE else View.GONE
        }
    
        override fun getItemCount() = messages.size
    
        class ViewHolder(itemView: ItemRvMsgBinding) : RecyclerView.ViewHolder(itemView.root) {
            var mView: ItemRvMsgBinding
    
            init {
                mView = itemView
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    然后在BaseSocketActivity中进行消息适配器的处理,新增initView(),代码如下:

    	private fun initView() {
            etMsg = binding.layBottomSheetEdit.etMsg
            btnSendMsg = binding.layBottomSheetEdit.btnSendMsg
            ivMore = binding.layBottomSheetEdit.ivMore
            //初始化BottomSheet
            initBottomSheet()
    
            //初始化列表
            msgAdapter = MsgAdapter(messages)
            binding.rvMsg.apply {
                layoutManager = LinearLayoutManager(this@BaseSocketActivity)
                adapter = msgAdapter
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    新增initBottomSheet()函数,代码如下:

    	private fun initBottomSheet() {
            //Emoji布局
            bottomSheetBehavior =
                BottomSheetBehavior.from(binding.layBottomSheetEdit.bottomSheet).apply {
                    state = BottomSheetBehavior.STATE_HIDDEN
                    isHideable = false
                    isDraggable = false
                }
            //表情列表适配器
            binding.layBottomSheetEdit.rvEmoji.apply {
                layoutManager = GridLayoutManager(context, 6)
                adapter = EmojiAdapter(SocketApp.instance().emojiList).apply {
                    setOnItemClickListener(object : EmojiAdapter.OnClickListener {
                        override fun onItemClick(position: Int) {
                            val charSequence = SocketApp.instance().emojiList[position]
                            checkedEmoji(charSequence)
                        }
                    })
                }
            }
            //显示emoji
            binding.layBottomSheetEdit.ivEmoji.setOnClickListener {
                bottomSheetBehavior!!.state =
                    if (isShowEmoji) BottomSheetBehavior.STATE_COLLAPSED else BottomSheetBehavior.STATE_EXPANDED
            }
            //BottomSheet显示隐藏的相关处理
            bottomSheetBehavior!!.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
                override fun onStateChanged(bottomSheet: View, newState: Int) {
                    isShowEmoji = when (newState) {
                        BottomSheetBehavior.STATE_EXPANDED -> {//显示
                            binding.layBottomSheetEdit.ivEmoji.setImageDrawable(
                                ContextCompat.getDrawable(this@BaseSocketActivity, R.drawable.ic_emoji_checked)
                            )
                            true
                        }
                        BottomSheetBehavior.STATE_COLLAPSED -> {//隐藏
                            binding.layBottomSheetEdit.ivEmoji.setImageDrawable(
                                ContextCompat.getDrawable(this@BaseSocketActivity, R.drawable.ic_emoji)
                            )
                            false
                        }
                        else -> false
                    }
                }
    
                override fun onSlide(bottomSheet: View, slideOffset: Float){}
            })
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    消息列表代码,新增updateList()函数,代码如下:

    	private fun updateList(isMyself: Boolean, msg: String) {
            messages.add(Message(isMyself, msg))
            runOnUiThread {
                (if (messages.size == 0) 0 else messages.size - 1).apply {
                    msgAdapter.notifyItemChanged(this)
                    binding.rvMsg.smoothScrollToPosition(this)
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    最后我们修改一下之前的接口函数,代码如下:

        override fun receiveClientMsg(ipAddress: String, msg: String) = updateList(false, msg)
    
        override fun receiveServerMsg(ipAddress: String, msg: String) = updateList(false, msg)
    
        override fun otherMsg(msg: String) {
            Log.d(TAG, "otherMsg: $msg")
        }
    
        override fun checkedEmoji(charSequence: CharSequence) {
            etMsg.apply {
                setText(text.toString() + charSequence)
                setSelection(text.toString().length)//光标置于最后
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    这里直接在处理消息的时候,只要是接收到的消息,就是其他的消息,这样好区分一些。

    ⑦ 发送消息

      发送消息有两种,发给服务端和客户端,在BaseSocketActivity,新增代码如下:

    	fun sendToClient(msg: String) {
            SocketServer.sendToClient(msg)
            etMsg.setText("")
            updateList(true, msg)
        }
    	
    	fun sendToServer(msg: String) {
            SocketClient.sendToServer(msg)
            etMsg.setText("")
            updateList(true, msg)
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    现在底部的代码就写完了,下面我们修改上层的代码。

    四、上层优化

      不出意外的话,目前你的ServerActivity和ClientActivity肯定是报错的,我们可以不管他,你也可以把它们删除掉,对应的布局也删掉。

    ① ServerPlusActivity

    下面我们在ui包下创建ServerPlusActivity,代码如下:

    class ServerPlusActivity : BaseSocketActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
    
            //开启服务/停止服务
            setServerTitle { if (openSocket) stopServer() else startServer() }
    
            //发送消息给服务端
            btnSendMsg.setOnClickListener {
                val msg = etMsg.text.toString().trim()
                if (msg.isEmpty()) {
                    showMsg("请输入要发送的信息");return@setOnClickListener
                }
                //检查是否能发送消息
                val isSend = if (openSocket) openSocket else false
                if (!isSend) {
                    showMsg("当前未开启服务或连接服务");return@setOnClickListener
                }
                sendToClient(msg)
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

      这里就很简单了,通过继承BaseSocketActivity(),然后调用之前写好的方法就可以了,相比之前就简化了很多,也去掉了很多重复代码。

    ② ClientPlusActivity

    然后再来看客户端的代码,在ui包下新建一个ClientPlusActivity,代码如下:

    class ClientPlusActivity: BaseSocketActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
    
            //连接服务/关闭服务
            setClientTitle { if (connectSocket) closeConnect() else showEditDialog() }
            //发送消息给服务端
            btnSendMsg.setOnClickListener {
                val msg = etMsg.text.toString().trim()
                if (msg.isEmpty()) {
                    showMsg("请输入要发送的信息");return@setOnClickListener
                }
                //检查是否能发送消息
                val isSend = if (connectSocket) connectSocket else false
                if (!isSend) {
                    showMsg("当前未开启服务或连接服务");return@setOnClickListener
                }
                sendToServer(msg)
            }
        }
    
        private fun showEditDialog() {
            val dialogBinding =
                DialogEditIpBinding.inflate(LayoutInflater.from(this@ClientPlusActivity), null, false)
            AlertDialog.Builder(this@ClientPlusActivity).apply {
                setIcon(R.drawable.ic_connect)
                setTitle("连接Ip地址")
                setView(dialogBinding.root)
                setPositiveButton("确定") { dialog, _ ->
                    val ip = dialogBinding.etIpAddress.text.toString()
                    if (ip.isEmpty()) {
                        showMsg("请输入Ip地址");return@setPositiveButton
                    }
                    connectServer(ip)
                    dialog.dismiss()
                }
                setNegativeButton("取消") { dialog, _ -> dialog.dismiss() }
            }.show()
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    相比于服务端,客户端代码稍微多一点,不过也比较简单。最后我们修改一下AndroidManifest.xml,代码如下:

    
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        package="com.llw.socket">
    
        <uses-permission android:name="android.permission.INTERNET" />
        <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
        <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    
        <application
            android:name=".SocketApp"
            android:allowBackup="true"
            android:dataExtractionRules="@xml/data_extraction_rules"
            android:fullBackupContent="@xml/backup_rules"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/Theme.SocketDemo"
            tools:targetApi="31">
    
            <activity android:name=".ui.ServerPlusActivity"
                android:exported="false" />
            <activity android:name=".ui.ClientPlusActivity"
                android:exported="false" />
            <activity
                android:name=".ui.BaseSocketActivity"
                android:exported="false"
                android:windowSoftInputMode="adjustResize"/>
            <activity
                android:name=".ui.SelectTypeActivity"
                android:exported="true">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.LAUNCHER" />
                intent-filter>
            activity>
        application>
    
    manifest>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    最后修改一下SelectTypeActivity中的代码,点击时跳转到我们刚才所写的ServerPlusActivity和ClientPlusActivity,代码如下:

    class SelectTypeActivity : BaseActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_select_type)
    
            findViewById<Button>(R.id.btn_server).setOnClickListener {
                jumpActivity(ServerPlusActivity::class.java)
            }
    
            findViewById<Button>(R.id.btn_client).setOnClickListener {
                jumpActivity(ClientPlusActivity::class.java)
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    经过这么长时间的代码编写,我们也该来看看效果了。

    五、源码

    如果你觉得代码对你有帮助的话,不妨Fork或者Star一下~

    源码地址:SocketDemo

  • 相关阅读:
    alsa音频pcm设备之i2c调试
    基于操作系统讨论Java线程与进程、浅谈Go的线程与管程
    外贸万圣灯饰、南瓜灯具美国站UL588测试项目
    51单片机的简易篮球计分器倒计时仿真设计( proteus仿真+程序+原理图+报告+讲解视频)
    实验四 图像复原及几何校正
    信号——信号处理函数和掩码
    【Java基础】File类 IO流
    数据结构-----堆(完全二叉树)
    PHP对接企业微信创建审批应用和对接提交审批
    【Bitburner】Factions
  • 原文地址:https://blog.csdn.net/qq_38436214/article/details/126528583