简介:本资源是一份面向高校计算机与移动应用开发专业学生的Android课程设计报告,聚焦于基于图灵机器人API构建智能聊天App的完整实践方案,帮助学习者系统掌握UI布局、ListView数据适配、ChatBean消息模型、OkHttp网络请求、JSON解析及四大组件协同等核心技能。压缩包为单个1.46MB的Word文档(.doc),涵盖实验目的、分步实现流程、activity_main与左右聊天项布局详解、RobotActivity五大核心方法说明(initView/showData/sendData/getDataFromServer/updateView)、ChatBean与ChatAdapter类设计、strings.xml欢迎语配置及6张关键界面截图,内容详实、结构清晰,可直接用于课设答辩或开发复现。目前已有2708人学习下载,是Android初学者进阶实战的优质参考材料。
1. Android 聊天机器人课设:不是 Demo,是能跑通的完整闭环工程
这不是一个“Hello World”式界面截图堆砌的课程设计报告,而是一份在真实 Android Studio 环境下、基于图灵机器人 API、用 ListView + OkHttp + 自定义 Adapter 实现的可编译、可安装、可交互、有状态管理、带欢迎逻辑的聊天机器人 App 全流程落地文档。它解决的不是“怎么画个按钮”,而是“用户发一条消息后,App 怎么发起网络请求、解析 JSON、区分左右气泡、滚动到底部、避免重复提交、处理空响应、防止 UI 线程阻塞”这一整套移动开发中高频踩坑链路。适合正在做移动应用开发课程设计、需要交源码+报告+运行截图的本科生,也适合想快速复现一个轻量级 Android 聊天界面原型的初学者——它不依赖 Jetpack Compose 或 MVVM 复杂架构,用的是 Android 原生控件和经典 MVC 分层,代码量可控(核心 Java 类不到 400 行),但每一步都卡在真实开发的关节上:比如onKeyDown()重写不是为了炫技,而是解决物理返回键退出逻辑;ChatBean.state = 1/2不是随便设的枚举,是适配器里if-else渲染左右布局的唯一判断依据;chat_left_selector和chat_right_selector这两个 drawable 文件,直接决定气泡圆角、阴影、背景色是否随消息方向自动切换。如果你正被“ListView 数据不刷新”“发送后界面没反应”“JSON 解析报错”“输入法弹出遮挡输入框”这些问题卡住,这份资源就是为你写的血泪经验压缩包。
2. 图灵机器人接入与网络通信:从 Key 申请到 OkHttp 异步回调落地
2.1 图灵机器人平台注册与 Key 获取:不是填完表单就结束
图灵机器人(tuling123.com)虽已停止公开注册,但该课设所用 API 接口在历史项目中仍广泛存在,且其请求结构清晰、响应格式稳定,是教学级网络编程的极佳样本。关键不在于“能不能注册”,而在于如何把 Key 安全嵌入、如何构造合法请求体、如何应对服务端返回的非 200 状态码。
首先,登录图灵官网(需注意:当前新用户注册入口已关闭,但旧 Key 仍有效;若无 Key,可用课设报告中提供的测试 Keyxxx替代,仅限学习环境)。创建机器人后,获得apiKey字段值,长度为 32 位十六进制字符串。切勿硬编码在 Java 源码中——这是新手最常翻车点。正确做法是将其存入strings.xml:
<!-- res/values/strings.xml --> <string name="tuling_api_key">your_32char_apikey_here</string>然后在RobotActivity.java中通过getString(R.string.tuling_api_key)获取。这样既避免 Git 泄露风险,又方便多环境切换。
2.2 OkHttp 3.12.0 集成:为什么选这个版本而非 Retrofit?
课设明确指定com.squareup.okhttp3:okhttp:3.12.0,这不是随意选择。该版本是 OkHttp 最后一个支持 Android 4.4(API 19)的稳定版,而大量高校实验机、模拟器仍运行在此类低版本系统上。更高版本(如 4.x)已弃用OkHttpClient.Builder().connectTimeout()的部分参数签名,会导致compileSdkVersion 28以下项目编译失败。
添加依赖后,必须在AndroidManifest.xml中声明网络权限:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />提示:Android 9(Pie)及以上默认禁止明文 HTTP 请求。图灵 API 为
http://开头,若目标设备targetSdkVersion >= 28,需在application标签内添加android:usesCleartextTraffic="true",否则请求直接被系统拦截,Logcat 仅显示Cleartext HTTP traffic to xxx not permitted,无任何网络日志。
2.3 构造标准请求体与异步回调实现
图灵 API 要求 POST 请求,Content-Type: application/json,Body 为 JSON 对象。核心字段包括:key(你的 apiKey)、info(用户输入文本)、userid(建议设为设备 ID 或固定字符串,用于上下文记忆)。课设中getDataFromServer()方法应封装如下逻辑:
private void getDataFromServer(String userMsg) { OkHttpClient client = new OkHttpClient(); JSONObject jsonBody = new JSONObject(); try { jsonBody.put("key", getString(R.string.tuling_api_key)); jsonBody.put("info", userMsg); jsonBody.put("userid", "android_student_" + Build.SERIAL); // 防止 userid 为空 } catch (JSONException e) { e.printStackTrace(); return; } RequestBody body = RequestBody.create( MediaType.parse("application/json; charset=utf-8"), jsonBody.toString() ); Request request = new Request.Builder() .url("http://www.tuling123.com/openapi/api") // 注意:非 https .post(body) .build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { // 网络异常:无网、超时、DNS 失败 runOnUiThread(() -> showToast("网络错误:" + e.getMessage())); } @Override public void onResponse(Call call, Response response) throws IOException { if (!response.isSuccessful()) { // HTTP 状态码非 200,如 403(Key 无效)、500(服务端错误) runOnUiThread(() -> showToast("服务器错误:" + response.code())); return; } String responseData = response.body().string(); parseAndShowResponse(responseData); } }); }关键说明:
enqueue()是异步调用,必须在onResponse中用runOnUiThread()切回主线程更新 UI,否则ListView刷新会抛CalledFromWrongThreadException;parseAndShowResponse()是独立方法,负责 JSON 解析与ChatBean封装,后续章节详述;Build.SERIAL作为userid是兼容性写法(Android 10+ 已废弃,但课设目标 SDK 通常 ≤28,可用);生产环境应改用Settings.Secure.ANDROID_ID。
3. ListView 聊天界面渲染:左右气泡、状态驱动与滚动控制
3.1 ChatBean:消息模型的最小必要字段设计
课设中ChatBean类看似简单,实则承载了整个 UI 渲染的决策逻辑。其字段设计直指 ListView 复用机制的核心痛点:
public class ChatBean { private int state; // 1=发送(用户),2=接收(机器人) private String content; // 消息正文 private long time; // 时间戳,用于排序(课设未用,但建议保留) // getter/setter 省略 }为什么state必须是 int 而非 boolean?
因为ChatAdapter.getView()中需用if (bean.getState() == 1)显式判断,避免if (bean.isSend())在null时 NPE;且state=1/2与布局文件名chatting_left_item/chatting_right_item形成语义映射,降低理解成本。
3.2 自定义 Adapter:复用逻辑与 ViewHolder 模式落地
ChatAdapter继承BaseAdapter,核心在getView()。课设要求“不同消息加载不同 view”,但新手常忽略View 复用时的属性残留问题——例如机器人消息的 TextView 设置了左对齐,若复用到用户消息却未重置为右对齐,气泡就会错位。
public View getView(int position, View convertView, ViewGroup parent) { ChatBean bean = getItem(position); ViewHolder holder; if (convertView == null) { // 根据 state 动态 inflate 不同 layout int layoutId = (bean.getState() == 1) ? R.layout.chatting_right_item : R.layout.chatting_left_item; convertView = LayoutInflater.from(context).inflate(layoutId, parent, false); holder = new ViewHolder(); holder.ivHead = convertView.findViewById(R.id.iv_head); holder.tvContent = convertView.findViewById(R.id.tv_content); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } // 关键:每次复用都强制重置内容与样式 holder.tvContent.setText(bean.getContent()); if (bean.getState() == 1) { holder.ivHead.setImageResource(R.drawable.myhead); holder.tvContent.setGravity(Gravity.END | Gravity.CENTER_VERTICAL); } else { holder.ivHead.setImageResource(R.drawable.robot_head); holder.tvContent.setGravity(Gravity.START | Gravity.CENTER_VERTICAL); } return convertView; } static class ViewHolder { ImageView ivHead; TextView tvContent; }参数说明:
R.layout.chatting_right_item:用户消息布局,TextViewlayout_gravity="end",背景@drawable/chat_right_selector;R.layout.chatting_left_item:机器人消息布局,TextViewlayout_gravity="start",背景@drawable/chat_left_selector;setGravity()而非 XML 中写死,确保复用时方向不继承上一条消息。
3.3 ListView 滚动到底部:smoothScrollToPosition()的时机陷阱
聊天消息新增后,UI 必须自动滚动到底部。但listView.smoothScrollToPosition(adapter.getCount()-1)若在adapter.notifyDataSetChanged()之前调用,会因数据未更新而滚动到错误位置;若在之后但未等 UI 绘制完成,可能失效。
课设中updateView()方法应采用可靠写法:
private void updateView() { adapter.notifyDataSetChanged(); // 先通知数据变更 // 延迟执行滚动,确保 ListView 已完成测量与布局 listView.post(() -> { if (adapter.getCount() > 0) { listView.smoothScrollToPosition(adapter.getCount() - 1); } }); }post()将滚动操作放入主线程消息队列末尾,保证在notifyDataSetChanged()的 UI 更新完成后执行,是 Android View 滚动的黄金实践。
4. 界面交互与生命周期:从输入框聚焦到返回键退出的完整链路
4.1 输入框(EditText)与软键盘行为控制
课设未提,但实际运行中必遇问题:点击 EditText 后软键盘弹出,可能遮挡输入框或 ListView 底部消息。解决方案分两步:
第一步:在AndroidManifest.xml中为RobotActivity设置软键盘模式
<activity android:name=".RobotActivity" android:windowSoftInputMode="adjustResize|stateHidden" />adjustResize:Activity 视图高度随键盘收缩,露出 EditText;stateHidden:Activity 启动时不自动弹出键盘。
第二步:发送后自动收起键盘
在sendData()方法末尾添加:
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); if (imm != null && editText.isFocused()) { imm.hideSoftInputFromWindow(editText.getWindowToken(), 0); }4.2 发送逻辑防重与空输入拦截
用户连续点击“发送”按钮易导致重复请求。课设中sendData()应加入状态锁:
private boolean isSending = false; private void sendData() { if (isSending) return; // 防重发 String input = editText.getText().toString().trim(); if (input.isEmpty()) { showToast("请输入内容"); return; } isSending = true; // 添加用户消息到列表 ChatBean userBean = new ChatBean(); userBean.setState(1); userBean.setContent(input); chatList.add(userBean); updateView(); // 清空输入框 editText.setText(""); // 请求机器人回复 getDataFromServer(input); }并在onResponse()成功后重置isSending = false,否则用户将无法再次发送。
4.3 返回键(Back Key)退出逻辑:onKeyDown()的正确重写
课设要求“点击后退键退出程序”,但直接finish()会导致 Activity 销毁,而System.exit(0)是反模式。正确做法是捕获KEYCODE_BACK并调用moveTaskToBack(true):
@Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { moveTaskToBack(true); // 将任务栈移至后台,符合 Android 设计规范 return true; // 拦截事件,不传递给父类 } return super.onKeyDown(keyCode, event); }注意:
moveTaskToBack(true)不会杀死进程,只是将 App 切换到后台,用户可通过最近任务列表恢复——这比粗暴finish()更符合用户预期,也是课设“退出”需求的合规实现。
5. 避坑 / 常见问题 / 排查:五个真实翻车现场与解法
5.1 现象:ListView 消息顺序错乱,新消息总出现在顶部
原因:chatList是ArrayList<ChatBean>,但add()默认加在末尾,而notifyDataSetChanged()后getView()的position从 0 开始索引。若未按时间顺序插入,视觉上就会倒序。
解决:确保所有消息(包括欢迎消息)均按时间先后add()到chatList末尾;若需严格时间排序,应在ChatBean中增加time字段,并在add()前用Collections.binarySearch()插入正确位置。
5.2 现象:机器人回复“我还没学会回答这个问题”,但 Logcat 显示 JSON 解析成功
原因:图灵 API 返回的 JSON 中,code字段为100000表示正常,code=200000表示无答案。课设parseAndShowResponse()未校验code,直接取text字段。
解决:解析时先读code,再分支处理:
int code = jsonObject.getInt("code"); if (code == 100000) { String reply = jsonObject.getString("text"); // 创建机器人消息 bean } else { // 根据 code 显示预设兜底文案,如 "正在思考中..." }5.3 现象:发送中文后,机器人回复乱码(如“ä½ å¥½”)
原因:OkHttp 请求未显式设置 UTF-8 编码,服务器返回的响应体被默认按 ISO-8859-1 解析。
解决:response.body().string()前,强制指定编码:
ResponseBody body = response.body(); String responseData = body.string(); // OkHttp 3.12.0 默认 UTF-8,但保险起见可加: // String responseData = body.string(StandardCharsets.UTF_8);5.4 现象:App 安装后闪退,Logcat 报NetworkOnMainThreadException
原因:getDataFromServer()中误用execute()同步方法,而非enqueue()异步。课设原文未明确,但学生常抄错。
解决:确认所有网络请求均使用enqueue();若需同步调试,必须在子线程中调用execute(),且不能在主线程runOnUiThread()内执行。
5.5 现象:点击发送按钮无反应,Logcat 无任何日志
原因:Button.setOnClickListener()未在initView()中正确绑定,或findViewById()返回null(ID 名拼写错误,如R.id.btn_send写成R.id.send_btn)。
解决:在initView()中为每个控件绑定后,立即Log.d("Init", "btnSend: " + btnSend)打印对象,确认非 null;检查activity_main.xml中Button的android:id是否与 Java 中引用一致。
6. 进阶验证与调试技巧:用三步法确认你的课设真正跑通
6.1 网络层验证:用 Postman 模拟图灵 API 请求
不要等到 App 编译运行才查网络问题。直接用 Postman(或 curl)验证 API 可用性,这是最高效的前置排查:
curl -X POST "http://www.tuling123.com/openapi/api" \ -H "Content-Type: application/json" \ -d '{ "key": "your_apikey_here", "info": "你好", "userid": "test123" }'预期响应(精简):
{ "code": 100000, "text": "你好啊!很高兴见到你!" }若返回{"code":40001,"text":"apikey error"},说明 Key 无效;若返回{"code":40004,"text":"invalid userid"},说明userid格式错误。这一步能 100% 隔离是 API 问题还是 App 代码问题。
6.2 UI 层验证:用 Layout Inspector 查看气泡渲染细节
Android Studio 自带 Layout Inspector,可实时查看真机/模拟器上 ListView 每一项的 View 结构。重点检查:
chatting_left_item的TextView是否应用了@drawable/chat_left_selector;chatting_right_item的ImageViewsrc是否为@drawable/myhead;- 两个
TextView的gravity属性是否分别为start和end。
若发现gravity未生效,大概率是ChatAdapter.getView()中未在复用时重置(见 3.2 节)。
6.3 逻辑层验证:手动注入欢迎消息并观察初始化流
课设要求“第一次进入显示欢迎信息”。不要依赖showData()的模糊描述,应精确验证:
RobotActivity.onCreate()中,chatList初始化后是否立即add()了机器人欢迎ChatBean;showData()是否在setContentView()后、initView()前调用;updateView()是否在showData()末尾触发。
可在showData()开头加Log.d("Welcome", "Size: " + chatList.size()),启动时看 Logcat 是否输出Size: 1。
6.4 一份可直接粘贴的strings.xml完整配置表
为防strings.xml配置遗漏,以下是课设必需字段的最小化清单(含注释):
| name | value | 说明 |
|---|---|---|
| app_name | 智能聊天机器人 | App 名称 |
| tuling_api_key | your_32char_apikey | 图灵 Key,务必替换 |
| welcome_msg_1 | 你好!我是智能小助手~ | 欢迎消息 1 |
| welcome_msg_2 | 有什么问题可以问我哦! | 欢迎消息 2 |
| send_button_text | 发送 | Button 文本 |
| hint_input | 请输入消息... | EditText 提示 |
将此表内容复制到res/values/strings.xml即可开箱即用。
从那以后我每次交付课设,都会先用 Postman 过一遍 API,再用 Layout Inspector 截一张气泡渲染图,最后在showData()里打一行 Log 确认欢迎消息进列表——这三步走完,基本杜绝了“报告写了、代码编译过、但运行起来啥也不显示”的玄学翻车。希望帮到你。
本文还有配套的精品资源,点击获取