拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Android的学生评教系统APP设计与实现全流程指南

基于Android的学生评教系统APP设计与实现全流程指南

简介:面向需要完成Android课程设计或毕业设计的计算机专业学生,这份基于Android的学生评教系统设计文档提供了从后台管理到前台客户端的完整实现思路。资源包为单个docx文档,大小453KB,内容涵盖课题背景、研究意义、开发工具选型和系统功能模块划分等章节。整体方案采用Java语言,后台管理系统包含教师管理、班级管理、科目管理、课程管理四大模块,前台Android客户端实现登录、个人信息查看与查询功能,并涉及Android、Web、MySQL等主流技术栈及JDBC、JQuery、Ajax等开发要点。文档目录结构完整,可帮助读者快速理解评教系统的架构层次、接口设计与数据库思路,也可作为教学管理类APP开发的参考模板。目前已有43人学习,适合用于课程设计、毕业设计或项目复现参考资料。

1. 把“基于Android学生评教系统APP设计与实现”当成一个落地项目来做

期末评教的时候,很多学校还在用机房集中填表的方式:全班排队等一台电脑,每人按顺序登录、打分、填主观意见。这套老做法维护成本高,学生也容易为了赶时间乱点一通。基于Android学生评教系统APP要解决的,就是把这个流程移到每个人手机里:学生用自己的账号登录,按课程逐项打分、提交评教数据;教师端能看到自己被评的分数和评语;管理员能管理师生账号、查看总评结果。这个选题不是新鲜的高大上方向,但它是典型的Android开发“全流程”练习——要用到多页面导航、数据存储、网络请求、权限适配、列表渲染这些基本功,又比做一个记账本、日历App更有业务深度,撑得起课程设计、毕业设计或者一个入门级的移动端项目经验。

适合做这件事的人有两类:一类是正在选课设题目、需要快速把项目跑通并写文档的在校生;另一类是想用一个小而完整的业务系统练习Android工程能力的开发者。这篇笔记不讲论文怎么写,只讲怎么把标题变成能演示、能验证、能说服答辩老师的真实运行项目。从需求边界到数据库设计、核心界面、数据提交、图表统计,再到最后几步验证和踩坑处理,按一线做的顺序盘一遍。

2. Android学生评教系统的需求拆分:先把三端角色和核心流程定死

刚拿到这类题目最容易犯的错是一上来就建工程写界面。评教系统虽然功能不算多,但它天然带三角色,不把边界理清,开发中一定会反复改需求。我的建议是先画一张粗粒度的流程图,再按角色写用例,最后再想代码结构。

2.1 三种角色与功能边界:学生、教师、管理员各管什么

基于Android学生评教系统APP,典型设计是三个入口共用同一个客户端。学生端是绝对核心:登录后加载“待评教课程列表”,一门课对应一套评教表,表的组成一般是客观题(比如十个打分项,每项1~5分)加一个主观意见输入框。学生提交前要有“未答项提示”,防止漏评。教师端更简单:登录后展示教评结果汇总,看课程列表和各维度平均分,点击能看单个学生对每一题的打分记录和评语(匿名或实名看业务要求)。管理员端负责账号管理、课程分配、评教时间段开关。

一段能撑住答辩的SQLite数据设计,至少要有四张表:user表存三端账号和角色字段;course表存课程信息;evaluation表存评教记录头,含学生id、课程id、总分、提交时间;evaluation_detail表存每个打分项的明细。如果你的设计中评教指标是动态的——管理员可以增删评分项——那就再加一张indicator表。下面这段SQL是建表的核心部分:

