ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

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

2026/9/30 4:43:16 拓冰建站 浏览量
基于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 MutableLiveDataListIndicatorItem indicators new MutableLiveData(); private final HashMapInteger, Integer scores new HashMap(); private final MutableLiveDataString comment new MutableLiveData(); private final int courseId; public EvaluateViewModel(NonNull Application application, int courseId) { super(application); this.courseId courseId; loadIndicators(); } private void loadIndicators() { // 从本地数据库或网络加载该课程的评教指标 ListIndicatorItem 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() { ListIndicatorItem items indicators.getValue(); if (items null || items.isEmpty()) return true; for (IndicatorItem item : items) { if (getScore(item.getId()) 0) return false; } return true; } }逻辑要点是打分时不直接操作界面控件而是写进scores这个HashMapActivity被系统回收后重建时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.AdapterCourseListAdapter.ViewHolder { private final ListCourseWithStatus data; private final OnItemClickListener listener; public CourseListAdapter(ListCourseWithStatus 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但没指定charsetUTF-8。解决OkHttp中构造RequestBody时写RequestBody.create(jsonStr, MediaType.parse(application/json; charsetutf-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 89的旧机一部Android 1214的新机一部带刘海屏或挖孔屏的设备。旧机重点验证启动速度和数据库兼容新机重点看你写的运行时权限逻辑是否弹窗正常异形屏设备验证的是页面顶部有没有控件被状态栏挡住的布局问题。使用Android 12及以上设备时还需要注意导出组件和Intent过滤的变化如果你的代码里写了针对第三方应用打开Activity的隐式Intent可能需要在清单里加exportedfalse声明防止系统拒绝。这属于Android 12适配中比较常见的一处改动搜索引擎里搜“android 16 适配”能看到更多细节但课程设计阶段只要保证自己在推荐环境下运行正常即可。最后说一句经验之谈这个项目的核心技术难度并不高真正拉开差距的地方在于“边界处理”——评教是否允许重复提交、未打完分能不能提交、设备旋转后页面数据会不会丢失、换一台手机登录后能不能看到自己的历史记录。把这些边界一个个想清楚并用代码兜住你的项目就已经超过多数照着一本书敲到一半就停的同学。希望帮到你动手时遇到卡壳别硬撑把日志打出来顺着异常栈往上追九成问题都能在半小时内定位。本文还有配套的精品资源点击获取