简介:这是一份面向Android初学者与高校学生的Android Studio学生选课系统项目源码,适合作为课程设计、毕业设计或Android入门实战的参考案例。压缩包共620个文件,约56.75MB,以flat、json、xml、dex、class等构建与资源文件为主,另含java源码、gradle构建脚本、properties配置、jar依赖及少量png、jpg图片素材,完整呈现了Android工程的目录结构与构建产物。项目围绕选课场景展开,涉及RecyclerView课程列表展示、SQLite数据持久化、网络通信与多线程处理等关键技术点,并包含Gradle构建配置、.gitignore版本控制配置等工程化内容,便于读者理解从源码到APK的完整链路。目前已有2086人学习下载,适合希望熟悉Android Studio开发流程、积累项目经验并对照实践UI设计与数据存储的读者参考借鉴。
1. 从一份“能跑起来”的选课系统说起
很多同学做课程设计时,最头疼的不是写不出增删改查,而是把 Android Studio 打开之后,面对一堆 Gradle 报错、模拟器起不来、数据库连不上,最后项目还没跑起来就先放弃了。这份 androidstdio 学生选课系统,本质上就是一个面向课程设计场景的完整 Android 工程:学生端能登录、浏览课程、选课退课、查看已选结果,管理端能维护课程信息,数据落在本地数据库里,不依赖外部服务器就能演示。它适合两类人:一类是刚接触 Android 开发、需要一份结构清晰、能照着跑通的参考工程的新手;另一类是已经写过 Demo、想看看别人怎么组织 Activity、Adapter、SQLite 和实体类的中级开发者。下面我按“先看清结构、再跑起来、最后动手改”的顺序,把这个工程拆开讲。
2. 工程结构与技术选型:一个选课 App 由哪些层组成
2.1 目录结构:从 Gradle 脚本到业务代码的分布
拿到一个 Android Studio 工程,第一件事不是急着点 Run,而是先看目录。这个选课系统的典型结构大致如下,不同版本可能略有差异,但核心分层是一致的:
StudentCourseSelection/ ├── app/ │ ├── build.gradle # 模块级构建脚本,声明依赖和编译配置 │ └── src/main/ │ ├── AndroidManifest.xml # 四大组件注册、权限声明 │ ├── java/com/example/selection/ │ │ ├── activity/ # 登录、主页、选课、课程管理等活动 │ │ ├── adapter/ # 课程列表、已选列表的 RecyclerView 适配器 │ │ ├── db/ # SQLiteOpenHelper 与 DAO 封装 │ │ ├── model/ # Student、Course、Selection 实体类 │ │ └── util/ # 会话管理、Toast 封装等工具 │ └── res/ │ ├── layout/ # 各页面 XML 布局 │ ├── values/ # 字符串、颜色、主题 │ └── drawable/ # 图标与背景资源 ├── build.gradle # 项目级构建脚本 ├── settings.gradle # 模块声明 └── gradle/wrapper/ # Gradle Wrapper 配置这个分层不算复杂,但对课程设计来说已经够用:activity负责页面流转和交互,adapter负责把数据绑到列表上,db把 SQLite 的建表和增删改查收口,model只放字段。很多同学写课程设计时喜欢把所有逻辑塞进一个 Activity,短期能跑,后期改一个字段就要翻几百行,血泪经验就是尽早把数据库操作抽出去。
2.2 技术栈:为什么是原生 Android + SQLite
这个工程没有引入 Jetpack Compose,也没有用 Room,走的是最传统的原生路线:Java 或 Kotlin 写 Activity,XML 写布局,SQLiteOpenHelper 管数据库,RecyclerView 展示列表。这样选的原因很实际——课程设计环境里,评委或老师往往要求“能直接打开工程看到代码”,原生方案依赖少、编译链短,出问题也容易定位。
| 层次 | 技术选型 | 作用 |
|---|---|---|
| 界面 | Activity + XML 布局 | 页面展示与跳转 |
| 列表 | RecyclerView + Adapter | 课程列表、已选课程列表 |
| 数据 | SQLite + SQLiteOpenHelper | 本地持久化,无需后端 |
| 构建 | Gradle + Android Gradle Plugin | 依赖管理与打包 |
| 会话 | SharedPreferences | 保存当前登录学生 ID |
需要说明的是,原文没有给出具体版本号,实际工程里build.gradle会声明compileSdk、minSdk和依赖版本。我一般会先打开模块级build.gradle确认这几项,再决定用哪个版本的 Android Studio 打开,避免一上来就被“Gradle 版本不兼容”拦住。
2.3 数据模型:三张表撑起选课逻辑
选课系统的核心数据关系并不复杂:学生、课程、选课记录。常见做法是建三张表,选课记录表用学生 ID 和课程 ID 做关联,同时加唯一约束防止重复选课。
-- 学生表 CREATE TABLE student ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_no TEXT UNIQUE NOT NULL, -- 学号,登录用 name TEXT NOT NULL, password TEXT NOT NULL ); -- 课程表 CREATE TABLE course ( id INTEGER PRIMARY KEY AUTOINCREMENT, course_code TEXT UNIQUE NOT NULL, -- 课程编号 course_name TEXT NOT NULL, teacher TEXT, credit REAL, -- 学分 capacity INTEGER, -- 容量 selected_count INTEGER DEFAULT 0 -- 已选人数 ); -- 选课记录表 CREATE TABLE selection ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id INTEGER NOT NULL, course_id INTEGER NOT NULL, select_time TEXT, UNIQUE(student_id, course_id) -- 防止重复选同一门课 );这段建表语句的关键在最后一行:UNIQUE(student_id, course_id)把“同一个学生选同一门课”这件事在数据库层就挡掉了,比在 Java 代码里先查再插更可靠。selected_count是冗余字段,用来快速判断课程是否满员,选课和退课时要同步更新,这一点后面讲 DAO 时会再提到。
3. 把工程跑起来:从导入到登录成功的完整路径
3.1 导入前的环境检查
在 Android Studio 里点“Open”选中工程根目录之前,先确认三件事:JDK 版本、Gradle 版本、SDK 版本。这个工程用的是 Gradle Wrapper,理论上不需要本机预装 Gradle,但需要 Android Studio 能下载到对应版本的 Gradle 发行包。如果公司或学校网络对下载有限制,常见做法是提前在gradle-wrapper.properties里确认distributionUrl,必要时换成本地已缓存的版本。
# 查看工程使用的 Gradle 版本 cat gradle/wrapper/gradle-wrapper.properties # 查看模块级构建脚本中的 SDK 配置 cat app/build.gradle第一条命令输出里会有distributionUrl,它决定了首次同步时下载哪个 Gradle;第二条命令重点看compileSdk、minSdk、targetSdk以及dependencies块。如果本机 SDK 里没有对应平台,Android Studio 会提示安装,按提示点即可。这里容易翻车的地方是 JDK:新版 Android Studio 自带 JDK,但老工程可能指定了 Java 8 语法,需要在build.gradle的compileOptions里确认sourceCompatibility和targetCompatibility。
3.2 首次同步与常见报错处理
点“Sync Project with Gradle Files”之后,最常见的报错集中在依赖下载和插件版本上。下面这段是模块级build.gradle的典型写法,重点看plugins和dependencies:
plugins { id 'com.android.application' } android { namespace 'com.example.selection' compileSdk 34 defaultConfig { applicationId "com.example.selection" minSdk 24 targetSdk 34 versionCode 1 versionName "1.0" } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'com.google.android.material:material:1.11.0' }namespace决定 R 类包名,applicationId决定安装到设备上的包名,两者可以不同但建议保持一致。minSdk 24意味着 Android 7.0 以下设备装不上,如果演示设备比较老,可以适当调低,但要确认用到的 API 在低版本可用。依赖里appcompat提供 Activity 兼容,recyclerview提供列表控件,material提供主题和部分控件样式。同步失败时先看 Build 窗口的完整堆栈,多数情况是网络拉不到依赖,或者 Gradle 与 Android Gradle Plugin 版本不匹配,按提示调整即可。
3.3 数据库初始化与登录流程
工程跑起来后,第一个页面通常是登录页。登录逻辑一般分两步:先按学号和密码查student表,查到就把学生 ID 写进 SharedPreferences,然后跳主页。数据库的创建和升级集中在SQLiteOpenHelper子类里:
public class DBHelper extends SQLiteOpenHelper { private static final String DB_NAME = "selection.db"; private static final int DB_VERSION = 1; public DBHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } @Override public void onCreate(SQLiteDatabase db) { // 建表语句与前面 SQL 一致,此处省略重复内容 db.execSQL(CREATE_STUDENT); db.execSQL(CREATE_COURSE); db.execSQL(CREATE_SELECTION); // 插入一条测试学生,方便首次登录 db.execSQL("INSERT INTO student(student_no, name, password) " + "VALUES('2024001', '测试学生', '123456')"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 课程设计阶段常见做法是直接重建,生产环境不能这么干 db.execSQL("DROP TABLE IF EXISTS selection"); db.execSQL("DROP TABLE IF EXISTS course"); db.execSQL("DROP TABLE IF EXISTS student"); onCreate(db); } }onCreate只在数据库第一次创建时调用,所以测试数据插在这里最省事。onUpgrade里直接删表重建,是课程设计里常见的偷懒做法,好处是改表结构不用写迁移脚本,坏处是升级会丢数据。如果这个工程要二次开发成真正可用的系统,这里必须改成逐版本迁移。登录成功后用SharedPreferences存学生 ID,后续选课、查已选都靠它取当前用户,避免每个页面都重新查一遍登录态。
4. 核心模块拆解:选课、退课与列表刷新怎么串起来
4.1 选课事务:人数校验和插入必须一起成功
选课不是简单插一条记录,它至少包含三个动作:判断课程是否已满、插入选课记录、更新课程已选人数。这三个动作要么全成功,要么全回滚,否则会出现“记录插了但人数没加”或者“人数加了但记录没插”的脏数据。常见做法是用 SQLite 的事务包起来:
public boolean selectCourse(int studentId, int courseId) { SQLiteDatabase db = getWritableDatabase(); db.beginTransaction(); try { // 1. 查课程容量与已选人数 Cursor cursor = db.rawQuery( "SELECT capacity, selected_count FROM course WHERE id = ?", new String[]{String.valueOf(courseId)}); if (!cursor.moveToFirst()) { return false; } int capacity = cursor.getInt(0); int selected = cursor.getInt(1); cursor.close(); // 2. 满员直接返回 if (selected >= capacity) { return false; } // 3. 插入选课记录,唯一约束会挡住重复选课 db.execSQL("INSERT INTO selection(student_id, course_id, select_time) " + "VALUES(?, ?, datetime('now'))", new Object[]{studentId, courseId}); // 4. 已选人数加一 db.execSQL("UPDATE course SET selected_count = selected_count + 1 " + "WHERE id = ?", new Object[]{courseId}); db.setTransactionSuccessful(); return true; } catch (SQLiteConstraintException e) { // 重复选课会走到这里 return false; } finally { db.endTransaction(); } }beginTransaction和setTransactionSuccessful之间的操作是一个整体,任何一步抛异常都会在finally里回滚。SQLiteConstraintException专门接住唯一约束冲突,也就是重复选课的情况,返回 false 让界面提示“已选过该课程”。参数上,studentId来自登录时存的会话,courseId来自列表项点击时带过来的课程主键,两个都不能为空。
4.2 退课逻辑:先删记录再减人数
退课是选课的逆操作,顺序反过来:先删选课记录,再把人数字段减一。这里要注意减一之后不能小于零,虽然正常流程不会出现负数,但二次开发时如果手动改过数据,最好加一层保护:
public boolean dropCourse(int studentId, int courseId) { SQLiteDatabase db = getWritableDatabase(); db.beginTransaction(); try { int deleted = db.delete("selection", "student_id = ? AND course_id = ?", new String[]{String.valueOf(studentId), String.valueOf(courseId)}); if (deleted == 0) { return false; // 本来就没选,直接返回 } db.execSQL("UPDATE course SET selected_count = " + "CASE WHEN selected_count > 0 THEN selected_count - 1 ELSE 0 END " + "WHERE id = ?", new Object[]{courseId}); db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } }delete返回受影响行数,为 0 说明这条选课记录不存在,直接返回 false,界面提示“未选该课程”。更新人数时用CASE WHEN兜底,避免出现负数。退课成功后,已选列表和课程列表都需要刷新,否则用户会看到人数没变或者课程还在已选里。
4.3 列表刷新:Adapter 与数据源的同步
课程列表用 RecyclerView 展示,数据来自course表查询结果。选课或退课之后,最直接的做法是重新查一遍数据库,然后调 Adapter 的刷新方法:
public void refreshCourseList() { List<Course> courses = courseDao.queryAll(); adapter.submitList(courses); // 使用 ListAdapter 时 // 若用普通 RecyclerView.Adapter,则: // adapter.setData(courses); // adapter.notifyDataSetChanged(); }queryAll里按课程编号或名称排序,保证每次刷新顺序稳定。如果 Adapter 继承的是ListAdapter,submitList会自动做差异比较,刷新更平滑;如果是普通 Adapter,notifyDataSetChanged会整表重绘,数据量小的时候无所谓,课程多了会有闪烁。这里的关键是“数据源单一”:列表只认数据库查询结果,不要在 Activity 里另存一份 List 再手动改,否则很容易出现界面和数据库不一致。
5. 避坑与排查:跑不通时先看这几处
5.1 现象:Gradle 同步一直转圈或报网络错误
原因多半是依赖仓库访问不稳定,或者distributionUrl指向的 Gradle 版本下载失败。解决方式是先看 Build 窗口具体卡在哪个依赖,确认本机 Gradle 缓存目录里有没有对应版本;如果只是某几个 androidx 依赖拉不下来,可以在settings.gradle里确认仓库顺序,优先使用本地已缓存的仓库。不要一上来就删.gradle目录,那会把所有缓存清掉,反而更慢。
5.2 现象:登录提示“用户名或密码错误”,但测试数据明明插了
原因通常是数据库已经创建过,onCreate不会再执行,测试学生没插进去。解决方式是卸载应用重装,或者在设备的应用管理里清除应用数据,让数据库重新创建。另一个可能是学号字段前后有空格,查询时用了精确匹配。排查时可以把查询语句和参数打到 Logcat,看一眼实际传进去的值。
5.3 现象:选课成功但课程列表人数没变
原因是选课之后没有触发列表刷新,或者刷新时查的是另一份缓存数据。解决方式是在选课回调里统一调一次refreshCourseList,并确认 Adapter 持有的数据源就是数据库查询结果。如果用了notifyDataSetChanged仍然不更新,检查一下传给 Adapter 的 List 是不是同一个对象引用,有些写法会重新new一个 List 但 Adapter 还指着旧的。
5.4 现象:重复选课没有提示,直接插入了两条记录
原因是建表时漏了UNIQUE(student_id, course_id)约束,或者插入时用了INSERT OR REPLACE。解决方式是确认建表语句里有唯一约束,插入用普通INSERT并捕获SQLiteConstraintException。如果数据库已经建好,改约束需要升级版本号并在onUpgrade里重建表,课程设计阶段可以直接清数据重来。
5.5 现象:模拟器能跑,真机安装失败
原因通常是minSdk高于真机系统版本,或者applicationId与已安装的同包名应用签名冲突。解决方式是先看真机 Android 版本,对照build.gradle里的minSdk;签名冲突则卸载旧应用再装。真机调试还要确认开启了开发者选项和 USB 调试,这部分属于环境问题,和代码无关,但排查时容易被忽略。
6. 二次开发改造:从课程设计走向可维护的小系统
如果要把这个选课系统继续往下做,我建议优先动三处。第一处是把onUpgrade里的删表重建改成逐版本迁移,哪怕只是加一个字段,也走ALTER TABLE,这样后续加“课程简介”“上课时间”之类字段时不会丢数据。第二处是把数据库操作从 Activity 里彻底抽到 DAO 层,Activity 只负责拿结果更新界面,这样换 UI 或者加管理端页面时不用重复写 SQL。第三处是给选课加一个“冲突检测”,比如同一时间段只能选一门课,这需要在course表里加时间段字段,选课前查一下该学生已选课程的时间段是否有交集。
验证改造是否成功,我一般用一个小清单走一遍:新装应用能登录、能选课、能退课、重复选课有提示、满员课程选不上、退课后人数正确回落、杀进程重进数据还在。这几步覆盖了增删改查和事务边界,任何一步不对都能快速定位到是哪一层的问题。还有一个具体技巧:在 DAO 里加一个只给调试用的dumpAll()方法,把三张表的内容打到 Logcat,联调时比反复点界面快得多,发布前记得关掉或者删掉。
从那以后我每次拿到一个课程设计工程,都强制先跑一遍“清数据重装 + 完整选退课流程”,确认数据库初始化和事务没问题,再去看界面细节。很多看起来是 UI 的 bug,根子都在数据层。希望这份拆解能帮你把这个选课系统顺利跑起来,也能顺着自己的需求改下去。
本文还有配套的精品资源,点击获取