安卓聊天工具开发(核心部分教程,附源代码)

安卓聊天工具开发(核心部分教程,附源代码)

主要步骤

新建一个空白的项目

  1. 打开viewBinding
  2. 添加依赖,其中以下部分是我手动添加的
    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”)
  3. 添加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
  4. 修改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()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值