CREATE TABLE user ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, role INTEGER NOT NULL, -- 1=学生 2=教师 3=管理员 real_name TEXT ); CREATE TABLE course ( id INTEGER PRIMARY KEY AUTOINCREMENT, course_name TEXT NOT NULL, teacher_id INTEGER NOT NULL, FOREIGN KEY(teacher_id) REFERENCES user(id) ); CREATE TABLE evaluation_indicator ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, max_score INTEGER DEFAULT 5 ); CREATE TABLE evaluation ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id INTEGER NOT NULL, course_id INTEGER NOT NULL, total_score REAL, comment TEXT, create_time TEXT DEFAULT (datetime('now', 'localtime')), UNIQUE(student_id, course_id), -- 防止重复评教 FOREIGN KEY(student_id) REFERENCES user(id), FOREIGN KEY(course_id) REFERENCES course(id) ); CREATE TABLE evaluation_detail ( id INTEGER PRIMARY KEY AUTOINCREMENT, evaluation_id INTEGER NOT NULL, indicator_id INTEGER NOT NULL, score INTEGER NOT NULL, FOREIGN KEY(evaluation_id) REFERENCES evaluation(id) );

这里有几个参数值得说明。UNIQUE(student_id, course_id)是在数据层拦截重复提交,避免学生对着同一门课反复写数据;如果不加这个约束,就要在提交接口里写“先查再插”的逻辑,面对并发时容易出漏洞。datetime('now', 'localtime')在SQL第一次执行时就按本地时间生成提交时间,比在Java里拼字符串稳,免去了时区偏移困扰。score字段用INTEGER而非TEXT,是为了统计平均分时直接做AVG()计算,不用解析字符串。

2.2 从需求文档到Android包结构:按功能分包而不是按角色分包

Eclipse时代的老项目喜欢按角色建包,比如com.example.student、com.example.teacher,这种结构在小demo里看着直观,一旦要加公共工具类,代码就会开始重复。更推荐按功能分包,这是Android Studio里比较协调的做法。

com.example.evaluation ├── activity/ -- 所有界面类,LoginActivity, StudentMainActivity等 ├── adapter/ -- RecyclerView相关Adapter ├── db/ -- SQLiteOpenHelper和DAO层 ├── model/ -- User, Course, Evaluation等实体类 ├── network/ -- HttpURLConnection或OkHttp封装 └── utils/ -- SharedPreferences工具, 时间格式化工具

按照这种结构,学生端和教师端虽然有各自的界面,但它们复用同一个UserDao、同一个CourseDao,只是界面层调用的组合方式不同。开发中你还会遇到资源共享的问题——登录信息用什么存?实际最常见的选择是SharedPreferences存userId和role,每次启动App时先读本地会话,再决定跳转到哪个主界面。这种做法虽然简单,但对课程设计级别的项目来说完全够用,而且便于答辩时解释“登录状态的保持方式”。不建议在这个项目里引入Token刷新、JWT那一套,既偏离Android本身的知识点,又容易把自己绕进去。

2.3 评教流程的时序确定:哪一个步骤先做、哪些数据先落本地

学生评教的主流程一般长这样:登录成功后,客户端先拉取“当前开放评教”的课程列表;点进一门课程后,加载对应的评教指标;逐项打分并填写评语;点提交时先在本地做完整性校验,再调用后端接口保存;提交成功后刷新列表。这里有一个容易忽略的点:答题过程中用户可能切到别的界面又切回来——Android的Activity会经历生命周期变化。

我见过不少翻车案例:学生填了一半的电话响了,或者误触返回键,再回来时发现打分全没了。原因是Activity被重建时没有保存界面状态。避免这个问题的标准方案是让打分数据跟随RecyclerView的Adapter数据源走,而Adapter的数据源被保存在ViewModel中,或者在onSaveInstanceState里保存当前打分项的HashMap。下面这段示意代码展示的是用ViewModel承载打分状态:

