前言
Socket通讯在许多地方都会用到,Android上相同不破例,Socket不是一种协议,而是一个编程调用接口(API),归于传输层,经过Socket,咱们才能在Andorid平台上经过 TCP/IP协议进行开发。先看看作用图:
正文
先阐明一下流程:
① 准备两台Android手机(真机)。 ② 衔接同一个WIFI网络 。 ③ 服务端敞开服务。 ④ 客户端衔接服务。 ⑤ 服务端与客户端进行音讯发送接纳。
那么依据这个流程咱们开端写代码。
一、创立项目
创立一个名为SocketDemo的项目,使用Kotlin。
<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"/>
然后再装备一下app的build.gradle,在android{}闭包下添加:
buildFeatures {
viewBinding true
}
这儿敞开项目的viewBinding,其他的就没啥好装备的了,进入正式的编码环节。
二、构建主页面布局
创立项目会默许有一个MainActivity,这个页面既是服务端,又是客户端。修正一下activity_main.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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<RadioGroup
android:id="@+id/rg"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<RadioButton
android:id="@+id/rb_server"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:checked="true"
android:text="Socket服务端" />
<RadioButton
android:id="@+id/rb_client"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Socket客户端" />
</RadioGroup>
<LinearLayout
android:id="@+id/lay_server"
android:layout_width="match_parent"
android:layout_height="110dp"
android:orientation="vertical">
<TextView
android:id="@+id/tv_ip_address"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="Ip地址:" />
<Button
android:id="@+id/btn_start_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="敞开服务"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/radioGroup" />
</LinearLayout>
<LinearLayout
android:id="@+id/lay_client"
android:layout_width="match_parent"
android:layout_height="110dp"
android:orientation="vertical"
android:visibility="gone">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/op_code_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_ip_address"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="衔接Ip地址"
android:inputType="text"
android:lines="1"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btn_connect_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="衔接服务"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btn_start_service" />
</LinearLayout>
<TextView
android:id="@+id/tv_info"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="16dp"
android:text="信息" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/et_msg"
android:layout_width="0dp"
android:gravity="center_vertical"
android:layout_height="40dp"
android:hint="发送给客户端"
android:textSize="14sp"
android:layout_weight="1"
android:background="@drawable/shape_et_bg"
android:padding="10dp" />
<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>
</LinearLayout>
这儿面有一个输入框的布景款式,在drawable下新增shape_et_bg.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="20dp" />
<solid android:color="#EEE" />
</shape>
阐明一下页面的内容,首要是RadioButton切换服务端和客户端,服务端则显现当时手机的IP地址和敞开服务按钮,客户端则显现一个输入框,和衔接服务按钮。中间首要内容便是服务端和客户端交互的信息,底部是一个输入框和发送音讯按钮。预览的作用如图所示:
三、服务端
在com.llw.socket包下新建一个server包,咱们服务端的代码就写在这个server包下。新建一个ServerCallback接口,代码如下:
interface ServerCallback {
//接纳客户端的音讯
fun receiveClientMsg(success: Boolean, msg: String)
//其他音讯
fun otherMsg(msg: String)
}
下面便是首要的服务端代码了,在server包下新建一个SocketServer类,代码如下:
object SocketServer {
private val TAG = SocketServer::class.java.simpleName
private const val SOCKET_PORT = 9527
private var socket: Socket? = null
private var serverSocket: ServerSocket? = null
private lateinit var mCallback: ServerCallback
private lateinit var outputStream: OutputStream
var result = true
/**
* 敞开服务
*/
fun startServer(callback: ServerCallback): Boolean {
mCallback = callback
Thread {
try {
serverSocket = ServerSocket(SOCKET_PORT)
while (result) {
socket = serverSocket?.accept()
mCallback.otherMsg("${socket?.inetAddress} to connected")
ServerThread(socket!!, mCallback).start()
}
} catch (e: IOException) {
e.printStackTrace()
result = false
}
}.start()
return result
}
/**
* 封闭服务
*/
fun stopServer() {
socket?.apply {
shutdownInput()
shutdownOutput()
close()
}
serverSocket?.close()
}
/**
* 发送到客户端
*/
fun sendToClient(msg: String) {
Thread {
if (socket!!.isClosed) {
Log.e(TAG, "sendToClient: Socket is closed")
return@Thread
}
outputStream = socket!!.getOutputStream()
try {
outputStream.write(msg.toByteArray())
outputStream.flush()
mCallback.otherMsg("toClient: $msg")
Log.d(TAG, "发送到客户端成功")
} catch (e: IOException) {
e.printStackTrace()
Log.e(TAG, "向客户端发送音讯失利")
}
}.start()
}
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) {
callback.receiveClientMsg(true, receiveStr)
receiveStr = ""
}
}
} catch (e: IOException) {
e.printStackTrace()
e.message?.let { Log.e("socket error", it) }
callback.receiveClientMsg(false, "")
}
}
}
}
代码从上往下看,首要是初始化一些变量,然后便是startServer()函数,在这儿进行回调接口的初始化然后开一个子线程进行ServerSocket的构建,构建成功之后会监听衔接,得到一个socket,这个socket便是客户端,这儿将衔接客户端的地址显现出来。然后再敞开一个子线程去处理客户端发送过来的音讯。这个地方服务端和客户端差不多,下面看ServerThread中的代码。Socket通讯,发送和接纳对应的是输入流和输入流,经过socket.getInputStream()得到输入流,获取字节数据然后转成String,经过接口回调,最终重置变量。封闭服务就没好说的,代码一望而知。最终便是发送到客户端的sendToClient()函数。接纳发送字符串,敞开子线程,获取输出流,写入字节数据然后刷新,最终回调到页面。
四、客户端
在com.llw.socket包下新建一个client包,咱们客户端的代码就写在这个client包下。新建一个ClientCallback接口,代码如下:
interface ClientCallback {
//接纳服务端的音讯
fun receiveServerMsg(msg: String)
//其他音讯
fun otherMsg(msg: String)
}
下面便是首要的客户端代码了,在client包下新建一个SocketClient类,代码如下:
object SocketClient {
private val TAG = SocketClient::class.java.simpleName
private var socket: Socket? = null
private var outputStream: OutputStream? = null
private var inputStreamReader: InputStreamReader? = null
private lateinit var mCallback: ClientCallback
private const val SOCKET_PORT = 9527
/**
* 衔接服务
*/
fun connectServer(ipAddress: String, callback: ClientCallback) {
mCallback = callback
Thread {
try {
socket = Socket(ipAddress, SOCKET_PORT)
ClientThread(socket!!, mCallback).start()
} catch (e: IOException) {
e.printStackTrace()
}
}.start()
}
/**
* 封闭衔接
*/
fun closeConnect() {
inputStreamReader?.close()
outputStream?.close()
socket?.apply {
shutdownInput()
shutdownOutput()
close()
}
Log.d(TAG, "封闭衔接")
}
/**
* 发送数据至服务器
* @param msg 要发送至服务器的字符串
*/
fun sendToServer(msg: String) {
Thread {
if (socket!!.isClosed) {
Log.e(TAG, "sendToServer: Socket is closed")
return@Thread
}
outputStream = socket?.getOutputStream()
try {
outputStream?.write(msg.toByteArray())
outputStream?.flush()
mCallback.otherMsg("toServer: $msg")
} catch (e: IOException) {
e.printStackTrace()
Log.e(TAG, "向服务端发送音讯失利")
}
}.start()
}
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) {
callback.receiveServerMsg(receiveStr)
receiveStr = ""
}
}
} catch (e: IOException) {
e.printStackTrace()
e.message?.let { Log.e("socket error", it) }
callback.receiveServerMsg( "")
}
}
}
}
客户端的代码和服务端其实很相似,这儿我就简单阐明一下,首要便是衔接服务,需求输入服务端的ip地址,端口号则是写死的一个端口号,也能够动态去设置。其他的地方和服务端相似。
五、业务交互
现在核心功用代码都写好了,下面怎么样让这些功用和页面串起来,这儿由于涉及到用户交互所以会阐明的多一点。
① 接口回调
还记得之前的ServerCallback和ClientCallback吗?这两个回调接口由于咱们是服务端和客户端在一起的,所以在同一个Activity中去完结接口。
然后完结接口中的办法,在MainActivity中新增如下代码:
override fun receiveClientMsg(success: Boolean, msg: String) {
}
override fun otherMsg(msg: String) {
}
override fun receiveServerMsg(msg: String) {
}
这儿的otherMsg()函数是服务端和客户端共用,由于函数名参数都共同,能够自行修正为不共用。这些函数里面后边会写代码,目前先不论,先完结页面的业务逻辑。
② 服务端和客户端切换
服务端和客户端的切换是会影响整个页面的,首要在MainActivity中定义变量,如下所示:
private val TAG = MainActivity::class.java.simpleName
private lateinit var binding: ActivityMainBinding
private val buffer = StringBuffer()
//当时是否为服务端
private var isServer = true
//Socket服务是否翻开
private var openSocket = false
//Socket服务是否衔接
private var connectSocket = false
然后修正一下onCreate()函数,代码如下:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
initView()
}
这儿便是完结ViewBinding,然后创立了一个initView()函数,代码如下:
private fun initView() {
binding.tvIpAddress.text = "Ip地址:${getIp()}"
//服务端和客户端切换
binding.rg.setOnCheckedChangeListener { _, checkedId ->
}
//敞开服务/封闭服务 服务端处理
binding.btnStartService.setOnClickListener {
}
//衔接服务/断开衔接 客户端处理
binding.btnConnectService.setOnClickListener {
}
//发送音讯 给 服务端/客户端
binding.btnSendMsg.setOnClickListener {
}
}
在initView()函数中,首要要做的便是显现当时的Ip地址,不论你是服务端仍是客户端,我都会获取Ip地址,由于在你切换时并不会重新获取Ip地址,这儿有一个getIp()函数,代码如下:
private fun getIp() =
intToIp((applicationContext.getSystemService(WIFI_SERVICE) as WifiManager).connectionInfo.ipAddress)
private fun intToIp(ip: Int) =
"${(ip and 0xFF)}.${(ip shr 8 and 0xFF)}.${(ip shr 16 and 0xFF)}.${(ip shr 24 and 0xFF)}"
这儿的WIFI_SERVICE就对应之前在AndroidManifest.xml中装备的WIFI状况读取权限。下面咱们完结服务端和客户端切换对UI上的改动。代码如下:
binding.rg.setOnCheckedChangeListener { _, checkedId ->
isServer = when (checkedId) {
R.id.rb_server -> true
R.id.rb_client -> false
else -> true
}
binding.layServer.visibility = if (isServer) View.VISIBLE else View.GONE
binding.layClient.visibility = if (isServer) View.GONE else View.VISIBLE
binding.etMsg.hint = if (isServer) "发送给客户端" else "发送给服务端"
}
这儿在对RadioGroup进行选中改动监听,点击RadioButton获取id设置是否为服务端,便是改动isServer的值,默许是服务端。然后便是依据isServer去设置服务端布局和客户端布局的显现状况,一起还需求设置底部输入框的提示文字。
③ 服务敞开和封闭
如果当时是服务端,则会看到敞开服务按钮,点击按钮的代码如下:
binding.btnStartService.setOnClickListener {
openSocket = if (openSocket) {
SocketServer.stopServer();false
} else SocketServer.startServer(this)
//显现日志
showInfo(if (openSocket) "敞开服务" else "封闭服务")
//改动按钮文字
binding.btnStartService.text = if (openSocket) "封闭服务" else "敞开服务"
}
这儿依据当时是否敞开服务条件去控制是敞开服务仍是封闭服务,还有一些不严谨,再往下便是一个显现日志的办法和修正按钮显现文字,这儿便是页面中部的那个TextView。showInfo()函数代码很简单,如下所示:
private fun showInfo(info: String) {
buffer.append(info).append("\n")
runOnUiThread { binding.tvInfo.text = buffer.toString() }
}
便是字符串拼接,然后显现出来。
④ 服务衔接和断开
如果当时是客户端,则会看到衔接服务按钮,点击按钮的代码如下:
binding.btnConnectService.setOnClickListener {
val ip = binding.etIpAddress.text.toString()
if (ip.isEmpty()) {
showMsg("请输入Ip地址");return@setOnClickListener
}
connectSocket = if (connectSocket) {
SocketClient.closeConnect();false
} else {
SocketClient.connectServer(ip, this);true
}
showInfo(if (connectSocket) "衔接服务" else "封闭衔接")
binding.btnConnectService.text = if (connectSocket) "封闭衔接" else "衔接服务"
}
这儿会先查看是否输入IP地址,没有就会提示一下,showMsg()函数代码如下:
private fun showMsg(msg: String) = Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
这儿还缺少一步查看ip地址是否合规,我就先不做了。ip地址有了地址就会依据connectSocket状况得知当时点击按钮时履行衔接仍是断开。最终相一起显现日志和修正按钮文字。
⑤ 发送音讯
终于到了底部的发送音讯处理了,点击按钮的代码如下:
binding.btnSendMsg.setOnClickListener {
val msg = binding.etMsg.text.toString()
if (msg.isEmpty()) {
showMsg("请输入要发送的信息");return@setOnClickListener
}
//查看是否能发送音讯
val isSend = if (openSocket) openSocket else if (connectSocket) connectSocket else false
if (!isSend) {
showMsg("当时未敞开服务或衔接服务");return@setOnClickListener
}
if (isServer) SocketServer.sendToClient(msg) else SocketClient.sendToServer(msg)
binding.etMsg.setText("")
}
查看是否有音讯输入,然后是依据当时是否为服务端进行音讯发送,发送后清空输入框。
⑥ 显现音讯内容
在服务端和客户端衔接之后,服务端发送音讯之后,客户端收到,客户端发送音讯之后,服务端收到。在①中咱们完结了接口,现在只要将接口回来的音讯显现出来就行了。
override fun receiveClientMsg(success: Boolean, msg: String) {
showInfo("ClientMsg: $msg")
}
override fun otherMsg(msg: String) {
showInfo(msg)
}
override fun receiveServerMsg(msg: String) {
showInfo("ServerMsg: $msg")
}
那么现在所有的代码都写完了,由于页面的底部是一个输入框,当点击之后会弹出软键盘,此时页面会被顶上去,为了避免这样的问题,修正修正一下咱们运转看看作用。
六、UI优化
已然现在音讯通讯现已能够了,那么咱们可不能够做成相似聊天的UI风格呢?当然能够。首要要改动一下UI,先把activity_main.xml中id为tv_info的控件控件删掉,换成RecyclerView,代码如下:
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_msg"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
现在MainActivity中肯定会有报错,不过咱们先不论它,先写列表适配器的代码。
① 列表适配器
做适配器的话要考虑服务端和客户端的联系,因此和传统的聊天是有差异的。首要在layout下创立一个item_rv_msg.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:orientation="horizontal"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/iv_server"
android:layout_width="60dp"
android:layout_height="60dp"
android:src="@drawable/icon_server"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/circleImageStyle" />
<TextView
android:id="@+id/tv_server_msg"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_weight="1"
android:background="@drawable/shape_left_msg_bg"
android:text="123"
android:textColor="@color/black"
app:layout_constraintStart_toEndOf="@+id/iv_server"
app:layout_constraintTop_toTopOf="@+id/iv_server" />
<TextView
android:id="@+id/tv_client_msg"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:background="@drawable/shape_right_msg_bg"
android:text="123"
android:textColor="@color/white"
app:layout_constraintEnd_toStartOf="@+id/iv_client"
app:layout_constraintTop_toTopOf="@+id/iv_client" />
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/iv_client"
android:layout_width="60dp"
android:layout_height="60dp"
android:src="@drawable/icon_client"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/circleImageStyle" />
</androidx.constraintlayout.widget.ConstraintLayout>
这儿用到了两个头像图片。
能够直接去我源码里面拿,一起为了设置圆形头像,我在themes.xml中添加了一个款式,代码如下:
<style name="circleImageStyle">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">50%</item>
</style>
然后便是音讯的布景款式了,由于是要区别服务端和客户端的,服务端在左,客户端在右。在drawable中新增shape_left_msg_bg.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#EEE" />
<corners
android:bottomLeftRadius="16dp"
android:bottomRightRadius="16dp"
android:topRightRadius="16dp" />
<padding
android:bottom="16dp"
android:left="16dp"
android:right="16dp"
android:top="16dp" />
</shape>
shape_right_msg_bg.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#4177F6" />
<corners
android:bottomLeftRadius="16dp"
android:bottomRightRadius="16dp"
android:topLeftRadius="16dp" />
<padding
android:bottom="16dp"
android:left="16dp"
android:right="16dp"
android:top="16dp" />
</shape>
适配器是需求数据的,由于咱们在com.llw.socket包下新增一个数据类,代码如下:
data class Message(val type:Int, val msg:String)
最终咱们写一个适配器,在com.llw.socket包下新增一个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.type == 1) {
holder.mView.tvServerMsg.text = message.msg
} else {
holder.mView.tvClientMsg.text = message.msg
}
holder.mView.ivServer.visibility = if (message.type == 1) View.VISIBLE else View.INVISIBLE
holder.mView.ivClient.visibility = if (message.type == 1) View.INVISIBLE else View.VISIBLE
holder.mView.tvServerMsg.visibility = if (message.type == 1) View.VISIBLE else View.GONE
holder.mView.tvClientMsg.visibility = if (message.type == 1) View.GONE else View.VISIBLE
}
override fun getItemCount() = messages.size
class ViewHolder(itemView: ItemRvMsgBinding) : RecyclerView.ViewHolder(itemView.root) {
var mView: ItemRvMsgBinding
init {
mView = itemView
}
}
}
这儿便是RecyclerView+ViewBinding的使用方式。依据不同的音讯类型设置控件状况就能够了。
② 修正页面逻辑
首要要将适配器和RV绑定起来,在MainActivity中新增如下代码:
//音讯列表
private val messages = ArrayList<Message>()
//音讯适配器
private lateinit var msgAdapter: MsgAdapter
然后在initView()函数中初始化,代码如下:
//初始化列表
msgAdapter = MsgAdapter(messages)
binding.rvMsg.apply {
layoutManager = LinearLayoutManager(this@MainActivity)
adapter = msgAdapter
}
代码添加方位如下图所示:
然后在MainActivity中新增一个updateList()函数,代码如下:
/**
* 更新列表
*/
private fun updateList(type: Int, msg: String) {
messages.add(Message(type, msg))
runOnUiThread {
(if (messages.size == 0) 0 else messages.size - 1).apply {
msgAdapter.notifyItemChanged(this)
binding.rvMsg.smoothScrollToPosition(this)
}
}
}
添加数据到列表,然后刷新方位,滑动到新增数据方位,将showInfo()函数代码先去掉,然后将btnStartService和btnConnectService按钮的点击事情中的showInfo修正为showMsg。
然后修正一下这三个回调函数,代码如下:
override fun receiveClientMsg(success: Boolean, msg: String) = updateList(2, msg)
override fun receiveServerMsg(msg: String) = updateList(1, msg)
override fun otherMsg(msg: String) {
Log.d(TAG, msg)
}
之前都是调用的showInfo()函数,现在都改了,为了让咱们发送音讯也能更新列表,在btnSendMsg按钮点击事情中,最终一行添加如下代码:
updateList(if (isServer) 1 else 2, msg)
那么现在代码就写完了,看看运转的作用。
七、源码
如果你觉得代码对你有帮助的话,不妨Fork或许Star一下~
源码地址:SocketDemo