安卓聊天工具开发(核心部分教程,附源代码)
主要步骤
新建一个空白的项目
- 打开viewBinding
- 添加依赖,其中以下部分是我手动添加的
implementation(“org.jetbrains.kotlin:kotlin-stdlib:1.8.20”) // 确保使用适合的 Kotlin 版本
implementation(“androidx.core:core-ktx:1.12.0”)
implementation(“androidx.appcompat:appcompat:1.7.0”)
implementation(“androidx.activity:activity-ktx:1.8.0”)
implementation(“androidx.recyclerview:recyclerview:1.2.0”)
implementation(“org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0”)
implementation(“org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0”) - 添加xml
JZBChat/JZBChatAndroid/app/src/main/res
添加文件夹layout,添加xml: activity_main.xml /item_message_received.xml/item_message_sent.xml
JZBChat/JZBChatAndroid/app/src/main/res/drawable 下添加:background_message_received.xml/background_message_sent.xml - 修改kotlin代码
JZBChat/JZBChatAndroid/app/src/main/java/com
修改 MainActivity.kt,添加 MessageAdapter.kt,Message.kt,TopClient.kt
5.编写服务器代码,并运行。 如果有公网IP就用公网的地址,也可以使用局域网。
主要遇到的问题:
1.socket创建不能在主线程,因此添加了一个子线程用来创建socket
2. binding.recyclerView语句一直报错,因为在xml中需要使用 <androidx.recyclerview.widget.RecyclerView />来创建, 一开始一直只用
3. 定义的Message类中text和isSentByUser的属性,在使用中写错变量名,导致一直报错
data class Message(val text: String, val isSentByUser: Boolean)
安卓代码部分
app/build.gradle.kts
build.gradle.kts
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
}
android {
namespace = "com.example.jzbchat"
compileSdk = 34
defaultConfig {
applicationId = "com.example.jzbchat"
minSdk = 29
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}
viewBinding {
enable = true
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.1"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}
dependencies {
//---------------------
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.8.20") // 确保使用适合的 Kotlin 版本
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.activity:activity-ktx:1.8.0")
implementation("androidx.recyclerview:recyclerview:1.2.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0")
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}
layout
JZBChat/JZBChatAndroid/app/src/main/res/layout
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_above="@+id/inputLayout"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_weight="1"/>
<LinearLayout
android:id="@+id/inputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:orientation="horizontal">
<EditText
android:id="@+id/editTextMessage"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Type a message"/>
<Button
android:id="@+id/buttonSend"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send"/>
</LinearLayout>
</RelativeLayout>
JZBChat/JZBChatAndroid/app/src/main/res/layout
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:padding="8dp"
android:background="@drawable/background_message_received">
<TextView
android:id="@+id/textViewMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black"/>
</LinearLayout>
JZBChat/JZBChatAndroid/app/src/main/res/layout/item_message_sent.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:padding="8dp"
android:background="@drawable/background_message_sent">
<TextView
android:id="@+id/textViewMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/white"/>
</LinearLayout>
JZBChat/JZBChatAndroid/app/src/main/res/drawable/background_message_received.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#4CAF50"/>
<corners android:radius="8dp"/>
</shape>
JZBChat/JZBChatAndroid/app/src/main/res/drawable/background_message_sent.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#99FF01"/>
<corners android:radius="8dp"/>
</shape>
JZBChat/JZBChatAndroid/app/src/main/java/com/example/jzbchat/MainActivity.kt
package com.example.jzbchat
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.jzbchat.databinding.ActivityMainBinding
import java.lang.Thread
import java.net.Socket
import android.util.Log
import android.os.Handler
import android.os.Looper
class MainActivity : ComponentActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var recyclerView: RecyclerView
private lateinit var editTextMessage: EditText
private lateinit var buttonSend: Button
private val messages = mutableListOf<Message>()
private lateinit var adapter: MessageAdapter
private var socket1: Socket? = null
private val tcpClient = TcpClient("115.29.204.91", 12345)
private lateinit var handler: Handler
private lateinit var socketTask: Runnable
private var socket: Socket? = null
private val INTERVAL_MS: Long = 10000 // 1秒
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Initialize ViewBinding
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Initialize UI components
recyclerView = binding.recyclerView
editTextMessage = binding.editTextMessage
buttonSend = binding.buttonSend
adapter = MessageAdapter(messages)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = adapter
//新增代码:
handler = Handler(Looper.getMainLooper())
socketTask = object : Runnable {
override fun run() {
ReceivedSocketData()
// handler.postDelayed(this, INTERVAL_MS)
}
}
handler.post(socketTask);
buttonSend.setOnClickListener {
val text = editTextMessage.text.toString()
if (text.isNotBlank()) {
sendMessage(text)
editTextMessage.text.clear()
}
}
// Start listening for incoming messages
}
private fun ReceivedSocketData() {
Thread {
try {
if (tcpClient.connect()) {
println("Connect success")
while (true) {
val message = tcpClient.receiveMessage()
if (message != null) {
runOnUiThread {
messages.add(Message(message, false))
adapter.notifyDataSetChanged()
recyclerView.scrollToPosition(messages.size - 1)
}
}
}
}
} catch (e: Exception) {
println("Connect fail")
runOnUiThread {
Toast.makeText(
this@MainActivity,
"Connection error: ${e.message}",
Toast.LENGTH_LONG
).show()
}
}
}.start()
}
private fun sendMessage(text: String) {
Thread {
try {
tcpClient.sendMessage(text)
runOnUiThread {
messages.add(Message(text, true))
adapter.notifyDataSetChanged()
recyclerView.scrollToPosition(messages.size - 1)
}
} catch (e: Exception) {
runOnUiThread {
Toast.makeText(this@MainActivity, "Send error: ${e.message}", Toast.LENGTH_LONG).show()
}
}
}.start()
}
override fun onDestroy() {
super.onDestroy()
tcpClient.close()
}
}
JZBChat/JZBChatAndroid/app/src/main/java/com/example/jzbchat/Message.kt
package com.example.jzbchat
data class Message(val text: String, val isSentByUser: Boolean)
JZBChat/JZBChatAndroid/app/src/main/java/com/example/jzbchat/MessageAdapter.kt
package com.example.jzbchat
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.jzbchat.databinding.ItemMessageSentBinding
import com.example.jzbchat.databinding.ItemMessageReceivedBinding
import androidx.viewbinding.ViewBinding
//data class Message(val text: String, val isSentByUser: Boolean)
class MessageAdapter(private val messages: List<Message>) : RecyclerView.Adapter<MessageAdapter.MessageViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
VIEW_TYPE_SENT -> {
val binding = ItemMessageSentBinding.inflate(inflater, parent, false)
SentMessageViewHolder(binding)
}
VIEW_TYPE_RECEIVED -> {
val binding = ItemMessageReceivedBinding.inflate(inflater, parent, false)
ReceivedMessageViewHolder(binding)
}
else -> throw IllegalArgumentException("Invalid view type")
}
}
override fun onBindViewHolder(holder: MessageViewHolder, position: Int) {
val message = messages[position]
holder.bind(message)
}
override fun getItemViewType(position: Int): Int {
return if (messages[position].isSentByUser) VIEW_TYPE_SENT else VIEW_TYPE_RECEIVED
}
override fun getItemCount() = messages.size
abstract class MessageViewHolder(binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) {
abstract fun bind(message: Message)
}
class SentMessageViewHolder(private val binding: ItemMessageSentBinding) : MessageViewHolder(binding) {
override fun bind(message: Message) {
binding.textViewMessage.text = message.text // Update here
}
}
class ReceivedMessageViewHolder(private val binding: ItemMessageReceivedBinding) : MessageViewHolder(binding) {
override fun bind(message: Message) {
binding.textViewMessage.text = message.text // Update here
}
}
companion object {
private const val VIEW_TYPE_SENT = 1
private const val VIEW_TYPE_RECEIVED = 2
}
}
JZBChat/JZBChatAndroid/app/src/main/java/com/example/jzbchat/TcpClient.kt
package com.example.jzbchat
import java.io.BufferedReader
import java.io.BufferedWriter
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.net.Socket
import android.util.Log
import java.net.InetAddress
class TcpClient(private val host: String, private val port: Int) {
private var socket: Socket? = null
private var writer: BufferedWriter? = null
private var reader: BufferedReader? = null
fun connect(): Boolean {
return try {
socket = Socket(InetAddress.getByName(host), port)
writer = BufferedWriter(OutputStreamWriter(socket!!.getOutputStream()))
reader = BufferedReader(InputStreamReader(socket!!.getInputStream()))
true
} catch (e: Exception) {
println("WANGJING CONNECT FAILED")
e.printStackTrace()
Log.e("TcpClient", "WANGJING Connection failed", e)
false
}
}
fun sendMessage(message: String) {
writer?.write(message)
writer?.newLine()
writer?.flush()
}
fun receiveMessage(): String? {
return reader?.readLine()
}
fun close() {
writer?.close()
reader?.close()
socket?.close()
}
}
服务器代码 PYTHON
这里设置了聊天室最大连接数为100,超过100个客户端后,新的客户端将收到“Server is full”的提示。
import socket
import threading
# 定义一个字典来存储客户端的连接
clients = {}
def handle_client(client_socket, client_address):
try:
while True:
message = client_socket.recv(1024).decode('utf-8')
if not message:
break
print(f"Received from {client_address}: {message}")
# 将消息广播给所有其他客户端
for other_client_socket in clients.values():
if other_client_socket != client_socket:
other_client_socket.send(message.encode('utf-8'))
except ConnectionResetError:
pass
print(f"Connection closed with {client_address}")
del clients[client_address]
client_socket.close()
def start_server():
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 设置 SO_REUSEADDR 选项
server_socket.bind(('0.0.0.0', 63311))
server_socket.listen(100) # 允许最多100个客户端连接
print("Server is listening...")
while True:
client_socket, client_address = server_socket.accept()
print(f"Connected to {client_address}")
# 如果已经有100个客户端连接,则拒绝新的连接
if len(clients) >= 100:
client_socket.send("Server is full".encode('utf-8'))
client_socket.close()
continue
clients[client_address] = client_socket
client_thread = threading.Thread(target=handle_client, args=(client_socket, client_address))
client_thread.start()
if __name__ == "__main__":
start_server()