public class EvaluateViewModel extends AndroidViewModel { private final MutableLiveData<List<IndicatorItem>> indicators = new MutableLiveData<>(); private final HashMap<Integer, Integer> scores = new HashMap<>(); private final MutableLiveData<String> comment = new MutableLiveData<>(); private final int courseId; public EvaluateViewModel(@NonNull Application application, int courseId) { super(application); this.courseId = courseId; loadIndicators(); } private void loadIndicators() { // 从本地数据库或网络加载该课程的评教指标 List<IndicatorItem> list = IndicatorRepository.getInstance(getApplication()).getByCourse(courseId); indicators.setValue(list); } public void setScore(int indicatorId, int score) { scores.put(indicatorId, score); } public int getScore(int indicatorId) { Integer score = scores.get(indicatorId); return score == null ? 0 : score; } public boolean isAllAnswered() { List<IndicatorItem> items = indicators.getValue(); if (items == null || items.isEmpty()) return true; for (IndicatorItem item : items) { if (getScore(item.getId()) <= 0) return false; } return true; } }

逻辑要点是:打分时不直接操作界面控件,而是写进scores这个HashMap;Activity被系统回收后重建时,ViewModel的数据还在,RecyclerView的adapter从ViewModel里恢复每一题的分值。isAllAnswered()是提交前的检查方法,遍历所有指标,任何一个没选分就返回false。这里要特别说明:score的默认值用0来表示“未打分”,因为Integer对象可以为null,但HashMap里存null在后续拆包装Int时会直接NPE,所以用0更安全。

3. 动手实现最小可运行APP:从登录到提交评教的核心链路

需求拆完后进入实现阶段。一个能演示闭环的最小APP至少需要:登录界面、主界面列表、评教表单界面、提交接口、结果展示界面。这五个界面做完,这个题目就已经完成一大半了。这里要敲定技术选型:网络层用OkHttp还是HttpURLConnection,数据层用原生SQLite还是Room——选型没有绝对标准,看你的环境和需求。

3.1 用Android Studio搭建工程时的三个关键设置

如果你的Android Studio打开项目后报“Could not resolve”这一类错误,大概率是Gradle版本和SDK版本不匹配。建议新建工程时直接选“Empty Views Activity”而不是“Empty Activity”——后者默认用ViewBinding,前者生成的XML布局文件和传统findViewById思路更贴近,课程设计代码查起来也更直观。

在app/build.gradle里重点确认这四行配置:

android { namespace 'com.example.evaluation' compileSdk 34 defaultConfig { applicationId "com.example.evaluation" minSdk 26 targetSdk 34 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.9.0' implementation 'androidx.recyclerview:recyclerview:1.3.0' implementation 'com.squareup.okhttp3:okhttp:4.11.0' }

minSdk 26意味着应用最低支持Android 8.0,这不是随便选的。你稍后做运行时权限、FileProvider、前台通知时,26是个安全的底线;再老的版本做兼容适配会占用很多时间。compileSdk 34对应的是Android 14的API级别,Android Studio里新建工程默认给到这个,不用特意降到旧版本去“兼容”,真实考试或答辩环境不会因为你用了新SDK而扣分。

3.2 登录界面和角色切换:用SharedPreferences保持会话

登录是第一个界面。课程设计级别的系统通常不要求做密码加密传输,但也不能明文存数据库——这里最常用的折中做法是SHA-256摘要。先展示登录校验的核心逻辑:

public class LoginActivity extends AppCompatActivity { private EditText etUsername, etPassword; private Button btnLogin; private UserDao userDao; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); userDao = new UserDao(this); etUsername = findViewById(R.id.et_username); etPassword = findViewById(R.id.et_password); btnLogin = findViewById(R.id.btn_login); btnLogin.setOnClickListener(v -> attemptLogin()); } private void attemptLogin() { String username = etUsername.getText().toString().trim(); String password = etPassword.getText().toString().trim(); if (username.isEmpty() || password.isEmpty()) { Toast.makeText(this, "用户名或密码不能为空", Toast.LENGTH_SHORT).show(); return; } String hashed = hashPassword(password); User user = userDao.queryByUsernameAndPassword(username, hashed); if (user == null) { Toast.makeText(this, "用户名或密码错误", Toast.LENGTH_SHORT).show(); return; } SharedPreferences sp = getSharedPreferences("session", MODE_PRIVATE); sp.edit() .putInt("userId", user.getId()) .putInt("role", user.getRole()) .putString("realName", user.getRealName()) .apply(); navigateToMain(user.getRole()); } private String hashPassword(String raw) { try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] bytes = digest.digest(raw.getBytes(StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(String.format("%02x", b)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } private void navigateToMain(int role) { Class<?> target = StudentMainActivity.class; if (role == 2) target = TeacherMainActivity.class; if (role == 3) target = AdminMainActivity.class; startActivity(new Intent(this, target)); finish(); } }

hashPassword方法把输入的密码先做SHA-256摘要再传给DAO查询,数据库中预置的账号密码也应该用同样算法处理。这个方法有它的边界:SHA-256明文摘要挡不住彩虹表攻击,但课程设计阶段的需求就是“不直接明文保存”,做到这一层已经能解释过去。navigateToMain里的finish()是为了把登录页从返回栈里清掉,防止按返回键又回到登录页面,这个小细节答辩时提出来是一个加分项。

3.3 用RecyclerView加载待评课程列表并画出进度条

学生登录后看到的是“课程列表 + 评教进度”,大部分学生会习惯在列表项上看到“已完成”或“未完成”的进度标签。这个场景非常适合用RecyclerView的Adapter来实现。如果你的目标是让设计更像一个真实上线产品,可以给每个卡片顶部加一个ProgressBar,按已评课程数除以总课程数计算百分比。

下面是列表Adapter的关键实现:

public class CourseListAdapter extends RecyclerView.Adapter<CourseListAdapter.ViewHolder> { private final List<CourseWithStatus> data; private final OnItemClickListener listener; public CourseListAdapter(List<CourseWithStatus> data, OnItemClickListener listener) { this.data = data; this.listener = listener; } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_course, parent, false); return new ViewHolder(v); } @Override public void onBindViewHolder(ViewHolder holder, int position) { CourseWithStatus item = data.get(position); holder.tvCourseName.setText(item.getCourseName()); holder.tvTeacherName.setText(item.getTeacherName()); int progress = item.isEvaluated() ? 100 : 0; holder.progressBar.setProgress(progress); holder.tvStatus.setText(item.isEvaluated() ? "已完成" : "待评教"); } @Override public int getItemCount() { return data.size(); } static class ViewHolder extends RecyclerView.ViewHolder { TextView tvCourseName, tvTeacherName, tvStatus; ProgressBar progressBar; ViewHolder(View itemView) { super(itemView); tvCourseName = itemView.findViewById(R.id.tv_course_name); tvTeacherName = itemView.findViewById(R.id.tv_teacher_name); tvStatus = itemView.findViewById(R.id.tv_status); progressBar = itemView.findViewById(R.id.progress_bar); } } interface OnItemClickListener { void onItemClick(int position); } }

注意适配器中我把setText、setProgress都放在onBindViewHolder里,这是RecyclerView的标准要求——不能在onCreateViewHolder里做数据绑定,否则滚动时会出现数据串行。ProgressBar默认样式是一条水平线,但如果你的XML布局里没有加style="?android:attr/progressBarStyleHorizontal"属性,它会显示成圆圈转圈动画,这是个特别容易踩的坑。

3.4 评教表单页:动态生成打分项并处理提交前的完整性校验

评教表不该写死在XML里,因为评分指标是管理员可以动态配置的。页面用一个纵向LinearLayout套ScrollView,指标加载后按数量动态添加子View。每个打分项一行,左边是指标描述文字,右边是一个RatingBar或一组RadioButton。如果客观题数量在5~10个,RatingBar操作更快;如果指标数量动态变化,代码要处理“试题索引”和视图控件的对应关系。

提交时的核心代码:

private void submitEvaluation() { if (!viewModel.isAllAnswered()) { Toast.makeText(this, "还有未完成的评分项,请检查后提交", Toast.LENGTH_SHORT).show(); return; } JSONObject obj = new JSONObject(); try { obj.put("studentId", userId); obj.put("courseId", courseId); obj.put("comment", etComment.getText().toString().trim()); JSONArray detailArray = new JSONArray(); for (IndicatorItem item : viewModel.getIndicators().getValue()) { JSONObject detail = new JSONObject(); detail.put("indicatorId", item.getId()); detail.put("score", viewModel.getScore(item.getId())); detailArray.put(detail); } obj.put("details", detailArray); } catch (JSONException e) { e.printStackTrace(); } // 本地先保存草稿,再尝试提交网络 boolean saved = evaluationDao.saveLocal( viewModel.getIndicators().getValue(), etComment.getText().toString(), userId, courseId); if (saved) { Toast.makeText(this, "提交成功", Toast.LENGTH_SHORT).show(); finish(); } }

这段代码体现了一个值得在答辩时讲清楚的设计:先保存本地数据,再向服务器同步。课程设计里的网络请求常常不稳定,如果数据只存在服务端,一旦网络超时学生填写的内容就全丢了。把evaluationDao.saveLocal()放在接口调用之前,网络失败时数据仍然留在本地,下次联网可以补传。这个“本地先落库、远端做同步”的思路在真实App开发中是常态操作,不是炫技。

3.5 教师端统计视图:用SQL聚合替代逐个循环

教师端最核心的页面是“查看我的课平均分”。很多人写这段时喜欢把评分记录查出来,在Java里循环算平均分,这样代码长还容易出错。SQL本身就提供GROUP BY聚合,一行查询就能搞定:

SELECT c.course_name, COUNT(e.id) AS student_count, AVG(e.total_score) AS avg_score FROM course c LEFT JOIN evaluation e ON c.id = e.course_id WHERE c.teacher_id = ? GROUP BY c.id, c.course_name;

LEFT JOIN保证那些还没被评过的课程也能出现在结果列表里,学生数显示0、平均分显示NULL。如果实际要求只显示已有评分的课程,可以改成INNER JOIN。在Android的SQLiteDatabase.rawQuery()中执行这一段,遍历Cursor填入RecyclerView,就能完成统计视图的一大半工作。如果要画饼状图或柱状图,常见做法是引入MPAndroidChart,把avg_score喂给BarChart,这是移动端统计可视化最常见的方案。

4. Android学生评教系统避坑指南:从编译失败到数据丢失的6个踩坑记录

从环境搭建到功能交付,课程设计级别项目80%的排障时间都耗在几个固定问题上。下面按“现象 → 原因 → 解决”的方式写清楚,照着排查能少走很多弯路。

4.1 Gradle编译报错“Could not resolve all task dependencies”

现象:Sync或Build时控制台一片红,常见于Android Studio版本升级后打开旧项目。

原因:旧项目build.gradle里依赖的库版本在本地Gradle缓存中不存在,或者默认仓库顺序没走对。另一个高频原因是distributionUrl指向的Gradle版本和Android Studio内置版本不匹配。

解决:第一步,在build.gradle的allprojects或settings.gradle的dependencyResolutionManagement中确认仓库包含google()和mavenCentral();第二步,把distributionUrl里的Gradle版本手动改成与当前Android Studio匹配的版本;第三步,如果还报错,关掉Gradle离线模式。在Android Studio的设置里搜“offline”,取消勾选后重新Sync。

4.2 模拟器或真机安装时报“INSTALL_FAILED_USER_RESTRICTED”

现象:点Run按钮后编译成功,但安装到手机失败,日志提示限制安装未知来源。

原因:Android 8及以上版本默认禁止安装非应用商店的APK,调试安装需要授权安装权限。

解决:开发者选项里打开“USB调试”还不够,还需要在弹窗中允许“通过USB安装应用”;小米和华为设备还要在“应用安全”或“纯净模式”中额外确认。如果是公司或校园统一管理的设备,干脆换一台没锁的Android机,很多刷过机的开发者设备没有这个问题。

4.3 ListView和ScrollView嵌套导致滑动冲突

现象:评教表单页指标一多,整个页面拖动卡顿,内层列表只能显示两三项。

原因:ListView嵌套在ScrollView中,两个可滚动方向相同的容器互相争抢触摸事件。

解决:最省事的方案是All-in-One——不在表单页里放ListView/RecyclerView,而是用主线程的ScrollView包LinearLayout,动态添加View;必须用RecyclerView时,要基于NestedScrollView包裹并调用recyclerView.setNestedScrollingEnabled(false)。这个属性关闭了RecyclerView的内部滚动,让外层ScrollView接管滑动。

4.4 Android 6.0以上访问系统文件或读写外部存储直接闪退

现象:真机上点击“导出评教数据”按钮,应用立刻崩溃,低版本模拟器上正常。

原因:Android 6.0开始在运行时弹权限框,Android 10开始分区存储限制更严格;如果代码中直接硬编码了一条旧外部存储路径/sdcard/,高版本机型必然异常。

解决:先申请运行时权限,再按分区存储规则写文件。课程设计里如果只是导出数据表,优先写getExternalFilesDir(null)目录,这块不需要额外权限,而且是每个应用的私有目录。打包出来的数据文件可以通过ADB pull取回电脑。FileProvider是另一个坑:App间做文件共享(比如从微信打开导出的Excel)必须用content://形式的FileProvider URI,不能用file://,否则Android 7.0以后会直接抛FileUriExposedException。配置时按官方模板在AndroidManifest.xml里加Provider、在res/xml/file_paths.xml里配路径映射即可。

4.5 中文评语保存后乱码或长度截断

现象:学生在主观评教意见里输入几千字,提交后另一台设备查看显示乱码或后半截变成问号。

原因:两个问题叠加——网络请求默认编码不是UTF-8,或数据库字段类型限制长度。如果你用OkHttp提交JSON,需要在请求体中显式指定编码;如果用SQLite自带的TEXT字段,它长度限制不是主要问题,主要问题往往出在客户端往服务端传时用了application/x-www-form-urlencoded但没指定charset=UTF-8。

解决:OkHttp中构造RequestBody时写RequestBody.create(jsonStr, MediaType.parse("application/json; charset=utf-8")),不要让框架用默认编码。数据落库前,把字符串做一次长度拦截,超过2000字截断并提示“评语长度超限”,这是避免底层数据库字段溢出的后悔药。

4.6 RSA算法加密登录密码后服务端解密失败的兼容性问题

现象:用RSA公钥对密码加密封装后,不同手机上提交登录信息,服务端间歇性解密失败。

原因:RSA填充模式不一致。Android默认的RSA/ECB/PKCS1Padding在部分设备上得到的结果和服务端Java环境默认的RSA/ECB/OAEPWithSHA-1AndMGF1Padding不兼容,表现为同样的密钥在不同设备上加密结果有时能解、有时不能。

解决:统一写法,加密和解密都显式指定Cipher.getInstance("RSA/ECB/PKCS1Padding");另外注意密文不能用String直接传,要Base64编码后再放入JSON。下面的示例代码是常用写法:

private String rsaEncrypt(String plainText, PublicKey publicKey) throws Exception { Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); return Base64.encodeToString(encrypted, Base64.NO_WRAP); }

如果你打算用RSA做密码加密,这段代码可以照抄;如果你打算加密后直接明文存SQLite,这和没加密一样,答辩时不要自曝这个设计漏洞。

5. 验证与进阶:项目跑通不等于验收通过,收尾还要做这几件事

项目能编译、能启动、能提交数据,这只是第一步。答辩或演示现场最怕的是:换一台设备跑就崩,或者数据库里查出脏数据。这里给出我常用的一套验证路径和两个进阶增强点,可以让你的项目比“能运行”的普通程度高出半档。

5.1 用ADB命令验证数据库落盘与网络请求参数

评教数据提交后怎么验证“确实存进去了”?最直接的方案是用SQLite数据库调试工具或直接跑ADB命令看数据库内容。如果是自带SQLiteOpenHelper的应用,数据库文件默认存放在/data/data/包名/databases/,该路径在非Root机型上不可直接访问,但在Android Studio的Device File Explorer里可以直接查看。

先在模拟器上启动应用并完成一次评教提交,然后在Android Studio的“Device Explorer”中找到数据库文件,导出后用SQLite工具查询。如果连查库工具都没有,可以用命令行的Sqlite3客户端,但要先用adb root拿到root权限。我要强调的是,如果你做的是网络版评教系统,验证接口比验证数据库更重要——在OkHttp的拦截器里加一个日志打印,把请求体原样打出来,确认studentId、courseId、comment和details数组的格式与你后端接口文档完全一致。网络调试这种需求,建议一开始就把OkHttp的HttpLoggingInterceptor加到依赖里,开发阶段保留,打包发布时关掉,这是最实用的调试习惯。

5.2 复盘:最值得加的两个进阶功能——草稿箱和导出Excel

课程设计做到“能评教、能统计、能管理”后,如果时间还有富余,我建议优先做两个功能,它们对最终答辩的评价影响最大。

第一个是评教草稿箱。学生在填表单页时按返回键,弹出提示框询问“是否保存草稿”。这个功能实现起来不复杂,核心是在onBackPressed里做拦截,将viewModel里scores数据和评语文本存入本地草稿表。避免因为误按返回键丢失所有打分的挫败感,提升演示时的完成度。

第二个是导出Excel文件。数据全在数据库里躺着,教师想看自己的历史评教记录不方便,管理员想打印归档更麻烦。这时用Apache POI的Android适配版或纯Java写CSV文件,把评教统计结果导出到getExternalFilesDir()目录。CSV文件的实现成本最低,只需要拼接逗号分隔的字符串然后写入输出流,打开方式也通用。这里注意文件名不能包含斜杠和中文?中文文件名在导出后传输时可能乱码,建议用yyyyMMdd_HHmmss格式命名,保留内部数据的中文内容。

5.3 真机兼容性验证的优先级排序

没有条件测试十台真机时,至少按这个优先级测三台:一部Android 8~9的旧机,一部Android 12~14的新机,一部带刘海屏或挖孔屏的设备。旧机重点验证启动速度和数据库兼容,新机重点看你写的运行时权限逻辑是否弹窗正常,异形屏设备验证的是页面顶部有没有控件被状态栏挡住的布局问题。

使用Android 12及以上设备时还需要注意导出组件和Intent过滤的变化,如果你的代码里写了针对第三方应用打开Activity的隐式Intent,可能需要在清单里加exported="false"声明防止系统拒绝。这属于Android 12适配中比较常见的一处改动,搜索引擎里搜“android 16 适配”能看到更多细节,但课程设计阶段只要保证自己在推荐环境下运行正常即可。

最后说一句经验之谈:这个项目的核心技术难度并不高,真正拉开差距的地方在于“边界处理”——评教是否允许重复提交、未打完分能不能提交、设备旋转后页面数据会不会丢失、换一台手机登录后能不能看到自己的历史记录。把这些边界一个个想清楚并用代码兜住,你的项目就已经超过多数照着一本书敲到一半就停的同学。希望帮到你,动手时遇到卡壳别硬撑,把日志打出来,顺着异常栈往上追,九成问题都能在半小时内定位。

本文还有配套的精品资源,点击获取

返回列表