行业资讯
📅 2026/9/7 6:51:00
Android Studio学生选课系统开发实战:SQLite事务与RecyclerView刷新
简介面向Android开发学习者的一份学生选课系统完整工程基于Android Studio构建适合用于课程设计、毕业设计或初学App综合实践。压缩包共620个文件约56.75MB包含Java源码、XML布局与清单文件、Gradle构建配置、JSON数据文件以及已编译的dex/class/apk产物目录中还可看到AndroidResources、资源索引和本地SDK配置便于直接导入工程查看或对照构建流程学习。已有2084人学习下载。通过解压可得到学生选课系统的前端界面、课程列表展示、本地数据持久化及与后端交互等模块的参考实现同时工程内保留Gradle脚本、版本忽略规则等工程化细节适合用于理解Android项目结构、熟悉常用组件和构建工具也有助于在现成代码基础上做二次开发与功能扩展。 搞Android课设的同学十有八九都会碰上“学生选课系统”这个题目。我当年第一次拿到这个需求时脑子里全是“一个登录页一个课程列表一个选课按钮”觉得一天就能搞定结果从数据库设计到列表刷新踩了一整周的坑。这篇文章就用我后来重构过的完整思路把Android Studio做学生选课系统的整个流程掰开揉碎讲清楚从表结构怎么建、事务怎么开到RecyclerView为什么不刷新、SQLite升级为什么崩溃全部配置和代码都给你能直接抄的版本。这套内容适合三类人正在做课程设计的学生、刚学完Android基础想练手的小白以及被各种诡异Bug折磨到怀疑人生的自学者。我会把我在真实开发中验证过的最优方案写出来你照着落地至少能少走我当初踩过的那些弯路。1. 项目启动前的整体构思1.1 为什么选Android Studio做学生选课系统学生选课系统这个题目看起来简单但它天然包含完整的CRUD、多表关联、业务规则校验比如选课冲突、人数上限和复杂界面交互正好覆盖Android开发的核心知识点。选课系统在技术上最适合用Android Studio写原生应用原因有三一是Android Studio自带SQLite支持和可视化数据库工具不需要额外搭服务器就能演示完整流程二是这个项目用到的大量列表展示、表单交互、状态管理用原生的RecyclerView和Activity就能完美解决没必要引入跨平台框架增加学习成本三是你做课设交作业时老师大概率要求看APP源码和运行效果原生项目在答辩时展示起来最直观代码结构也最容易讲清楚。另一个实际问题是很多人一上来就想着用MySQL后端API结果发现还要配Tomcat、处理网络请求、搞JSON解析工作量瞬间翻倍。学生选课系统本质上是一个单机演示级应用用SQLite存储完全够用后续如果你想扩展成客户端/服务器模式DAO层和数据模型仍然可以复用迁移成本很低。1.2 功能边界与技术选型清单一个完整的学生选课系统至少需要三类角色功能但课设场景下一般只做学生端最多加一个管理员端用来管理课程。我的建议是学生端做四个核心模块登录认证、课程浏览、选课/退课、我的课表查询。教务管理类的功能课程增删改、学生信息维护可以做一个极简管理界面或者直接用SQLite的预置数据代替不必为了功能全而拖垮开发进度。技术选型上我当时踩过不少坑最终稳定跑通的方案如下模块技术方案选型理由开发工具Android Studio Hedgehog或更新版本自带Gradle配置和模拟器调试方便编程语言Java课设资料最多遇到问题最容易搜到答案Kotlin也可以但没必要冒险数据库SQLite SQLiteOpenHelper零配置、单文件、自带事务适合单机演示列表展示RecyclerView RecyclerView.Adapter强制使用ViewHolder复用性能比ListView好登录状态SharedPreferences轻量键值对存储保存当前登录学生ID事务与线程事务开启 新线程或Executor避免ANR保证选课写入原子性这个组合的核心思路就是数据层用SQLite扛住所有增删改查界面层用RecyclerView统一管理列表业务层通过事务保证选课这类复合操作的完整性。2. 数据库设计与核心表结构2.1 student、course、选课表的设计要点数据库设计是这个项目最容易被忽视但又最关键的一步。很多人上来就建一张大表把学生信息、课程信息、选课状态全部塞进去结果查询逻辑越写越乱。我实践下来最合理的方案是三张表学生表、课程表、选课关系表。学生表存储固定信息字段设计如下CREATE TABLE student ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_no TEXT UNIQUE NOT NULL, password TEXT NOT NULL, name TEXT NOT NULL, major TEXT );课程表存储课程基本信息关键是设置容量和已选人数的默认值CREATE TABLE course ( id INTEGER PRIMARY KEY AUTOINCREMENT, course_no TEXT UNIQUE NOT NULL, course_name TEXT NOT NULL, teacher TEXT NOT NULL, credit REAL DEFAULT 2.0, capacity INTEGER DEFAULT 50, selected INTEGER DEFAULT 0, schedule TEXT );选课关系表是连接学生和课程的桥梁一定要做联合唯一约束CREATE TABLE enroll ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id INTEGER NOT NULL, course_id INTEGER NOT NULL, enroll_time TEXT DEFAULT (datetime(now, localtime)), UNIQUE(student_id, course_id) );这里有个很容易犯的错误选课关系表里的学生ID和课程ID存的是什么。存自增主键id没问题但如果你从登录状态里拿到的是student_no就得先去学生表查出id再做关联。更简洁的做法是选课表直接存student_no和course_no牺牲一点理论上的规范化程度换来的是查询时少两次join。在课设项目里我推荐后者因为数据量不大性能差异察觉不到但代码可读性和写起来的手感会好很多。CREATE TABLE enroll2 ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_no TEXT NOT NULL, course_no TEXT NOT NULL, enroll_time TEXT DEFAULT (datetime(now, localtime)), UNIQUE(student_no, course_no) );2.2 事务处理与选课冲突判断选课操作本质上是两步先检查课程是否还有余量再插入选课记录并更新课程表的selected字段。这两个动作必须在一个事务里完成否则可能出现“查到有余量但插入时已满员”的并发问题。实现上通过SQLiteOpenHelper的子类拿到可写数据库然后手动开启事务SQLiteDatabase db dbHelper.getWritableDatabase(); db.beginTransaction(); try { // 检查课程当前已选人数 Cursor cursor db.rawQuery( SELECT capacity, selected FROM course WHERE course_no ?, new String[]{courseNo}); if (cursor.moveToFirst()) { int capacity cursor.getInt(0); int selected cursor.getInt(1); if (selected capacity) { cursor.close(); return 课程已满员; } } cursor.close(); // 插入选课记录 ContentValues enrollValues new ContentValues(); enrollValues.put(student_no, studentNo); enrollValues.put(course_no, courseNo); db.insertOrThrow(enroll2, null, enrollValues); // 更新课程已选人数 db.execSQL(UPDATE course SET selected selected 1 WHERE course_no ?, new Object[]{courseNo}); db.setTransactionSuccessful(); return 选课成功; } finally { db.endTransaction(); }注意几个细节检查余量和插入记录必须放在同一事务里不能先查再开事务cursor用完要立刻关闭否则下次查询会报“数据库锁”错误insert使用insertOrThrow而不是insert方便捕获主键冲突异常。这里用到的execSQL更新已选人数比先查出来再加一省很多事而且天然线程安全——数据库级别的行锁保证了不会出现多人同时选同一门课导致超员的极端情况。3. 界面实现与关键代码3.1 登录页与全局状态管理登录页看起来简单但它是整个APP的入口状态管理做不好后面全乱套。我的做法是登录成功后把学生号写入SharedPreferences之后所有页面都从SharedPreferences里读取当前用户。登录逻辑的核心代码public void login(View view) { String studentNo etStudentNo.getText().toString().trim(); String password etPassword.getText().toString().trim(); if (studentNo.isEmpty() || password.isEmpty()) { Toast.makeText(this, 学号和密码不能为空, Toast.LENGTH_SHORT).show(); return; } SQLiteDatabase db dbHelper.getReadableDatabase(); Cursor cursor db.rawQuery( SELECT name FROM student WHERE student_no ? AND password ?, new String[]{studentNo, password}); if (cursor.moveToFirst()) { String name cursor.getString(0); SharedPreferences sp getSharedPreferences(user_info, MODE_PRIVATE); SharedPreferences.Editor editor sp.edit(); editor.putString(student_no, studentNo); editor.putString(student_name, name); editor.apply(); Intent intent new Intent(this, MainActivity.class); startActivity(intent); finish(); } else { Toast.makeText(this, 学号或密码错误, Toast.LENGTH_SHORT).show(); } cursor.close(); }这里要注意两个问题。第一getSharedPreferences的MODE_PRIVATE是必须要带的参数高版本Android强制要求私有模式不要用MODE_WORLD_READABLE否则直接Crash。第二登录跳转后记得调用finish()这样用户按返回键不会回到登录页否则体验很差。如果你还想做“记住密码”也是存在SharedPreferences里但不建议明文保存密码演示项目可以直接存明文真实项目必须做加密处理。3.2 课程列表与选课/退课操作课程列表是学生选课系统的颜值担当用RecyclerView实现时最大的坑是Adapter里的数据源更新后RecyclerView不刷新。很多人遇到这个问题第一反应是改布局其实是没搞清楚notifyDataSetChanged只是通知界面重新绑定数据如果数据源本身没变或者变的是数据库里的数据而不是内存里的List那怎么通知都没用。我的标准做法是每次从数据库查询课程列表时都返回一个新的List然后调用adapter.setData(newList)更新Adapter内部的数据源再调用notifyDataSetChanged()。可以在Adapter里写一个setData方法public void setData(ListCourse courses) { this.courseList.clear(); this.courseList.addAll(courses); notifyDataSetChanged(); }在Activity里查询数据时必须开子线程或使用Executor不能在主线程直接查数据库否则数据量大一点就会ANR。我习惯用ExecutorServiceExecutorService executor Executors.newSingleThreadExecutor(); executor.execute(() - { ListCourse courses dbHelper.queryAllCourses(); runOnUiThread(() - adapter.setData(courses)); });你可能会问runOnUiThread和Executor搭配为什么要这样做Executor里的代码跑在子线程不能直接操作UI组件runOnUiThread会把更新界面的代码切回主线程执行。Android的UI线程刷新机制就是这么规定的不用纠结记住这个模式就行。列表项的布局我采用LinearLayout嵌套三个TextView课程名加粗、教师和上课时间灰色小字右侧放一个Button。选课按钮的点击事件通过接口回调处理在Adapter里定义一个OnEnrollClickListenerpublic interface OnEnrollClickListener { void onEnrollClick(Course course); }然后在Activity里实现接口执行真正的选课或退课逻辑Override public void onEnrollClick(Course course) { String studentNo sp.getString(student_no, ); String result dbHelper.enrollCourse(studentNo, course.getCourseNo()); Toast.makeText(this, result, Toast.LENGTH_SHORT).show(); // 刷新列表 refreshCourseList(); }这里有个细节容易忽略选课成功后调用refreshCourseList()重新查库但refreshCourseList里使用的还是之前那个adapter对象只是数据变了。只要setData方法正确更新了数据源notifyDataSetChanged就会让界面跟着变不需要重新setAdapter。3.3 我的课表与数据回显“我的课表”页面本质上是选课关系表和学生表、课程表的联合查询。SQL写起来不复杂但很多人不知道怎么把查询结果传到一个新的Activity里。我推荐的做法是再写一个查询方法返回List public ListCourse queryMyCourses(String studentNo) { ListCourse courses new ArrayList(); SQLiteDatabase db getReadableDatabase(); Cursor cursor db.rawQuery( SELECT c.course_no, c.course_name, c.teacher, c.credit, c.schedule FROM course c INNER JOIN enroll2 e ON c.course_no e.course_no WHERE e.student_no ?, new String[]{studentNo}); while (cursor.moveToNext()) { Course course new Course(); course.setCourseNo(cursor.getString(0)); course.setCourseName(cursor.getString(1)); course.setTeacher(cursor.getString(2)); course.setCredit(cursor.getDouble(3)); course.setSchedule(cursor.getString(4)); courses.add(course); } cursor.close(); return courses; }再开一个MyCourseActivity复用相同的RecyclerView布局和Adapter只是数据来源从全部课程换成了我的课程。列表项的按钮文案也要区分我的课表里按钮显示“退课”仍然通过同一个OnEnrollClickListener回调但Activity里执行的是退课逻辑。这样可以最大程度复用代码不需要写两套Adapter。退课逻辑和选课相似也是事务操作但方向相反先从enroll2表删除记录再把course表的selected字段减1。删除时如果不用事务也能跑但为了保持代码风格一致我建议还是写在事务里防止删除记录成功但更新人数失败造成的脏数据。4. 常见问题与排查技巧实录4.1 数据库升级崩溃的根源与解法SQLiteOpenHelper的onUpgrade方法如果没写或者用的方式是删表重建那么在真机上升级APP时用户的数据就会全部丢失。更崩溃的是如果你改了表结构但没改DATABASE_VERSION甚至会出现“no such column”的运行时异常。这个问题在开发阶段最坑因为你改了表的字段模拟器上还留着旧版本数据库跑起来就一直报错。解法是养成两个习惯第一每次修改表结构必须把DATABASE_VERSION加1第二onUpgrade里写清楚迁移逻辑而不是直接drop table。课设阶段最简单可靠的迁移方式Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 从旧版本迁移如果只是课设备份后重建 db.execSQL(DROP TABLE IF EXISTS enroll2); db.execSQL(DROP TABLE IF EXISTS course); db.execSQL(DROP TABLE IF EXISTS student); onCreate(db); }如果你的项目有真实数据需求不能用这种暴力的迁移方式应该用ALTER TABLE和临时表把数据先备份再恢复。但做课设时别在迁移逻辑上浪费时间优先保证开发期改表结构不会疯狂崩溃。还有一个小技巧开发调试时直接把APP卸载再重装SQLite数据库文件会跟着删除等于彻底重置数据库比改版本号更快。4.2 选课并发冲突与事务引发的“假死”学生选课系统虽然单机演示时一般不会真的并发但多线程测试时经常遇到数据库锁异常。SQLite的锁机制是文件级别的如果一个连接长时间持有写锁其他连接想读数据库时就会报“database is locked”。实际开发中我遇到过一种离奇情况事务里的insert执行成功但之后没有调用setTransactionSuccessful就endTransaction了导致整个事务回滚数据看起来没写入但又没报错。这就是“假死”现象排查时最迷惑。所以事务的代码结构千万要记牢beginTransaction - 业务操作 - setTransactionSuccessful - endTransaction。setTransactionSuccessful一旦调用endTransaction就会提交不调用则回滚。这个顺序错了数据就悄悄丢了。另外一个建议是所有数据库操作都走同一个SQLiteOpenHelper实例。不要在每个Activity里new一个helper否则很容易出现多个连接互相锁的情况。我习惯用一个单例或者Application里持有helper对象全局只开一个数据库连接public class MyApplication extends Application { private static DBHelper dbHelper; Override public void onCreate() { super.onCreate(); dbHelper new DBHelper(this); } public static DBHelper getDbHelper() { return dbHelper; } }4.3 RecyclerView数据刷新与界面闪烁问题RecyclerView不刷新是新手问得最多的问题。症状通常是数据库里已经插入了数据但界面上还是老样子。大部分原因就是我在3.2里说的数据源没有更新。但还有一种隐蔽情况你更新了数据库也调用了notifyDataSetChanged界面却还是不动这时要检查Adapter里的getItemCount方法是不是写死了返回某个固定值。还有一种情况是界面刷新时闪屏或白屏通常是因为你在主线程里同步查数据库导致UI卡顿。把数据库操作挪到子线程后闪屏会消失但会出现一个更微妙的问题用户点击选课按钮后Toast提示成功但列表数据是异步回来的会有一瞬间的延迟看起来像是没执行。我的处理方式是在refreshCourseList里先清空Adapter的数据再异步查库这样用户能明确感知到列表在刷新体验比无脑等待好很多。public void refreshCourseList() { adapter.setData(new ArrayList()); // 先清空 executor.execute(() - { ListCourse courses dbHelper.queryAllCourses(); runOnUiThread(() - adapter.setData(courses)); }); }5. 从课设到毕业设计的扩展思路5.1 数据可视化与统计报表如果你不想止步于基础选课功能最值得扩展的方向是数据可视化。学生端可以增加“各专业选课人数统计”管理员端可以增加“热门课程排行榜”。技术上用MPAndroidChart这个开源图表库就能实现它支持柱状图、饼图、折线图接入方式很简单在build.gradle里加一行依赖就行。数据来源还是SQLite用GROUP BY和COUNT统计后转成图表需要的实体类。这个模块的难点在于数据模型的转换。SQLite查询出来的Cursor是扁平的而图表库需要的是List 所以需要额外的数据处理层。我当时花了不少时间在这个地方但做完以后整个项目的档次完全不一样答辩时老师对图表模块的兴趣明显高于基础的列表展示。5.2 客户端/服务器架构改造如果导师要求做成真正的联网应用就要把SQLite替换成远程MySQL并引入HTTP通信层。这时建议不要大改业务层逻辑而是抽象出一个Repository接口本地版用SQLite实现远程版用OkHttp Retrofit实现。界面层完全不用改动只需要在调用数据方法时切换实现类的实例即可。这种改造背后的思想叫依赖注入不管用什么框架核心就是让上层不依赖具体的数据源实现。Android原生没有内置的依赖注入框架但可以手写一个简单的ServiceLocator模式把数据库实现和网络实现统一注册进去。这样从课设升级成毕设代码结构依然干净也方便你在答辩时讲清楚架构演进的过程。我在实际开发中比较深的体会是学生选课系统虽然名字听着老套但它把Android开发的数据库、UI、异步、状态管理、异常处理全部串起来了。做完这个项目再去上手别的APP开发很多套路都是相通的。如果你正在做这个题目建议先照着文中的表结构和事务思想把后端数据层跑通再逐步完善界面不要一上来就追求高颜值UI数据层稳了界面只是时间问题。本文还有配套的精品资源点击获取