简介:这份资源是面向移动应用开发课程学习者与Android初学者的课程设计报告,围绕在Android平台上实现一款接入图灵机器人API的智能聊天应用展开,适合需要完成课设、期末项目或想系统串联Android基础知识的读者参考。报告完整覆盖实验目的、实验内容、程序设计与说明三大板块,既有申请机器人Key、添加okhttp依赖等前期准备,也逐条讲解activity_main、chatting_left_item、chatting_right_item等布局的设计思路,并对MainActivity中initView、showData、sendData、getDataFromServer、updateView五个方法的职责、ChatBean消息实体与ChatAdapter数据适配器的作用做了类级说明,配合运行界面截图便于对照理解。压缩包共1个doc文件,约1.46MB,篇幅完整、层次清晰,可直接作为课设报告撰写模板,也能帮助读者理清从UI布局、ListView展示到网络请求与JSON解析的完整实现脉络。目前已有2708人学习,配套源码可参考作者同名博客。
1. 从一份能跑通的课设包说起:Android 聊天机器人的边界在哪
每到课设答辩季,聊天机器人都是被追问最多的一类题目。界面跑起来了、气泡左右切换了,可一旦被问到「消息从哪来、JSON 里哪个字段是回复、主线程直接请求为什么崩」,不少人就卡壳。这份 Android 聊天机器人课程设计包解决的就是这个落差:它把ChatBean数据模型、ChatAdapter适配器、RobotActivity里五个方法的调用时序和网络层代码放在一套工程里,配合一份写清设计思路的报告,能让人从布局拼装一路走到消息回环。适合正在做移动应用开发课设、想用一次完整的 HTTP 对话接口把 ListView、Bean、Adapter、OkHttp、JSON 解析串起来的人,也适合已经写完界面却不知道消息为什么要异步回来的人。
2. 拆包看骨架:ChatBean、ChatAdapter 与三份布局的咬合关系
拿到包先别急着点运行,先看文件结构。这份工程的目录切分很清楚,聊天页相关的东西集中在四个位置:布局在res/layout下,数据模型ChatBean和适配器ChatAdapter各自一个 Java 文件,业务逻辑全部压在RobotActivity里。这种「一个 Activity 管页面 + 一个 Bean 管数据 + 一个 Adapter 管渲染」的三段式结构,是 Android 入门课设里最稳的写法,改动半径小,答辩时也容易讲清楚每一层在做什么。
| 文件 | 职责 | 改动频率 |
|---|---|---|
activity_main.xml | 聊天页整体骨架,标题、列表、输入区 | 低 |
chatting_left_item.xml | 机器人气泡:头像 + 文本 | 低 |
chatting_right_item.xml | 用户气泡:文本 + 头像 | 低 |
ChatBean.java | 单条消息的状态与内容 | 低 |
ChatAdapter.java | 把 List 数据映射成左右两种 View | 中 |
RobotActivity.java | 初始化、发送、请求、刷新 | 高 |
先看布局。activity_main.xml用的是最外层竖直LinearLayout,顶部一个TextView当聊天窗口标题,下面套一个RelativeLayout,里面先放ListView占据剩余空间,再在底部放一个横向的小RelativeLayout,里面是EditText和「发送」按钮。这个层级安排的关键点是ListView必须写在输入区上方、并且带layout_weight或固定高度,否则输入框会被列表顶出屏幕——这是很多人在真机上第一眼就翻车的地方。
两个气泡布局的结构几乎对称:chatting_left_item.xml里ImageView在左、TextView在右,chatting_right_item.xml则反过来。两个TextView共用同一个style,背景分别指向chat_left_selector和chat_right_selector。这种抽style的做法省事,但也埋了个雷:如果两个气泡只改背景不改内边距,长文本换行时会一边贴边一边留白,视觉上很别扭。稳妥做法是在 style 里统一padding,再让 selector 只负责背景色和圆角。
2.1 ChatBean:state 字段为什么用 int 而不是 boolean
ChatBean的设计是整份代码里最值得先读懂的一块。它只放两个字段:一个state表示消息归属,一个content表示消息正文。原文把「发送消息设为 1、接收消息设为 2」,这个约定看起来随手,实际上决定了后面 Adapter 的分支逻辑能不能写干净。
public class ChatBean { // 消息状态:1 表示用户发出的消息,2 表示机器人回复的消息 private int state; // 消息正文,直接展示到气泡 TextView 上 private String content; public ChatBean(int state, String content) { this.state = state; this.content = content; } public int getState() { return state; } public void setState(int state) { this.state = state; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } }这里的逻辑说明很直白:构造时就把状态和内容一起塞进去,避免先new再逐个set导致中间态被 Adapter 读到。参数上,state用int而不是boolean的理由是扩展性——今天只有收发两种,明天要加「正在输入」「系统提示」就是 3、4,用布尔值就得改字段类型、改适配器判断、改所有调用点。我一般在课设里会顺手把状态值定义成常量,比如public static final int TYPE_SEND = 1;,这样getView里的if读起来不是魔法数字。
还有一个新手常忽略的点:ChatBean不要放Bitmap或Drawable头像字段。头像在布局里是静态资源,跟着 item 布局走就行;一旦把图片塞进 Bean,列表滚动时反复解码会拖慢帧率,还会让 Bean 跟 Android 资源系统耦合,答辩时被问「Bean 为什么要持有 View 层资源」就很难圆。
2.2 ChatAdapter:getViewTypeCount 与 getItemViewType 的三件套
ChatAdapter继承BaseAdapter,核心工作是把List<ChatBean>里的每条数据翻译成左边或右边一种 item 布局。原文说「getView 里用 if 和 else 判断当前消息是发送还是接收」,这个思路没错,但只写getView里的分支是不够的,必须同时重写getViewTypeCount()和getItemViewType(),否则 ListView 的回收机制会把左右两种布局混着复用,滚动几屏之后就会出现「机器人消息跑到右边」的灵异现象。
public class ChatAdapter extends BaseAdapter { private List<ChatBean> data; private LayoutInflater inflater; public ChatAdapter(Context context, List<ChatBean> data) { this.data = data; this.inflater = LayoutInflater.from(context); } @Override public int getCount() { return data == null ? 0 : data.size(); } @Override public Object getItem(int position) { return data.get(position); } @Override public long getItemId(int position) { return position; } // 关键一:告诉 ListView 一共有两种 item 类型 @Override public int getViewTypeCount() { return 2; } // 关键二:每条数据按 state 返回类型,1 和 2 正好对上 @Override public int getItemViewType(int position) { return data.get(position).getState(); } @Override public View getView(int position, View convertView, ViewGroup parent) { ChatBean bean = data.get(position); ViewHolder holder; if (convertView == null) { holder = new ViewHolder(); // 按状态选择加载哪一份 item 布局 if (bean.getState() == 1) { convertView = inflater.inflate(R.layout.chatting_right_item, parent, false); holder.head = convertView.findViewById(R.id.myhead); } else { convertView = inflater.inflate(R.layout.chatting_left_item, parent, false); holder.head = convertView.findViewById(R.id.robot_head); } holder.content = convertView.findViewById(R.id.chat_content); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } holder.content.setText(bean.getContent()); return convertView; } static class ViewHolder { ImageView head; TextView content; } }代码后的逻辑说明分三层。第一层,getViewTypeCount()返回 2 是告诉 ListView「我有两种长相的 item,回收站要分开存」,这样 ListView 不会把右边布局塞给机器人消息。第二层,getItemViewType()直接返回state,是因为前面把用户消息定成 1、机器人定成 2,正好落在[0, getViewTypeCount())区间里;如果状态值乱定成 0 和 1 也能跑,但 1 和 2 更直观,答辩时解释成本低。第三层,convertView == null时才inflate,非空时直接取ViewHolder,这是 ListView 的标准防卡顿写法,课设里不写也能跑,但列表一长就会明显掉帧。
参数上还要注意inflater.inflate的第三个参数必须传parent且attachToRoot为false。传null会让 item 的layout_width="match_parent"失效,气泡宽度缩成内容宽;传true则会直接挂到 ListView 上,返回的convertView跟预期不一致,运行时会抛IllegalStateException。这个坑我在早期项目里踩过,表现是气泡全部缩成一小团,查了半天才发现是 inflate 参数写错。
2.3 三份布局的固定与可变部分
把三份布局拆开看,固定的是控件树,可变的是背景和文本。activity_main里真正会动的只有ListView的内容和EditText的输入,标题和按钮都是静态的;两个 item 布局里会动的只有TextView的文本和气泡背景,头像是静态drawable。这种「静态骨架 + 动态数据」的分法决定了后面 Activity 里只需要findViewById一次、Adapter 里只需要setText一次,不用每次刷新重建 View。
这里有个容易忽略的细节:ListView在activity_main里必须给一个android:layout_weight="1"或者明确高度,否则当消息条数少的时候它不会撑开,输入框会浮在中间;条数多的时候它又会把输入框顶走。正确写法是外层RelativeLayout里让ListView的layout_above指向输入区,或者用LinearLayout的 weight 分配。原文用的是RelativeLayout套ListView,那就必须保证输入区的layout_alignParentBottom="true"、ListView的layout_above指过去,这两行属性缺一不可。
3. 打通对话链路:OkHttp 请求、JSON 解析与网络权限配置
界面骨架搭好之后,真正的分水岭在网络层。这份工程选的是 OkHttp,在build.gradle的dependencies里加一行implementation 'com.squareup.okhttp3:okhttp:3.12.0'就能把库拉进来。选它的理由不是「流行」,而是课设场景下三个实际需求:一是它自带连接池和超时配置,不用自己封装HttpURLConnection的流读写;二是同步、异步两套 API 清楚,异步enqueue天然把回调扔回主线程,省掉Handler切线程的样板代码;三是请求体和响应体的字符串读取有现成方法,解析 JSON 之前不用手写缓冲区。
3.1 申请机器人身份标识与请求参数
对话接口的调用前提是先拿到一个 Key。原文的流程是在机器人开放平台注册账号、创建一个机器人、复制平台分配的 apikey。这个 Key 在请求里扮演身份标识,服务端靠它判断请求来自哪个机器人、走哪套语料。课设里最常见的错误是直接拿示例代码里的假 Key 去跑,返回码不是 200 而是鉴权失败,然后怀疑网络有问题——其实第一步就该先确认 Key 是不是自己账号下刚生成的。
请求本身是一个 POST,参数不多,但每个都有用:
| 参数 | 是否必填 | 作用 | 常见误用 |
|---|---|---|---|
key | 必填 | 机器人身份标识 | 复制时带空格或换行 |
info | 必填 | 用户输入的文本 | 传了空串或纯空格 |
userid | 选填 | 区分会话用户 | 每次随机生成导致上下文丢失 |
userid这个参数在课设报告里经常被略过,但它影响的是多轮对话的连贯性。同一个userid发「我叫什么」再发「我刚才说了什么」,平台能串起来;每次请求换一个随机userid,机器人就当成新用户,上下文全丢。我一般会把它设成设备标识或者固定字符串,课设演示时够用,也不会因为频繁变号导致回复驴唇不对马嘴。
3.2 OkHttp 的封装写法与超时参数
网络请求的代码集中在getDataFromServer()里,写法上建议把OkHttpClient提成成员变量而不是每次请求都 new 一个。OkHttp 内部有连接池和线程池,反复 new 会导致连接无法复用,请求一多就明显变慢。
// 成员变量,整个 Activity 生命周期内复用 private OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) // 建立连接的超时 .readTimeout(15, TimeUnit.SECONDS) // 等待响应的超时 .writeTimeout(10, TimeUnit.SECONDS) // 发送请求体的超时 .build(); private void getDataFromServer(String info) { // 请求体使用表单格式,参数名与服务端约定一致 RequestBody body = new FormBody.Builder() .add("key", API_KEY) .add("info", info) .add("userid", USER_ID) .build(); Request request = new Request.Builder() .url(API_URL) .post(body) .build(); // 异步请求,回调默认回到主线程 client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { // 网络异常时给用户一个可读的兜底提示 runOnUiThread(() -> updateView("网络开小差了,稍后再试")); } @Override public void onResponse(Call call, Response response) throws IOException { if (response.isSuccessful() && response.body() != null) { String json = response.body().string(); String reply = parseReply(json); runOnUiThread(() -> updateView(reply)); } else { runOnUiThread(() -> updateView("服务返回异常:" + response.code())); } } }); }逻辑上要抓住三个点。第一,超时参数给的是秒级,connectTimeout管的是 TCP 握手,readTimeout管的是服务端思考时间,课设环境网络一般,10 到 15 秒是比较稳的区间,给 3 秒容易在答辩现场因为网络抖动直接超时。第二,enqueue是异步,回调线程不是主线程,所以任何碰 UI 的操作都要包runOnUiThread,否则就是CalledFromWrongThreadException。第三,response.body().string()只能调一次,调完流就关了,如果先打印日志再解析,第二次读会拿到空串——这个坑很隐蔽,表现是「日志里有返回,界面上没回复」。
3.3 JSON 解析与错误码兜底
平台返回的 JSON 结构通常是外层一个状态码、内层一个结果对象,文本回复藏在结果对象的text字段里。解析用 Android 自带的org.json就够,不用额外引 Gson。
private String parseReply(String json) { try { JSONObject root = new JSONObject(json); int code = root.optInt("code", -1); if (code == 200) { // 正常返回时取结果对象里的文本字段 JSONObject result = root.optJSONObject("result"); if (result != null) { return result.optString("text", "我没听懂"); } } // 非 200 时按错误码给不同提示 if (code == 40001 || code == 40002) { return "身份校验没通过,检查 Key 是否正确"; } if (code == 40003) { return "今日调用次数用完了"; } return "接口返回异常,code=" + code; } catch (JSONException e) { // 解析失败时返回兜底文案,避免界面空着 return "返回内容解析失败"; } }这段的关键在于optString和optInt的兜底默认值。getString在字段缺失时直接抛异常,optString会返回第二个参数,课设演示里宁可显示一句「我没听懂」也不要让应用闪退。错误码分支不用背,但至少要区分「鉴权失败」和「配额用完」两类,因为前者改 Key 能救,后者只能等或者换账号,答辩时被问到「为什么报错」能答上来。
3.4 网络权限与真机验证
请求代码写完不等于能跑,AndroidManifest.xml里少了<uses-permission android:name="android.permission.INTERNET" />就是UnknownHostException或者SecurityException。这个权限是普通权限,声明即生效,不用动态申请,但位置要写在<application>标签外面。
真机验证时还有两个环境差异要留意。模拟器默认走宿主网络,接口通常直通;真机如果连着内网或者开了省电模式,后台网络可能被限,表现是请求一直挂到readTimeout才回调onFailure。我的习惯是先在模拟器上确认 JSON 能解析出来,再换真机看一遍超时和回调线程,两步都过再往报告里写截图,避免报告里写「运行成功」但演示时卡在转圈。
4. RobotActivity 的五个方法:从 initView 到 updateView 的时序拆解
原文把聊天页逻辑拆成initView()、showData()、sendData()、getDataFromServer()、updateView()五个方法,这个拆分方式看起来朴素,但时序关系理清楚之后,整个页面就是一个单向数据流:初始化拿控件,欢迎语进列表,用户点发送把消息塞进列表并触发请求,请求回来再刷新列表。理清这条线,答辩时被问「消息为什么先上屏再等回复」就能答得具体。
注意:原文里 Activity 名字出现过
MainActivity和RobotActivity两种写法,实际代码里聊天页统一用RobotActivity,MainActivity只做入口跳转。报告和源码对不上是课设常见现象,读的时候以源码的类名为准,不要两个名字混着改。
4.1 initView:控件绑定与列表初始化
initView()干三件事:findViewById拿到ListView、EditText、Button;new一个ArrayList<ChatBean>当数据源;new一个ChatAdapter并setAdapter到列表上。这三步的顺序不能反,Adapter 依赖 List,List 依赖 Activity 上下文,setAdapter又必须在 ListView 拿到之后。
private void initView() { listView = findViewById(R.id.chat_list); inputEt = findViewById(R.id.input_et); sendBtn = findViewById(R.id.send_btn); // 数据源与适配器一次性建好,后续只改 List 内容 chatList = new ArrayList<>(); adapter = new ChatAdapter(this, chatList); listView.setAdapter(adapter); // 发送按钮点击后走统一入口 sendBtn.setOnClickListener(v -> sendData()); }参数上没什么花哨的,但有个顺序细节:chatList必须是final或者成员变量,不能在方法里new完就丢,否则 Adapter 持有的是旧引用,后面add新消息时列表不会刷新。另外setOnClickListener里调sendData()而不是把发送逻辑直接写在匿名内部类里,是为了让「点击」和「业务」解耦,后面加语音输入或者长按发送时不用改监听器。
4.2 showData:欢迎语怎么进列表
欢迎信息存在res/values/strings.xml里,showData()负责把它包装成一条state=2的ChatBean塞进列表并通知刷新。原文提到「第一次进入应用机器人会发送欢迎信息」,这个「第一次」在课设里通常不做持久化判断,每次进页面都发一条,简单直接。
private void showData() { // 从资源文件取欢迎语,支持多语言时只改 strings.xml String welcome = getString(R.string.welcome_text); chatList.add(new ChatBean(2, welcome)); adapter.notifyDataSetChanged(); // 滚到最新一条,避免欢迎语被输入区挡住 listView.smoothScrollToPosition(chatList.size() - 1); }逻辑说明:state=2表示这条是机器人说的,走左边气泡;notifyDataSetChanged必须调,否则 List 变了但界面不动;smoothScrollToPosition是体验细节,不调也能显示,但消息多了之后新消息会落在屏幕外,用户以为没发出去。参数上chatList.size() - 1是最后一条的下标,传size()会越界。
4.3 sendData:用户消息入列与输入框清空
sendData()是用户点击发送后的统一入口,流程是:读输入框文本、判空、构造state=1的ChatBean、加入列表、刷新、清空输入框、调getDataFromServer发请求。
private void sendData() { String text = inputEt.getText().toString().trim(); if (text.isEmpty()) { // 空消息不发送,给个轻提示即可 Toast.makeText(this, "说点什么吧", Toast.LENGTH_SHORT).show(); return; } // 先上屏,让用户看到自己发的消息 chatList.add(new ChatBean(1, text)); adapter.notifyDataSetChanged(); listView.smoothScrollToPosition(chatList.size() - 1); inputEt.setText(""); // 再触发网络请求,回复回来后追加到列表 getDataFromServer(text); }关键顺序是「先上屏、再请求」。如果等请求回来才把用户消息加进列表,弱网环境下用户会盯着输入框怀疑没发出去;先上屏能立刻给反馈,机器人回复慢一点也符合直觉。清空输入框放在请求之前,避免请求期间用户又输入内容被覆盖。参数上trim()不能省,否则一串空格也能发出去,服务端返回空回复,界面上就是一条空气泡。
4.4 getDataFromServer 与 updateView:请求回传后的收尾
getDataFromServer在 3.2 节已经贴过主体,这里只补它在时序里的位置:它接收sendData传来的文本,异步请求,回调里拿到回复后调updateView。updateView做两件事:把机器人回复包装成state=2的ChatBean加进列表,刷新并滚到底。
private void updateView(String reply) { chatList.add(new ChatBean(2, reply)); adapter.notifyDataSetChanged(); listView.smoothScrollToPosition(chatList.size() - 1); }注意updateView既被成功回调调用,也被失败回调调用,所以它接收的是「要显示的文本」而不是「解析后的对象」,失败时传兜底文案即可。这样成功和失败走同一条渲染路径,不会出现「失败时界面不刷新」的情况。滚动到底同样用smoothScrollToPosition,参数是size() - 1。
4.5 onKeyDown:退出逻辑别写反
原文重写onKeyDown实现「点后退键退出聊天程序」。实现时要注意:如果是直接finish(),在聊天页按一次返回就退出,体验上没问题;如果要加「再按一次退出」的提示,就要记时间戳,两次间隔小于 2 秒才finish,否则只弹 Toast。课设演示里前者更稳,后者容易因为计时逻辑写错导致第一次返回没反应、第二次直接杀进程。
@Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { // 直接结束当前聊天页,回到入口页 finish(); return true; } return super.onKeyDown(keyCode, event); }返回true表示事件已消费,不再往上传;返回super的默认实现会走系统默认返回行为。两者在单页课设里差别不大,但显式写finish()能让「退出」这个动作在代码里有迹可循,答辩时问「返回键做了什么」能指着这段答。
5. 五种翻车现场与排查路径:聊天机器人上线前的避坑清单
这套代码在模拟器上跑通和在答辩现场跑通是两回事,中间隔着一堆环境差异和时序问题。下面五条是我在类似课设里反复见到的翻车点,每条按现象、原因、解决写,照着排查能省不少来回。
5.1 一点发送就闪退,日志里是 NetworkOnMainThreadException
现象:点击发送按钮,应用直接崩,Logcat 里一行红色android.os.NetworkOnMainThreadException。
原因:Android 从 3.0 开始禁止在主线程做网络请求,OkHttpClient.execute()是同步方法,如果误用了execute而不是enqueue,请求就在主线程发出去了。
解决:确认调用的是client.newCall(request).enqueue(callback),回调里碰 UI 前包runOnUiThread。如果一定要同步请求,必须放到子线程或AsyncTask里,课设阶段直接用异步就够了,不要为了「代码短」去碰同步。
5.2 机器人回复到了,但界面不刷新
现象:Logcat 里能看到返回的 JSON,parseReply也返回了文本,但列表里就是不出现新气泡。
原因有两个常见分支。一是updateView里只add了数据但没调adapter.notifyDataSetChanged();二是回调线程不是主线程,add操作本身不报错,但 ListView 不会重绘,看起来像没生效。
解决:先确认notifyDataSetChanged在add之后调用;再确认updateView整体跑在runOnUiThread里。如果用的是Handler切线程,检查Looper是不是主线程的,别 new 了一个默认Handler在子线程里循环。
5.3 消息滚动几屏后左右气泡错位
现象:聊天记录超过一屏,往上滑再滑回来,机器人消息跑到了右边,或者用户消息背景变成了左边样式。
原因:ChatAdapter只重写了getView里的 if/else,没重写getViewTypeCount和getItemViewType。ListView 回收convertView时不知道有两种类型,把右边布局复用给了机器人消息。
解决:补上getViewTypeCount()返回 2、getItemViewType()返回state。补完之后还要确认state的取值范围严格落在 1 和 2,如果某条消息state被写成 0,getItemViewType返回 0 会命中getView里的 else 分支,视觉上还是错的。
5.4 返回码不是 200,提示「身份校验失败」
现象:请求发出去了,onResponse里code是 40001 或 40002,界面上显示 Key 相关错误。
原因:apikey 复制时带了空格或换行,或者创建机器人后平台分配的 Key 被重置过,代码里还是旧值。还有一种情况是请求参数名拼错,比如把info写成text,服务端认不出参数,返回鉴权类错误。
解决:把 Key 打到 Logcat 里逐字符比对,确认首尾没有空白;对照平台文档核对参数名大小写;如果 Key 重置过,重新复制一遍替换常量。不要把 Key 硬编码在会提交到公开仓库的文件里,课设演示无所谓,但习惯上可以放到local.properties或BuildConfig里。
5.5 输入框里的字发出去之后还在
现象:点击发送,消息上屏了,但输入框里的文字没清空,再点一次发出去两条一样的。
原因:inputEt.setText("")漏写,或者写在getDataFromServer回调里而不是sendData里。写在回调里的话,网络慢的时候用户等不及又点一次,就会重复发送。
解决:把清空动作紧跟在「用户消息入列」之后、网络请求之前,保证一次点击只对应一次清空;同时在sendData开头加空串判断,清空后的第二次点击会因为text.isEmpty()直接 return,不会重复发。
6. 让课设多扛一轮答辩:几个能直接加分的工程习惯
代码跑通只是及格线,答辩时真正拉开差距的是「你知道这份代码的边界在哪」。下面几个改动都不大,但每一个都能在报告里单独写一小节,也能在被追问时接得住。
第一个习惯是把会话记录落盘。现在每次杀进程聊天记录就没了,加一个SharedPreferences存最近 N 条ChatBean的 JSON 串,进页面时先读出来再补欢迎语,演示时关掉应用重进,记录还在,观感完全不一样。存储时只存state和content两个字段,不要存 View 或 Bitmap,反序列化时用JSONArray逐条还原,读失败就退回空列表,不要让解析异常把启动流程打断。
第二个习惯是把ListView换成RecyclerView。不是为了炫技,而是RecyclerView的Adapter天然支持多布局,getItemViewType的写法和现在一样,但回收逻辑更稳,长列表滚动不掉帧。迁移时把ChatAdapter继承改成RecyclerView.Adapter<ChatAdapter.VH>,getView换成onCreateViewHolder和onBindViewHolder,其余数据逻辑不动,改完在报告里写一句「为解决 ListView 复用错位问题」,就是一个完整的优化点。
第三个习惯是给气泡文本加最大宽度。现在长文本会把气泡撑到贴边,加一个android:maxWidth="240dp"或者代码里按屏幕宽度算maxWidth = screenWidth * 0.7,气泡就不会顶到另一边。这个改动小到不值一提,但截图对比很明显,答辩时被问「UI 上做过什么处理」能直接指出来。
第四个习惯是给请求加一层「正在输入」状态。sendData之后、回调之前,往列表里塞一条state=3的占位消息,回调回来替换成真实回复,失败就替换成错误文案。这样弱网时界面不是干等,而是有一个明确的过程反馈。实现上只需要在ChatBean里加一个状态值、在 Adapter 里给state=3加一个「...」的气泡布局,改动可控,效果直观。
最后一个习惯跟报告有关:把ChatBean的状态常量、ChatAdapter的类型返回值、getDataFromServer的超时参数这三个地方在报告里各写一句「为什么是这个值」,不要只贴代码。答辩老师追问的往往不是「怎么写的」,而是「为什么这么写」,这三个点答上来,这份课设就从「跑通了」变成「讲得清」。
提示:改完任何一处,先清一次
Build -> Clean Project再跑,资源 ID 变更后增量编译偶尔会残留旧索引,表现是findViewById返回 null,清完就好。
我在第一次做类似课设时,把「能跑」当成终点,结果答辩被问到getViewTypeCount为什么返回 2 时答不上来,现场翻代码才补上。从那以后我每次写完 Adapter 都强制走一遍「多布局三件套」的自检:getViewTypeCount有没有、getItemViewType返回值和状态值对不对得上、getView里的分支和类型数是不是一致。这三步花不了两分钟,但能挡掉后面几小时的错位排查。希望帮到你。
本文还有配套的精品资源,点击获取