拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Android模拟考试系统源码导入与改造实战:Gradle排错与题库设计

简介面向Android初学者的完整模拟考试系统源码包基于Java语言与Apache开源技术实现单选、多选、判断三类题目的在线答题、自动判分、自定义抽题等功能覆盖从界面搭建到数据流转的完整开发链路。包内共124个文件压缩包仅3.73MB38个txt文件存放题库与说明19个xml文件为界面布局7个java文件展示核心业务逻辑另有jar依赖、apk成品及class和dex编译产物便于对照理解构建流程。已有655人学习下载。适合想动手实践文件IO流读取、按钮监听器事件处理、TextView/RadioButton/CheckBox等控件组合使用及自动改卷逻辑的初学者该源码比零散教程更完整可运行导入工程后可按模块拆解每个功能点的实现思路。1. 拿到「基于android模拟考试系统源代码.zip」后先搞清楚它是什么「android模拟考试系统源代码.zip」这类压缩包在毕业设计、课程设计和二手源码群里出现频率很高。标题里叠了两层东西Android 工程本身以及「模拟考试系统」这个业务——题库、答题、倒计时、判分、成绩记录。zip 只是交付形态它意味着你拿到的是源代码而不是安装包一切工作从解压和编译开始。容易踩的认知误区是先读业务代码。实际上这类项目最先击败你的往往是构建环节Gradle 版本、SDK 版本、依赖仓库地址任何一个不对工程都起不来。把「基于android模拟考试系统源代码.zip」落地成真机上能跑、能换题库的 App时间主要花在从 zip 到 apk 的排错链路上。2. 从 zip 到可运行工程Android Studio 导入与 Gradle 排错2.1 解压后先认项目是 Eclipse 工程还是 Gradle 工程拿到压缩包不要急于双击解压然后拖进 Android Studio。先在终端里用无痕查看方式确认内部结构这一步能省掉之后「为什么导入后一片红」的一半排查时间unzip -l 基于android模拟考试系统源代码.zip | head -40unzip -l 只列出压缩包内文件清单不实际解压head -40 限制输出前 40 行。看什么看第一层目录下有没有 build.gradle、settings.gradle 这样的 Gradle 标志文件有没有 .project、.classpath 这样的 Eclipse 老工程标志以及 AndroidManifest.xml 是出现在顶层还是在嵌套子目录里。三种情况对应三种导入姿势zip 里的标志文件工程年代与类型导入姿势settings.gradle app/build.gradle现代 Gradle 工程Import Project 选含 settings.gradle 的目录project.properties .classpathEclipse ADT 年代工程让 Android Studio 自动转换后导入只有 AndroidManifest.xml没有 gradle 文件极老工程或源码快照建议建空工程再搬迁资源成本较高还有一类常见情况zip 第一层是个带版本号的文件夹真正的工程在第二层。这不影响运行但导入时选错目录会导致 Gradle 找不到 sync 目标。Windows 上解压中文文件名容易出现乱码建议用 7-Zip 以 UTF-8 规则解压避免 layout 资源路径引用失败。2.2 Android Studio 导入步骤与三个必调参数定位到工程根目录后用 File New Import Project 选择目标目录先让它做一次 Gradle Sync。这个阶段大概率飘红因为老工程的构建脚本还在用 compile、jcenter 这类旧写法。下面这份 app/build.gradle 是导入后最常见的最小可用形态android { compileSdkVersion 34 defaultConfig { applicationId com.example.exam minSdkVersion 21 targetSdkVersion 34 versionCode 1 versionName 1.0 } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation androidx.appcompat:appcompat:1.7.0 implementation androidx.recyclerview:recyclerview:1.3.2 }参数说明compileSdkVersion 决定用哪个 android.jar 参与编译新装的 Android Studio 一般自带 34 或更高它不影响最低运行版本。minSdkVersion 决定应用能装到多老的设备上模拟考试工具设 21Android 5.0足够覆盖绝大多数机型。targetSdkVersion 影响系统行为23 以上需要动态申请权限老源码如果不处理存储权限在 Android 6 上会拿不到文件写入能力。依赖里 implementation 替代了老旧的 compileandroidx 前缀的包说明工程已启用 Jetifier 迁移这两项是导入期最常出现的 diff 来源。仓库地址调整也是必做项老工程 build.gradle 里只写 jcenter() 的现在 sync 会卡在依赖解析失败。直接替换成 google() 和 mavenCentral()Gradle 8 时代这两个仓已经覆盖了绝大多数公共库。2.3 常见 Gradle 报错与「Invalid zip archive: could not find EOCD」排错时遇到频率最高的几类报错直接列处理方式报错原文含义与原因处理动作Invalid zip archive: could not find EOCD依赖包下载不完整或 gradle cache 里有损坏文件删除 ~/.gradle/caches/modules-2 下对应模块后重新 syncCould not find com.android.tools.build:gradle:X仓库缺 google()或所填 AGP 版本不存在查 AGP 与 Gradle 版本兼容表改到可用版本SDK location not found没有 local.properties 或 SDK 路径变更项目根目录建 local.properties写 sdk.dir...Invoke-customs only supported starting with Android OJava 8 语法特性没开在 compileOptions 里打开 1_8 支持第二行这个 Invalid zip archive 报错听起来像源码包损坏实际更多是 Gradle 下载依赖时断流导致缓存内 zip 不完整。删缓存重新同步比反复解压源码包有效得多。local.properties 记录的是本机 SDK 路径不应提交到版本库IDE 导入时会自动生成手动新建时注意 sdk.dir 里反斜杠需要转义。提示排错期间把解压路径改成纯英文无空格目录再做任何操作。这类跨度几年的源码对路径里的中文和支持普遍不好报错往往与代码无关。3. 读懂模拟考试系统的工程结构与题库数据层3.1 一套模拟考试 App 的典型目录结构源码能编译后下一步不是翻 UI而是先建立「工程地图」。模拟考试系统的结构高度相似一般长这样app/src/main/ ├── assets/ # 题库 JSON 或预置 .db 所在目录 ├── java/com/example/exam/ │ ├── activity/ # 页面登录、主页、考试、成绩 │ ├── adapter/ # 题目列表 RecyclerView Adapter │ ├── db/ # SQLiteOpenHelper 与 DAO │ ├── model/ # Question、User 等实体类 │ └── utils/ # 倒计时、文件读写等工具 └── res/layout/这类源码的架构大多停留在 MVC 或轻量 MVPActivity 里直接操作 SQLiteOpenHelperAdapter 负责绑定 ListView。它被诟病不好测、不好维护但对理解一个系统的完整运行链路这种直来直去的结构反而友好。别急着重构 MVVM先跑通再谈改造。还需要留意版本差异zip 里代码的原始版本往往和演示截图不同历届同学或同事在原版上改过命名和注释对不上甚至混着两个功能重复的页面。判断依据不是 README而是 AndroidManifest.xml 里 launcher activity 指向谁——以它为阅读起点大多数源码十分钟内能理出主链路。3.2 题库落地SQLite、JSON 还是 SharedPreferences题库是整个系统的数据核心。源码里常见的落地方案有三种方案优点缺点适合场景assets 放 JSON启动解析入库题库可替换改题不重新编译首次启动解析耗时编码易错课程设计频繁换题assets 预置 .db首启拷贝到 databases查询快SQL 直接可用改题要重建库包变大正式发布型考试 AppSharedPreferences 存题代码量最小没法按科目/题型查询几十道题的演示 Demo我一般会选 JSON 解析入库源码头一回交付的题库往往是 Excel 转出来的JSON 一眼能看出哪里错。入库之后随机抽题、按科目查题都走 SQL不用全量读文件判分和错题本也有地方挂。预置 .db 方案维护成本偏高改一道题要重新导出数据库不适合练习性质的项目。3.3 建表语句与初始化数据导入题目表至少要覆盖题型、题干、选项、答案和解析这样判分、错题本、成绩分析才都站得住CREATE TABLE t_question ( id INTEGER PRIMARY KEY AUTOINCREMENT, subject TEXT NOT NULL, -- 科目如法律法规 qtype INTEGER NOT NULL DEFAULT 0, -- 0 单选 1 多选 2 判断 content TEXT NOT NULL, -- 题干 opt_a TEXT, opt_b TEXT, opt_c TEXT, opt_d TEXT, answer TEXT NOT NULL, -- A、ABD、对 analysis TEXT -- 答案解析 ); CREATE INDEX idx_question_subject ON t_question(subject);字段与命名说明qtype 用 0/1/2 整数而不是中文枚举判分时 switch 更直接。answer 统一存字符串单选存 A多选存 ABD判断存 对/错把比较规则集中在判分函数里。subject 冗余在题目表而不是单独建科目表正好避免小型系统出现无谓的 Join。初始化导入常见写法private String loadAssets(String path) throws IOException { StringBuilder sb new StringBuilder(); try (BufferedReader br new BufferedReader( new InputStreamReader(getAssets().open(path), UTF-8))) { String line; while ((line br.readLine()) ! null) sb.append(line); } return sb.toString(); } public int importQuestions(SQLiteDatabase db) { db.beginTransaction(); int count 0; try { JSONArray array new JSONArray(loadAssets(questions.json)); for (int i 0; i array.length(); i) { JSONObject obj array.getJSONObject(i); ContentValues cv new ContentValues(); cv.put(subject, obj.getString(subject)); cv.put(qtype, obj.getInt(qtype)); cv.put(content, obj.getString(content)); cv.put(opt_a, obj.optString(opt_a)); cv.put(opt_b, obj.optString(opt_b)); cv.put(opt_c, obj.optString(opt_c)); cv.put(opt_d, obj.optString(opt_d)); cv.put(answer, obj.getString(answer)); cv.put(analysis, obj.optString(analysis)); db.insert(t_question, null, cv); } db.setTransactionSuccessful(); } catch (Exception e) { Log.e(Import, 题库导入失败, e); } finally { db.endTransaction(); } return count; }逻辑说明读文件强制 UTF-8避免 Windows 上 GBK 编辑器导出的乱码JSONArray 一次性解析只适用几千道题的规模库文件到几十 MB 时换成逐题流式解析整段导入包在事务里几千行写入从秒级压到毫秒级失败回滚不会留下半个题库。optString 对缺失选项返回空串而不是抛异常适配没有 D 选项的题目。4. Android 答题页的核心实现倒计时、交卷与自动判分4.1 页面组织Activity 承载答题流程答题页是模拟考试系统的核心。典型形态是顶部倒计时与进度条中间 RecyclerView 承载题目底部上一题/下一题/交卷。页面用一个 Activity 控制状态题目数据在启动时从数据库拉取public class ExamActivity extends AppCompatActivity { private ListQuestion questions; // 当前试卷题目 private MapInteger, ListInteger userAnswers; // questionId - 已选选项 private long remainMs; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState null) { long examId getIntent().getLongExtra(exam_id, 1); questions loadQuestions(examId); userAnswers new HashMap(); } else { userAnswers restoreAnswers(savedInstanceState); remainMs savedInstanceState.getLong(remain_ms); } } }状态保存这一段最容易有坑来电、旋转屏幕都会触发 Activity 重建答案只存在内存集合里的话一切换就全丢。把 userAnswers 序列化进 Bundle重建时在 onCreate 恢复比在 onResume 里重新加载题库可靠。真机测试时多转几圈屏幕重点观察答案是否还在。4.2 倒计时与自动交卷CountDownTimer 的正确姿势倒计时常见的实现坑有两个用 Handler Runnable 自己算时间戳在 onResume 里直接 new CountDownTimer(totalTime)。前者容易在 Activity 重建后丢状态后者会在切后台返回后把剩余时长重置。正确姿势是持有剩余时长字段重建后继续private CountDownTimer timer; private void startTimer() { if (timer ! null) { timer.cancel(); } timer new CountDownTimer(remainMs, 1000) { Override public void onTick(long millisUntilFinished) { remainMs millisUntilFinished; tvCountdown.setText(formatTime(remainMs)); } Override public void onFinish() { submitExam(SUBMIT_TIMEOUT); // 自动交卷 } }.start(); } Override protected void onStart() { super.onStart(); if (pausedAt ! 0) { remainMs - System.currentTimeMillis() - pausedAt; pausedAt 0; } startTimer(); }参数说明构造参数两个总时长毫秒数与 tick 间隔毫秒数1000 表示每秒刷新一次倒计时。onTick 里只更新 UI 和成员变量不放数据库写入长时间占用主线程会让答题卡顿。onFinish 触发自动交卷调用链上要加「是否已手动交卷」标志否则定点交卷和手动交卷会重复执行。后台停留补偿onStop 时记 pausedAtonStart 里用系统时钟差值减掉不处理这个锁屏十分钟回来倒计时还在原地走。onDestroy 里必须 timer.cancel()避免泄漏。4.3 交卷评分与错题统计自动交卷后进入判分阶段。题目量大时别在主线程逐题比对放到子线程或线程池执行否则交卷按钮会像卡死一样。判分代码常见形态private ScoreResult calcScore(ListQuestion questions, MapInteger, ListInteger userAnswers) { ListQuestion wrongList new ArrayList(); int correct 0; for (Question q : questions) { ListInteger sel userAnswers.get(q.getId()); if (q.match(sel)) { correct; } else if (sel ! null !sel.isEmpty()) { wrongList.add(q); // 错题本数据来源 } } return new ScoreResult(correct, questions.size() - correct, wrongList); }Question.match 里按题型分支boolean match(ListInteger sel) { if (qtype TYPE_SINGLE) { return sel ! null sel.size() 1 sel.get(0).equals(answer.get(0)); } else if (qtype TYPE_MULTI) { return sel ! null new HashSet(sel).equals(new HashSet(answer)); } else if (qtype TYPE_JUDGE) { return sel ! null sel.get(0).equals(answer.get(0)); } return false; }判定细节多选题要避免顺序问题用户选 A,C 与标准答案 C,A 必须判一致统一转 Set 比较是最直接的做法更严苛的多选规则少选、错选都扣分需要再写容错逻辑先判断是否存在未选中的正确项再判断是否选中了错误项。判断题常见的坑是数据里答案字符串「对/错」与代码里装成 int 0/1 不一致在导入层统一规则不要在 match 里做兼容。题型判定方式常见问题单选题唯一选项与标准答案相等下标与选项字母混用多选题用户选择集合与答案集合完全相等顺序不同导致误判判断题对/错或 0/1相等中文与数字混存5. 把模拟考试源代码改成自己的系统换题库、加错题本与验证技巧5.1 用 Python 脚本把 Excel 转成题库 JSON拿到源码后第一个实用改造是换题库。常见做法是写脚本让 Excel 表直接变成 assets 里的 questions.jsonimport json from openpyxl import load_workbook wb load_workbook(questions.xlsx) rows [] for r in wb.active.iter_rows(min_row2, values_onlyTrue): _, subject, qtype, content, a, b, c, d, answer, analysis r rows.append({subject: subject, qtype: qtype, content: content, opt_a: a, opt_b: b, opt_c: c, opt_d: d, answer: answer, analysis: analysis}) with open(assets/questions.json, w, encodingutf-8) as f: json.dump(rows, f, ensure_asciiFalse, indent2)ensure_asciiFalse 与 indent2 保证中文可读、后续 diff 可追踪。替换 JSON 后要卸载重装或清除应用数据因为导入逻辑通常只在首次启动执行一次。5.2 用 adb 验证题库落库顺手解决「当前不会命中断点」换完题库别急着点按钮先在调试期确认数据落库。应用处于 debug 可调试状态时用 adb 配合 run-as 把私有目录里的数据库拉出来adb shell run-as com.example.exam sh -c \ cat databases/exam.db /sdcard/exam_backup.db adb pull /sdcard/exam_backup.db . sqlite3 exam_backup.db SELECT qtype, count(*) FROM t_question GROUP BY qtype;run-as 只对 debuggable 应用生效release 包会直接拒绝这是它的边界。真机调试记得在系统设置里打开开发者选项与 USB 调试。另一个高频告警「当前不会命中断点」多数是源代码文件与 dex 行号不同步Clean Project 后重新 Run 就能对齐。5.3 在 DAO 层给模拟考试系统加错题本错题本是这类源码通常没有、但加一个也不难的模块。改动点集中在 DAO建一张 t_wrong_book交卷时把 wrongList 写进去错题列表页用联表查询。db.execSQL(CREATE TABLE IF NOT EXISTS t_wrong_book ( question_id INTEGER PRIMARY KEY, wrong_answer TEXT, create_time INTEGER)); for (Question q : wrongList) { db.delete(t_wrong_book, question_id?, new String[]{String.valueOf(q.getId())}); ContentValues cv new ContentValues(); cv.put(question_id, q.getId()); cv.put(wrong_answer, sampleAnswer); cv.put(create_time, System.currentTimeMillis()); db.insert(t_wrong_book, null, cv); }先删再插是为了让同一道题只保留最近一次错误记录建表语句放进 DBHelper 的 onUpgrade 并把版本号加 1避免老用户升级后表不存在直接白屏。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门