Android Studio智能聊天机器人课设:从界面到服务端的完整实现
简介这是一个基于Android Studio开发的智能聊天机器人完整项目专为安卓课程设计或初学者准备。项目综合运用SQLite数据库、HttpURLConnection网络请求、ListView列表展示、GSON解析及SharedPreferences本地存储等技术实现了欢迎页自动跳转、账号注册校验、登录记住密码、主界面随机欢迎语及机器人智能回复等完整功能链路。压缩包共包含624个文件以Java源码、XML布局文件、Gradle配置、JSON资源、dex打包文件及调试APK为主整体大小约17.2MB项目结构完整规范可直接导入Android Studio运行或二次开发。该资源已吸引1699人学习下载是快速掌握安卓综合开发、完成高分课设的实用参考。从界面布局到后台逻辑再到网络通信代码组织清晰适合小白逐步理解数据库操作与网络交互的完整实现。1. 课设标题里的“智能聊天机器人”先想清楚要答辩什么用 Android Studio 实现智能聊天机器人几乎是安卓课设里出现频率最高的题目。但大部分人的第一个版本都做成了“假聊天机器人”界面上只有一个输入框和一个按钮点发送后回复一句写死的话被老师一问就露馅。真正能拿高分的课设至少要讲清楚三件事消息怎么从界面走到后台、机器人凭什么决定回什么、网络断开时系统会不会崩。这篇笔记按这个思路给出一套用 Android Studio 从零搭起的聊天机器人最小方案覆盖界面、适配器、HTTP 请求、服务端接口和答辩现场的高频翻车点。适合第一次独立做安卓课设的新手也适合想快速摸清实现边界的老手。2. 聊天机器人的技术方案怎么选别一上来就上大模型课设题目里带“智能”两个字不代表一定要接大模型。Android Studio 做 AI 开发的门槛从来不在 IDE而在模型和接口的可控性。如果你一上来就打算接一个第三方大模型 API先要面对几个现实问题密钥要不要钱、答辩现场的校园网能不能连上外网、老师追问“模型输入输出格式是什么”你能不能答上来。对多数课设场景来说稳定的得分点不是模型多聪明而是你的代码结构完整、交互流程清楚、每一个环节都能现场演示。2.1 三条实现路线各自适配什么样的课设场景常见的实现方式可以归纳成三类。第一类是纯本地规则用 if/else 或者关键词表匹配用户输入返回预先写好的回复。优点是完全不依赖网络演示永远不会因为断网翻车缺点是对老师的冲击力有限太像大作业里的“玩具”。第二类是接入第三方问答 API拿别人封装好的聊天接口安卓端只负责发请求、解析返回。优点是回复内容比本地规则丰富缺点是大部分免费接口有调用次数限制而且聊天机器人真正的决策逻辑在黑匣子里答辩被追问时很难展开讲。第三类是自建服务端在电脑或者云服务器上跑一个轻量 HTTP 服务安卓端把用户消息 POST 过去服务端做完关键词匹配或意图判断后返回 JSON。它比纯本地规则多了一层网络通信比接第三方 API 多了一段你能完整讲清楚的业务逻辑是最适合作为课设展示的方案。下面用一张表把这三种路线放在一起对比方案实现成本网络依赖答辩讲解难度课设适配度纯本地 if/else低无内容太薄容易被追问一般第三方问答 API低强依赖外网决策逻辑不可见容易露怯不建议自建服务端中仅局域网每个环节都能讲透推荐2.2 推荐组合本地关键词优先服务端接口兜底我一般推荐“服务端为主、本地规则兜底”的混合结构。也就是说安卓端先把用户消息发给自建服务端服务端匹配关键词并返回回复如果网络不通安卓端本地再套一层最简单的关键词表保证界面不会因为请求失败就卡死。这个设计的核心价值在于课设演示时可以自信地说“我的对话逻辑在服务端但客户端有容错”这句话本身就是答辩加分项。从原理上说聊天机器人在课设这个规模下不需要 NLP。你只需要把“输入文本到输出文本”拆成两步意图识别和回复生成。意图识别用关键词包含判断就够了比如用户消息里含“天气”就触发天气分支含“名字”就触发自我介绍分支回复生成则是每个分支对应一条回复模板。这个流程用伪代码描述就是如果 消息里包含 天气: 返回 我现在查不了实时天气但你可以试试问我你是谁、讲个笑话 如果 消息里包含 笑话: 返回 程序员最讨厌的两件事一是写文档二是别人不写文档 否则: 返回 这个问题超出我的知识范围了换个问法试试这种做法的优点很明显每一行逻辑都能对着代码讲清楚不会在答辩时被“这个回复是怎么来的”问住。缺点也真实存在——回复内容不够丰富但只要你有意识地把关键词表扩展得足够多比如覆盖问候、自我介绍、讲笑话、报时间、简单计算课设评分已经完全够用。2.3 先定数据契约消息格式统一成 JSON动手写代码之前建议先把安卓端和服务端之间的数据格式定下来。最省事的约定是安卓端发送一条 JSON 给服务端字段叫message服务端返回一条 JSON字段叫reply。这样两端可以并行开发你甚至可以先写好服务端用浏览器或命令行工具测通了再做安卓端界面。{message: 你好}{reply: 你好呀我是课设机器人找我有什么事}字段越少越不容易出错。别在这个阶段引入code、data、session_id这类复杂结构课设最大的敌人是把简单事情搞复杂。保持一致的数据契约后后面写网络层、写适配器都会非常顺。3. 用 Android Studio 搭出聊天界面布局文件、RecyclerView 适配器与最小可跑工程界面是课设的门面但也是最容易写成一团乱麻的部分。很多人一开始就去找别人现成的聊天界面源码结果复制进来以后标题栏、边距、气泡风格全都对不上改起来比从零写还痛苦。聊天界面的核心其实就是三个东西一个用于显示消息列表的 RecyclerView、一个用于输入文本的 EditText、一个用于发送的 Button其余都是在这个骨架上加装饰。3.1 新建工程时的三个设置决定后面要不要返工如果你还在 Android Studio 的安装配置阶段先把 SDK Manager 里 Android 10 或更高版本的 SDK 装上模拟器能正常开机再动手写代码。新建工程时模板选Empty Views Activity不要选带 Compose 的模板因为课设最常见的要求是“看得懂 XML 布局能解释生命周期”Compose 对新手来说答辩时很难讲透。语言方面我一般建议跟课设要求走没要求就选 Java参考资料最多老师也最容易看懂。第三个关键设置是 Minimum SDK。建议选API 24或API 26覆盖了绝大多数真机和模拟器。如果你把最低版本压到 API 21要考虑低版本机型对纯明文 HTTP 的支持差异反而给自己挖坑。项目建好后先确认 Gradle 同步通过再往app/build.gradle的dependencies里加 RecyclerView 依赖dependencies { implementation androidx.recyclerview:recyclerview:1.2.1 }加完依赖点一下 Sync等右下角进度条走完。这一步没有坑但如果不加依赖后面写布局时你会到处看到androidx.recyclerview.widget.RecyclerView报红。3.2 布局文件聊天窗口和消息气泡一次写明白主界面布局用垂直方向的 LinearLayout从上到下排列标题栏、RecyclerView、输入栏。RecyclerView 要给它一个layout_weight1这样输入栏始终贴着屏幕底部消息多的时候列表自动压缩。这是聊天界面最稳妥的布局方式比用 ScrollView 包 RecyclerView 少掉一堆嵌套滚动冲突。?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical androidx.recyclerview.widget.RecyclerView android:idid/recyclerView android:layout_widthmatch_parent android:layout_height0dp android:layout_weight1 android:clipToPaddingfalse android:padding8dp / LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal android:padding6dp EditText android:idid/editInput android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:hint说点什么… android:maxLines3 / Button android:idid/btnSend android:layout_widthwrap_content android:layout_heightwrap_content android:text发送 / /LinearLayout /LinearLayoutclipToPaddingfalse是为了让最后一条消息滚到最底部时仍保留 8dp 内边距不会紧贴列表边框。输入框的maxLines3防止用户写长文本时把按钮挤到屏幕外。每个消息条目的布局单独放在item_chat.xml里用 FrameLayout 同时放左右两个 TextView谁可见谁展示?xml version1.0 encodingutf-8? FrameLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightwrap_content android:padding6dp TextView android:idid/tvLeft android:layout_widthwrap_content android:layout_heightwrap_content android:layout_gravitystart android:background#EEEEEE android:padding10dp android:textColor#222222 android:visibilitygone / TextView android:idid/tvRight android:layout_widthwrap_content android:layout_heightwrap_content android:layout_gravityend android:background#D6E4FF android:padding10dp android:textColor#222222 android:visibilitygone / /FrameLayout左边是机器人气泡灰色底右边是自己发送的气泡浅蓝色底。FrameLayout 的特性是子 View 重叠摆放所以两个 TextView 即使都设置了可见性也只会显示当前激活的那一个。3.3 消息实体类与 RecyclerView 适配器左右气泡的切换逻辑先定义一个消息实体类只保留两个字段消息内容和是否来自自己。不要在这个类里塞时间、头像、状态等暂时用不上的属性够用就行。public class ChatMessage { private String content; private boolean isFromMe; public ChatMessage(String content, boolean isFromMe) { this.content content; this.isFromMe isFromMe; } public String getContent() { return content; } public boolean isFromMe() { return isFromMe; } }适配器负责把ChatMessage列表渲染成对应的气泡。关键点在onBindViewHolder里手动切换tvLeft和tvRight的可见性public class ChatAdapter extends RecyclerView.AdapterChatAdapter.ViewHolder { private ListChatMessage list; private LayoutInflater inflater; public ChatAdapter(Context context, ListChatMessage list) { this.list list; this.inflater LayoutInflater.from(context); } NonNull Override public ViewHolder onCreateViewHolder(NonNull ViewGroup parent, int viewType) { View view inflater.inflate(R.layout.item_chat, parent, false); return new ViewHolder(view); } Override public void onBindViewHolder(NonNull ViewHolder holder, int position) { ChatMessage msg list.get(position); if (msg.isFromMe()) { holder.tvLeft.setVisibility(View.GONE); holder.tvRight.setVisibility(View.VISIBLE); holder.tvRight.setText(msg.getContent()); } else { holder.tvRight.setVisibility(View.GONE); holder.tvLeft.setVisibility(View.VISIBLE); holder.tvLeft.setText(msg.getContent()); } } Override public int getItemCount() { return list.size(); } static class ViewHolder extends RecyclerView.ViewHolder { TextView tvLeft, tvRight; ViewHolder(View itemView) { super(itemView); tvLeft itemView.findViewById(R.id.tvLeft); tvRight itemView.findViewById(R.id.tvRight); } } }这段代码的核心逻辑就是可见性控制自己的消息只显示右侧气泡机器人的消息只显示左侧气泡。如果你在迭代中遇到两个气泡同时出现或者布局错位几乎可以肯定是两个 TextView 的可见性没有配对切好检查onBindViewHolder里两个 setVisibility 是否成对出现。3.4 主界面代码把输入框和列表接起来主界面只做三件事初始化 RecyclerView、设置发送按钮监听、把新消息插入列表。为了先让界面跑起来我先写一个只处理“你好”“你是谁”“笑话”的本地回复方法占位用下一章再替换成真正的网络请求。public class MainActivity extends AppCompatActivity { private RecyclerView recyclerView; private EditText editInput; private ChatAdapter adapter; private ListChatMessage messageList new ArrayList(); Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); recyclerView findViewById(R.id.recyclerView); editInput findViewById(R.id.editInput); LinearLayoutManager layoutManager new LinearLayoutManager(this); recyclerView.setLayoutManager(layoutManager); adapter new ChatAdapter(this, messageList); recyclerView.setAdapter(adapter); findViewById(R.id.btnSend).setOnClickListener(v - sendMessage()); } private void sendMessage() { String content editInput.getText().toString().trim(); if (content.isEmpty()) return; messageList.add(new ChatMessage(content, true)); adapter.notifyItemInserted(messageList.size() - 1); recyclerView.scrollToPosition(messageList.size() - 1); editInput.setText(); String reply localReply(content); messageList.add(new ChatMessage(reply, false)); adapter.notifyItemInserted(messageList.size() - 1); recyclerView.scrollToPosition(messageList.size() - 1); } private String localReply(String msg) { if (msg.contains(你好)) return 你好呀我是课设机器人; if (msg.contains(名字)) return 我叫小安是这个安卓课设的对话机器人。; if (msg.contains(笑话)) return 程序员最讨厌的两件事一是写文档二是别人不写文档。; return 你可以问我你好、你叫什么、讲个笑话。; } }这段代码里有几个细节要留意。notifyItemInserted比notifyDataSetChanged好它只刷新新增的那一条列表不会整个闪烁。scrollToPosition必须在notifyItemInserted之后调用而且传入的是最新一条消息的索引。输入框内容用trim()去掉首尾空格避免发出一条空消息。这个版本跑起来以后你已经有一个能对话的聊天界面了接下来把本地回复换掉接上真正的网络层。4. 让机器人真正开口Flask 服务端、HttpURLConnection 与回复决策界面能跑通之后下一步就是把“假回复”换成“真回复”。这里的“真”不是指模型多聪明而是指消息从安卓端发出去经过网络传输到服务端服务端做出决策再返回整个过程完整可控。课设答辩时老师看重的正是这条链路是否完整、你是否能讲清楚每一跳。4.1 服务端选型本地电脑跑 Flask 就够了常见做法是在电脑上装 Python用 Flask 写一个极简 HTTP 接口。这是最容易复现、跨平台问题最少的方式。你需要准备的只有 Python 环境和一个 Flask 安装命令pip install flask服务端要跑在0.0.0.0而不是127.0.0.1这点很关键。127.0.0.1意味着只能本机访问模拟器里的安卓应用会被拒绝监听0.0.0.0才能让同一局域网里的手机和模拟器都访问到。端口可以选 8080避开常用的 80、443也少一点权限问题。from flask import Flask, request, jsonify app Flask(__name__) REPLIES { 你好: 你好呀我是课设机器人有什么可以帮你, 名字: 我叫小安是运行在安卓端 Flask 服务端的对话机器人。, 笑话: 程序员最讨厌的两件事一是写文档二是别人不写文档。, 天气: 我现在还查不了实时天气但你可以试着问我你好、你叫什么、讲个笑话。, } app.route(/chat, methods[POST]) def chat(): data request.get_json() msg data.get(message, ) for key in REPLIES: if key in msg: return jsonify({reply: REPLIES[key]}) return jsonify({reply: 这个问题有点难你可以换个说法再问我一次。}) if __name__ __main__: app.run(host0.0.0.0, port8080)这段服务端代码的核心是REPLIES字典的遍历顺序。Python 3.7 以后字典保持插入顺序所以“你好”“名字”这些更具体的词会先被匹配到。把“天气”放在后面是避免用户说“明天天气怎么样”时因为包含“天”字就误命中别的规则。注意data.get(message, )当请求体缺少message字段时不会抛异常而是返回兜底回复。写完后在终端跑python app.py看到Running on http://0.0.0.0:8080输出服务端就绪。先别急着写安卓代码用 curl 验证一下接口curl -X POST http://127.0.0.1:8080/chat \ -H Content-Type: application/json \ -d {message: 你好}如果返回{reply:你好呀我是课设机器人有什么可以帮你}说明服务和数据契约都没问题接下来只差安卓端把消息送出去。4.2 安卓端网络层用 HttpURLConnection 发 POST 请求安卓端发 HTTP 请求有两种主流写法OkHttp 和 HttpURLConnection。课设场景我建议用 HttpURLConnection原因很实际不用额外引第三方库Android API 自带代码短排错时能看到每一行到底做了什么。下面写一个ChatBotClient工具类负责把消息发给服务端并解析返回的reply字段。public class ChatBotClient { // 模拟器访问宿主机用 10.0.2.2真机改成电脑局域网 IP private static final String URL http://10.0.2.2:8080/chat; public static String sendMessage(String question) throws IOException { HttpURLConnection conn null; try { URL url new URL(URL); conn (HttpURLConnection) url.openConnection(); conn.setRequestMethod(POST); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); conn.setRequestProperty(Content-Type, application/json;charsetUTF-8); conn.setDoOutput(true); JSONObject body new JSONObject(); try { body.put(message, question); } catch (JSONException e) { throw new RuntimeException(e); } OutputStream os conn.getOutputStream(); os.write(body.toString().getBytes(UTF-8)); os.flush(); os.close(); if (conn.getResponseCode() 200) { InputStream is conn.getInputStream(); BufferedReader reader new BufferedReader(new InputStreamReader(is, UTF-8)); StringBuilder sb new StringBuilder(); String line; while ((line reader.readLine()) ! null) { sb.append(line); } reader.close(); JSONObject result new JSONObject(sb.toString()); return result.getString(reply); } return 服务端开小差了稍后再试。; } catch (JSONException e) { throw new RuntimeException(e); } finally { if (conn ! null) conn.disconnect(); } } }几个参数值得说清楚。setConnectTimeout(5000)是建立连接的超时时间单位毫秒5 秒对局域网环境足够如果你在实训室弱网下演示可以调到 8000太短容易误判为失败太长会让界面长时间无响应。setReadTimeout(5000)是等待服务端返回数据的超时时间服务端逻辑很轻5 秒足够。charsetUTF-8是请求体的编码声明中文消息能不能被正确解析就看这里。getResponseCode() 200是判断服务端是否正常返回的通用做法如果服务端抛异常返回 500这里会走进兜底文案。4.3 把网络请求接到界面上注意操作系统不允许在主线程联网安卓操作系统不允许在主线程执行网络请求否则会直接抛NetworkOnMainThreadException。所以sendMessage必须放进子线程。一个简洁的写法是new Thread(() - { ... }).start()拿到结果后用runOnUiThread回到主线程更新列表。private void sendMessage() { String content editInput.getText().toString().trim(); if (content.isEmpty()) return; messageList.add(new ChatMessage(content, true)); adapter.notifyItemInserted(messageList.size() - 1); recyclerView.scrollToPosition(messageList.size() - 1); editInput.setText(); String question content; new Thread(() - { String reply; try { reply ChatBotClient.sendMessage(question); } catch (IOException e) { reply localReply(question); } final String result reply; runOnUiThread(() - { messageList.add(new ChatMessage(result, false)); adapter.notifyItemInserted(messageList.size() - 1); recyclerView.scrollToPosition(messageList.size() - 1); }); }).start(); }注意这里把上一章的localReply保留了下来用在了catch分支里。这个组合形成了一种容错服务端通的时候走真接口服务端挂了、网络断了、超时了退回本地关键词回复界面永远能给出响应。答辩时你能主动讲出这个设计比功能本身更亮眼。4.4 在 AndroidManifest 里放行网络权限Android Studio 新建工程默认不会帮你加联网权限不加权限时HttpURLConnection会直接报SecurityException: Permission denied。打开app/src/main/AndroidManifest.xml在manifest标签下加一行uses-permission android:nameandroid.permission.INTERNET /这一步非常简单但漏掉的人非常多。加完权限后先跑一次模拟器如果收到回复说明整条链路已经通了。如果还没通大概率是下一章要写的这些课设验收常见坑在等着你。5. 课设验收避坑模拟器联网、明文 HTTP 与 5 个常见崩溃现场这一章是血泪经验的合集。网络层代码写完之后真正让课设卡壳的往往不是逻辑而是几个看起来很小、查起来却很耗时的环境问题。下面五条按照“现象 → 原因 → 解决”的顺序写基本覆盖了聊天机器人课设里最常踩的坑。5.1 模拟器里请求 127.0.0.1 失败地址根本不是一个世界现象点击发送后消息列表一直只剩本地兜底回复。用 Android Studio 的 Logcat 能看到ConnectException: ECONNREFUSED (Connection refused)。原因Android 模拟器是一个独立虚拟设备它自己的127.0.0.1指向模拟器本身而不是你的电脑。你电脑上 Flask 跑在宿主机的127.0.0.1:8080模拟器里访问127.0.0.1:8080当然会被拒绝。Android 模拟器访问宿主机有一套专用地址是10.0.2.2。解决把ChatBotClient里的 URL 从http://127.0.0.1:8080/chat改成http://10.0.2.2:8080/chat。真机调试时则要改成电脑在局域网里的 IP比如http://192.168.1.100:8080/chat。排查的时候先别用手机直接用 curl 打一下服务端接口能通就说明问题在安卓端的地址或网络配置上能省下很多时间。5.2 Android 9 以上访问明文 HTTP 直接被拦翻车现场现象真机或者高版本模拟器上请求发出去以后立刻报CLEARTEXT communication to 10.0.2.2 not permitted by network security policy服务器根本没收到请求。原因从 Android 9API 28开始系统默认禁止应用使用明文 HTTP 流量。你的地址是http://开头没有任何加密系统直接把请求拦在应用层之外了。解决在 AndroidManifest.xml 的application标签上加一行android:usesCleartextTraffictrue允许整个应用走明文 HTTP。这是课设阶段最便宜的后悔药。有同学会去查 networkSecurityConfig 的配置想只对局域网 IP 放行但network-security-config的domain配置对 IP 字面量的支持很不稳定折腾半天不如一个全局开关干净。如果老师追问安全性你就如实说课设阶段用 HTTP 是为了演示方便生产环境必须换 HTTPS这反而是个不错的加分回答。5.3 消息发出后列表不自动滚到底部滚动时机不对现象插入新消息后RecyclerView 停在原来的位置最后一条消息被挤到屏幕外用户要手动滑下去才能看到很影响演示效果。原因notifyItemInserted之后立即调用scrollToPosition但这时候 RecyclerView 还没有完成新 item 的测量和布局滚动目标算出来是错的。解决把滚动操作放到 RecyclerView 的布局完成回调里用post方法recyclerView.post(() - recyclerView.scrollToPosition(adapter.getItemCount() - 1));另外注意插入消息时用notifyItemInserted(messageList.size() - 1)不要为了省事改成notifyDataSetChanged()。后者会把整个列表重新绑定一遍不仅浪费性能还会把用户正在观看的上下文打断。这个坑在消息较多的时候特别明显演示前一定要多聊几轮验证一下。5.4 键盘弹起把输入框顶飞窗口模式没设置现象点击编辑框输入文字时软键盘把输入栏顶到屏幕上方或者干脆把消息列表挤得只剩一条界面看起来很狼狈。原因Activity 的windowSoftInputMode默认是adjustUnspecified系统会自己选一种处理方式结果经常是 pan 模式整个窗口被软键盘顶上去而不是缩小。解决在 AndroidManifest.xml 里给 MainActivity 加上android:windowSoftInputModeadjustResize让软键盘弹出时窗口自动压缩而布局里用layout_weight的 RecyclerView 会自动让出空间activity android:name.MainActivity android:windowSoftInputModeadjustResize /同时不要用 ScrollView 包 RecyclerView。两者都是滚动控件嵌套之后手势冲突会变成玄学问题这也是很多新手改布局改到崩溃的根源。聊天列表直接用 RecyclerView不要加外套。5.5 中文回复乱码编码不统一现象服务端返回的中文变成ä½ å¥½之类的乱码或者 JSON 里出现一大串\u转义字符。原因绝大多数情况下是客户端读取流时没有指定字符集。InputStreamReader默认使用系统默认字符集不同版本的模拟器表现不一样导致中文被错误解码。另外JSON 里的\u4f60\u597d其实是 Unicode 转义序列解析后就是“你好”很多人误以为这也是乱码。解决客户端统一用new InputStreamReader(is, UTF-8)来读响应发送请求体时用body.toString().getBytes(UTF-8)两端编码对齐问题基本消失。服务端如果是 Flaskjsonify默认会用 JSON 转义这正是\u的来源属于正常现象。如果服务端返回的中文在浏览器里看着正常、在安卓端乱码优先检查客户端读取代码里有没有写死UTF-8而不是去改服务端。6. 高分局的三个小技巧语音输入、消息状态与可配置服务端地址功能跑通、坑也填完之后剩下的就是怎么让课设看起来比别人“多一点”。下面三个技巧改动量都不大但对演示效果的提升很明显。6.1 用系统语音识别给聊天加一个入口Android 自带语音识别 Intent不需要接任何第三方 SDK。在输入框旁边放一个麦克风按钮点击后用RecognizerIntent唤起系统语音识别private void startVoiceInput() { Intent intent new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); intent.putExtra(RecognizerIntent.EXTRA_PROMPT, 对着手机说点什么); try { startActivityForResult(intent, 1001); } catch (ActivityNotFoundException e) { Toast.makeText(this, 当前设备没有语音识别服务, Toast.LENGTH_SHORT).show(); } }识别完成后在onActivityResult里把识别文本填进输入框再自动发送。演示时用语音说一句“你好”比手动打字直观得多。要注意部分手机 ROM 要求申请RECORD_AUDIO运行时权限可以在答案辩前先手动授权。6.2 做一个“对方正在输入”的等待状态网络请求有延迟如果点完发送界面上没有任何反馈老师会以为程序卡死了。最简单的做法是在发请求前向列表插入一条isFromMefalse的占位消息内容写成“对方正在输入…”请求成功后再把它替换成真实回复。这就避免了突然出现整段文字的突兀感。6.3 把服务端地址抽出来方便真机演示很多同学模拟器上用10.0.2.2跑通后到答辩现场改用真机代码里的地址还是模拟器专用地址直接连不上。把地址定义成常量放在一个单独的Config类里真机演示前只需要改一行http://192.168.x.x:8080/chat。我现在接到这类课设第一件事已经不是写代码而是问清楚要在哪台设备上演示、网络能不能通然后再决定服务端放哪。这个习惯帮我少熬了很多夜希望帮到你。本文还有配套的精品资源点击获取