简介:这是一份基于Android Studio开发的学生选课系统项目资源,适合正在学习Android开发、希望从完整项目中掌握工程结构与常见技术要点的学习者。压缩包共620个文件,体积约56.75MB,涵盖java源代码、xml界面与资源文件、json配置、class与dex编译产物、apk安装包以及gradle构建脚本等类型,既有可直接阅读的源码,也有可供运行的构建结果,便于对照学习。内容围绕选课场景展开,涉及RecyclerView课程列表、SQLite数据持久化、网络通信、Loader与LiveData等知识点,能够帮助读者理解Android应用的UI设计、数据存储与异步处理流程。目前已有2084人学习下载,对于需要项目实战参考或课程设计借鉴的开发者来说,是一份可落地的参考资料。
1. 项目全貌:先搞清楚你要做的是一个什么系统
先说个题外话。“androidstdio”这个拼写我见的频率相当高,实际上正确写法是Android Studio,不过这并不影响我们理解这个题目的本质——它是计算机软工类课程设计里出现率极高的一道题:一个跑在 Android 手机上的学生选课系统。很多读者第一次看到这个题目时,脑子里的第一反应是“我要做一个 APP 给全校学生选课用”,然后开始幻想登录、选课、退课、课表、消息推送、管理后台全套上齐。真这么做,大概率会在中期检查时翻车。
冷静下来拆解一下,这个项目的核心其实很聚焦:学生端是主战场,日常操作无非是登录、浏览课程、选课、退课、查看已选课程;管理端即便要做,也不是重点,能有一个简单地添加课程、查看选课人数的入口就够了。整体数据量级也就是“几百个学生、几十门课程”的水准,完全不需要分布式架构,不需要消息队列,更不需要微服务。想清楚这一点,你选择的每一项技术都是为了“快速、稳定、能演示、能答辩”,而不是为了炫技。
我在指导这类项目时经常和学生说一句话:选课系统这事,本质上就是一个“带约束条件的购物车”。用户浏览商品(课程),把商品加入购物车(选课),但购物车有规则——库存不能超(课程容量),同一时间段不能加两件“同类型冲突商品”(课程时间冲突)。一旦你把这个类比放在脑子里,整个系统的逻辑边界就非常清晰了,写代码的时候也不会东一榔头西一棒槌。
这篇博文适合正在做课程设计、毕业设计,或者想拿一个完整 Android 项目练手的朋友。我会从需求、技术选型、数据库设计、核心代码实现到排坑经验,按一个过来人的视角讲透。文章中涉及的具体实现方案,是我基于大量同类项目总结出的最稳妥路线,你完全可以照着落地。
2. 核心设计:把数据库和核心流程先理顺
2.1 三张表打天下:学生、课程、选课记录
数据库设计是这个项目最不该偷懒的模块。我见过太多人上来就建了七八张表,角色表、权限表、院系表、教师表……最后发现用到的就三张。实际上一个典型的学生选课系统,三张表足够覆盖所有核心场景。
第一张是student(学生表),字段基本就是学号、姓名、密码、班级。学号建议设为主键,既是登录账号又是唯一标识,这样登录时你可以直接用“学号 + 密码”作为查询条件,少一次关联查询。第二张是course(课程表),字段包括课程编号、课程名称、授课教师、上课时间、上课地点、学分、容量、已选人数。这里的“上课时间”建议用字符串直接存,比如“周一 3-4 节”,而不是拆成星期几和第几节两个字段——为什么?因为你做的是 Android 应用,后台不是企业级 ERP,简单直白的数据格式能帮你省掉大量解析逻辑。第三张是enrollment(选课记录表),字段为记录 ID、学号、课程编号,学号和课程编号联合起来做唯一约束,避免同一个人重复选同一门课。
也许你会问:要不要把“已选人数”实时算出来,而不是存一个字段?理论上是,但实操中我建议直接在 course 表里维护一个选课人数计数。原因很简单:这个项目的数据量级根本不需要考虑并发一致性,每次选课成功就把计数加一,退课就减一,查询课程列表时直接读,不用写复杂的 COUNT 语句,UI 刷起来还快。这是典型的“用空间换逻辑简化”的思路,答辩时你也能讲清楚设计动机。
2.2 选课规则怎么落地:容量、冲突、重复
选课系统的核心不只是“能存数据”,而是规则校验要严谨。我把规则拆成了三关,每次选课操作都要依次通过这三关才能落库。
第一关是重复性校验:先查询 enrollment 表里是否已经存在该学生的这门课记录。有就直接提示“该课程已选,请勿重复操作”,不需要再往下走。第二关是容量校验:比较 course 表里的已选人数和容量。已选人数 >= 容量时,直接拒绝,并且前端要友好提示“课程名额已满”。这里有个细节值得注意——不要只在学生点“选课”按钮时才判断容量,课程列表展示时就应该把已满的课程置灰或标注“已满”,让规则前置。第三关是时间冲突校验:找出这个学生已选的所有课程,逐一比对上课时间。如果新选课程的时间段和已选课程有重叠,必须拒绝。
时间冲突这个逻辑,我在第一次做的时候偷懒只做了“星期几”维度的比较,结果出了 bug:同一周的周二和周五不冲突,这没问题,但如果一门课是“周一 1-2 节”,另一门是“周一 3-4 节”,它们星期相同但时间错开了,其实是不冲突的。后来我把上课时间解析成“星期 + 开始节次 + 结束节次”三个值来做区间重叠判断,才算真正解决问题。这块逻辑是选课系统的“灵魂”,也是答辩时最容易被老师追问的地方,值得多花点心思。
public boolean hasTimeConflict(String newTime, List<String> selectedTimes) { // 将"周一 3-4节"解析为 星期几 + 起止节次 TimeSlot newSlot = TimeSlot.parse(newTime); for (String selected : selectedTimes) { TimeSlot slot = TimeSlot.parse(selected); // 同一星期才判断区间是否重叠 if (slot.week == newSlot.week && newSlot.start <= slot.end && slot.start <= newSlot.end) { return true; } } return false; }3. 实操过程与关键实现:从新建项目到跑通选课流程
3.1 项目结构与界面层的组织方式
确定好数据库设计之后,就到了真正动手指建项目的时候。用 Android Studio 新建工程时,很多初学者喜欢默认生成一个 MainActivity,然后所有代码全堆在里面——Activity 里写数据库操作、写列表适配、写点击事件、写网络请求。这种写法不是不能跑,但到了后期每加一个功能都要在几百行的 Activity 里翻找,维护成本极其痛苦。更重要的是,如果你要拿着这个项目答辩,老师看到上千行的 Activity,第一印象就会打折。
我推荐的方案是按功能分包,至少分四层。第一层是activity包,放 LoginActivity、MainActivity(课程列表)、SelectedCoursesActivity(已选课程)、AdminActivity(管理端)等界面类。第二层是adapter包,放各个列表的 RecyclerView.Adapter。第三层是db包,放数据库帮助类和 DAO(Data Access Object)类。第四层是model包,放 Student、Course、Enrollment 这种实体类。实体类的作用是让数据在 Java 对象和数据库行之间来回转换,典型的三层架构思想,代码结构一眼就能看出“界面只管显示、数据层只管存取、模型层统一格式”。
为了让数据库操作更顺手,我给每个表写一个独立的 DAO 类,比如CourseDao就是所有针对 course 表查询、插入、更新的操作集合。这样页面里不会出现任何一行 SQL 语句,所有数据操作都是调用方法。比如课程列表页面,只需要courseDao.getAllCourses()就能拿到一个List<Course>,然后传给 RecyclerView.Adapter 展示。
提示:如果你对 Android 自带 SQLite 的 Cursor 操作感到繁琐,可以引入 Room 库。Room 会在编译期帮你校验 SQL 语句,还能自动处理大部分模板代码,对初学者非常友好。但有个前提——如果你对 SQL 本身还不熟,我建议先用原生 SQLite 跑通一套,再换 Room,否则出了问题你分不清是 SQL 写错还是框架问题。
3.2 登录、课程列表、我的课表:三个关键界面
登录界面是整个 App 的门面,也是老师上手 Demo 时第一个看到的东西。我建议登录方式就用“学号 + 密码”即可,不需要注册功能——预置几个测试账号放进数据库里,比如2021001 / 123456,这样演示时输入就能进,省去现场注册的尴尬和麻烦。登录的核心代码很简单:从输入框读取学号和密码,去 student 表里查记录,匹配成功就跳转主页面,并把当前学生的学号通过 Intent 传递过去;失败则用 Toast 提示“学号或密码错误”。
课程列表主页面,我用的 RecyclerView + CardView 的方案。每张卡片展示课程名称、教师、时间地点、学分、容量进度(比如“已选 45/50”),右侧放一个“选课”按钮。这里最关键的体验细节是按钮状态要实时反映课程容量:已满就变灰不可点,已选就显示“已选”且不可点。实现方法并不复杂,Adapter 绑定数据时顺便判断一下该课程是否已被当前学生选过,然后动态切换按钮样式。
“我的课表”页面就更有意思了。如果把已选课程平铺展示,太普通了;我见过做得好的同学,用一个简单的 GridLayout 模拟出周一到周五、第 1 到第 8 节的网格,每门课按时间位置放进对应的格子。这个效果看起来加分不少,技术实现难度其实不大——把已选课程按“星期几 + 第几节”分组,往 GridLayout 里对应的位置塞进一个 TextView 就行。这种“最笨但最直观”的课表实现,比引入复杂框架划算得多。
3.3 选课和退课的操作流程:数据库事务要重视
选课按钮的点击事件是整个项目最核心的代码路径。每一步都不能马虎,我按实际代码顺序给你拆解一下。第一步,从按钮所在位置拿到对应的 Course 对象和当前登录学生的学号。第二步,调用 EnrollmentDao 的查询方法,确认没选过这门课。第三步,比较已选人数和容量。第四步,查询该学生所有已选课程的时间,做时间冲突检测。全部通过,才执行插入选课记录和把 course 表的选课人数加一这两步数据库操作。
说到这我必须强调一个很多初学者完全意识不到的坑:这两步数据库操作必须放在同一个事务里。什么叫事务?就是一荣俱荣、一损俱损。如果只插入选课记录成功,但更新人数失败了,那选课记录和人数就对不上了;反过来也一样。SQLite 的 beginTransaction 和 setTransactionSuccessful 这两个方法就是干这个用的。以前我见过一个学生在这里踩坑,退课时删除了选课记录但人数没减,演示了三次,数据一次比一次离谱。后来我让他把数据库操作全部包进事务里,问题立刻消失。
db.beginTransaction(); try { enrollmentDao.insert(enrollment); // 插入选课记录 courseDao.increaseSelectedCount(courseId); // 选课人数+1 db.setTransactionSuccessful(); } finally { db.endTransaction(); }退课的逻辑正好相反,先删除选课记录,再把选课人数减一,同样放在一个事务里。退课时不需要校验容量和时间冲突,但是需要判断一条边界情况:这门课是不是已经选过了。如果没有选过却点了退课,要给出提示而不是报错。一个好的设计是:已选列表页只展示真正选过的课程,从根源上杜绝“退不存在的课”这种操作。
4. 常见问题与排查实录:我能想到的坑都在这了
4.1 环境与构建:模拟器卡、Gradle 下载慢、SDK 版本不匹配
先聊构建环境的问题。Android Studio 首次创建项目时,Gradle 和依赖库需要从远程仓库下载,如果你的网络状况不太理想,很容易卡在构建进度条上。我的建议是:创建项目之前,先到官网把对应版本的 Gradle 压缩包下载好,手动放到 Gradle 的本地目录下,再修改项目中gradle-wrapper.properties的配置指向本地路径。这个操作听起来麻烦,但能让你免去无数次重新构建的等待。
模拟器卡顿也是高频问题。如果你的开发机内存不到 16G,我建议直接用真机调试,用 USB 连上手机,打开开发者选项里的 USB 调试,比模拟器省心太多。真机调试还有一个好处:碰到手机屏幕适配问题能第一时间发现。很多初学者只盯着 Pixel 模拟器调 UI,拿到华为、小米真机一跑,按钮全部错位,原因就是没有考虑屏幕尺寸差异。
还有一个很隐蔽的坑:compileSdk 和 targetSdk 版本不一致导致的依赖冲突。当你引入 Room 或其他第三方库时,有时会提示要求最低 API 级别比你项目配置的要高。解决办法很简单:把build.gradle里的 compileSdk 和 targetSdk 统一调高到 34 或 35,重新同步即可。这类错误信息通常很长,但关键就一句“requires compileSdk 34”,学会读报错信息比死记配置重要得多。
4.2 数据刷新:列表不更新是最常见的问题
我接手过不少选修课项目的“售后咨询”,其中被问得最多的一句话是:“老师,我选课成功了,但列表里没变。”这个问题百分之九十出在——你把选课操作完成后,忘了让列表重新加载数据。RecyclerView 的数据源是内存里的List<Course>,数据库变了不等于内存里的列表变了,你必须手动重新查询数据库,再把新数据设置给 Adapter,并调用notifyDataSetChanged()方法通知界面刷新。
更优雅一点的方案是改变数据源而不是整体刷新。比如选课成功后,你可以在内存中把对应 Course 对象的已选人数加一,并记录一个“本课程已被当前学生选过”的标记,然后只对这个 item 做局部刷新。这种方法避免了整个列表的闪烁,体验好很多。但我坦白讲,对于课设级别的项目,直接重新查询 + 全量刷新是最稳的,代码不容易出 bug,答辩演示也看不出差异。
另外一个很容易漏掉的刷新场景:从课程列表选完课,跳到“我的课表”页面时忘记在onResume里重新加载数据。如果你只在onCreate里加载了一次数据,那么你从选课页返回课表页时,看到的永远是老数据。我的习惯是:凡是列表类页面,数据加载逻辑都写在onResume里而不是onCreate里,这样每次页面回到前台都会自动刷新,从根上解决“返回后数据不更新”的玄学问题。
注意:如果你用原生 SQLite 而没有用 LiveData 之类的东西,在子线程操作数据库后,更新 UI 一定要切换到主线程。否则应用会直接崩溃,报错信息是
Only the original thread that created a view hierarchy can touch its views。简单粗暴的解决办法就是:数据量小的时候直接在 UI 线程跑数据库查询,虽然不优雅,但不会崩。
4.3 选课逻辑边界:并发、重复点击、时间冲突的隐性 bug
选课系统的并发问题,在课设答辩时被问到的概率极高。老师最爱问的一句话是:“如果两个学生同时选同一门只剩一个名额的课,怎么办?”你需要明白,本地 SQLite 架构下,真正的并发并不会发生——你的 App 没有同时被两个人操作的可能。但你要能回答出理论方案:如果是服务器数据库,可以给课程表加行锁,或者在更新人数时使用“原子更新 + 条件判断”的方式,比如UPDATE course SET selected_count = selected_count + 1 WHERE course_id = ? AND selected_count < capacity,这句 SQL 自带条件判断,能够从数据库层面兜底。面试时能说出这种方案,说明你思考过并发问题,加分项。
还有一个非常隐蔽的 bug 来自“重复点击”。选课按钮如果响应很快,用户可能连续点了两下。第一次点击已经完成选课,第二次点击又触发了一遍逻辑。如果你的重复性校验做在了异步回调里,第一次插入还没完成,第二次请求已经进来了,就会出现重复记录。我的处理方式简单粗暴:在按钮点击后立刻设置一个标志位,禁止再次点击,选课完成后再恢复。这种用户体验上的细节,优化成本极低,但能有效避免莫名其妙的数据问题。
最后说一个容易被忽略的 UI 细节:课程时间显示格式。很多课设喜欢把时间存成中文格式,比如“周一第3-4节”,这样用户看着舒服,但你的代码要做时间冲突判断时,解析中文会变得非常痛苦。我建议数据表里用“foo”这种简写格式存储,展示时再做格式化。宁可多写一个格式化方法,也不要在冲突判断时纠结字符串匹配。
5. 从课程设计到可扩展的系统:还能往哪个方向再进一步
跟你分享一个我私心觉得有用的“加分思路”。如果这个项目你打算拿去参加答辩或者作品展示,可以考虑加一个**“选课结果导出”**的功能——把当前学生的课表生成一个 PDF 或分享成文本。实现思路是读取已选课程列表,用 Android 的 PdfDocument 类或者简单的 Intent 分享文本,导出课表文件。这个功能本身不复杂,但特别容易打动评委,因为它体现了一个“能够站在用户角度考虑问题”的思维,而不是“只会实现布置的功能”。
再往远一点想,你还可以把这个本地单机版改造成客户端-服务器版。服务器端用 Java Web 或 Node.js 写一套 REST API,Android 端通过 OkHttp 请求接口获取数据。数据层从本地 SQLite 换成远程 MySQL,界面上几乎不用改动。这种演进路径非常自然,也是目前企业里常见的前后端分离开发模式。说实话,能独立走完这条路的应届生,在面试时的竞争力已经比大多数只会调界面的候选人强了。
如果你学有余力,还可以考虑引入简洁的 MVVM 架构——用 ViewModel 管理界面数据,用 Repository 层统一封装数据来源(本地数据库或远程接口),UI 层仅负责展示和交互。这样做的好处是逻辑更清晰、更容易写单元测试。但记住,架构是为可维护性服务的,如果你的项目本身只有几个页面,强行上 MVVM 反而会把简单问题复杂化。选一条适合自己当前能力水平的路线,比什么都重要。
我在实操中反复体会到一个道理:选课系统这类 CRUD 项目,难点从来不在技术,而在边界情况是否考虑周全、数据结构是否设计合理、流程是否走通闭环。你能不能在规定时间内把这三件事做到位,比用多么时髦的框架重要得多。希望这篇分享能帮你少走一些弯路,把精力花在真正能提升项目质量的地方。
本文还有配套的精品资源,点击获取