
简介这份资源是面向Android初学者与进阶开发者的学生选课系统完整项目源码基于Android Studio构建适合作为课程设计、毕业设计或Android技能练手的实战案例。压缩包共620个文件约56.75MB涵盖java源代码、xml布局与清单、gradle构建脚本、properties配置、dex与class编译产物、png与jpg图片资源及json数据文件等完整呈现了一个安卓应用的工程结构与构建链路。项目围绕选课场景展开涉及RecyclerView课程列表展示、SQLite选课数据持久化、网络通信与多线程处理等关键技术点并包含Gradle依赖管理、SDK本地配置等工程化细节。已有2086人学习下载读者可借此熟悉Android Studio项目目录组织、理解build.gradle与清单文件的作用并对照源码掌握UI设计、数据存储与模块协作的实现思路对提升安卓开发实践能力具有较高参考价值。1. 从一次课程设计答辩说起这个 Android 选课系统到底能干什么去年帮一个学弟看课程设计他打开 Android Studio 模拟器登录、选课、退课、查课表一气呵成老师当场问了一句「并发选同一门课怎么处理」他愣了三秒。这个场景基本概括了「androidstdio学生选课系统」这类项目的真实价值它不是一个玩具 Demo而是一个把学生端、课程数据、选课状态流转串起来的最小可用闭环。用 Android Studio 做客户端后端通常配一个轻量服务或本地数据库覆盖登录鉴权、课程列表、选课退课、课表查看这几条主线。适合谁正在做课程设计的学生、想练手 Android 网络请求与本地存储的初级开发者、需要一套能改能扩的选课原型做二次开发的人。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序把这个项目的骨架拆开讲清楚。2. 技术栈与工程结构一个选课 App 的骨架长什么样2.1 客户端技术选型与依赖关系这类项目在 Android Studio 里落地最常见的组合是 Java 或 Kotlin 原生 View 体系网络层用 OkHttp 或 Retrofit本地缓存用 SQLite 或 Room列表展示用 RecyclerView。为什么不用 Compose不是不能用而是课程设计场景下RecyclerView Adapter 的写法资料多、报错好查指导老师也看得懂。我一般会建议保持这个组合把精力放在业务逻辑上而不是新框架的玄学问题上。依赖层面build.gradle里通常会有这几块网络请求库、JSON 解析库Gson 或 Moshi、数据库组件、以及 Material Design 组件库。下面是一个典型的模块级配置片段注意版本号要和你本地 Android Studio 的 Gradle 插件版本对齐否则同步阶段就会翻车。// app/build.gradle android { compileSdk 34 defaultConfig { applicationId com.example.courseselect minSdk 24 // 覆盖绝大多数教学用模拟器 targetSdk 34 versionCode 1 versionName 1.0 } } dependencies { implementation androidx.recyclerview:recyclerview:1.3.2 implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.retrofit2:converter-gson:2.9.0 implementation androidx.room:room-runtime:2.6.1 annotationProcessor androidx.room:room-compiler:2.6.1 implementation com.google.android.material:material:1.11.0 }这段配置的逻辑很直白minSdk 24是为了兼容教学机房里那些老模拟器Retrofit 负责把 HTTP 接口转成 Java 方法调用Room 负责把课表和选课记录落到本地断网时还能看到已选课程。参数上最容易被忽略的是compileSdk和 Gradle 插件版本的匹配关系两者差一个大版本同步就会报「Unsupported class file major version」这是新手最高频的翻车点之一。2.2 目录结构与模块职责划分一个能跑起来的选课系统包结构通常按职责切分而不是按页面堆。下面是我拆过的项目里比较清爽的一种组织方式app/src/main/java/com/example/courseselect/ ├── ui/ # 页面层 │ ├── LoginActivity.java │ ├── CourseListActivity.java │ └── ScheduleActivity.java ├── adapter/ # RecyclerView 适配器 │ └── CourseAdapter.java ├── model/ # 数据实体 │ ├── Course.java │ └── Student.java ├── network/ # 网络层 │ ├── ApiService.java │ └── RetrofitClient.java ├── db/ # 本地数据库 │ ├── AppDatabase.java │ └── CourseDao.java └── util/ # 工具类 └── SessionManager.java这个结构的核心思想是「页面只管展示和交互数据从 network 或 db 来实体在 model 里统一」。SessionManager单独拎出来存登录态用 SharedPreferences 实现避免每个页面都去读一遍用户信息。很多同学把网络请求直接写在 Activity 里短期能跑一旦要加「选课成功后刷新列表」这种联动代码就开始互相纠缠这是血泪经验。2.3 数据流一次选课请求经过哪些层把一次「点击选课按钮」的完整链路走一遍你就理解了这个项目的架构。用户在CourseListActivity点击某门课的「选课」按钮Adapter 通过接口回调把课程 ID 传给 ActivityActivity 调用ApiService.selectCourse(studentId, courseId)Retrofit 发出 POST 请求服务端返回选课结果回调里根据成功或失败更新本地 Room 数据库并刷新列表。这条链路里任何一层出问题都会表现为「点了没反应」所以排查时要按层定位而不是盯着按钮看。// CourseListActivity 中的选课回调 private void onSelectClicked(Course course) { String studentId SessionManager.getStudentId(this); apiService.selectCourse(studentId, course.getId()) .enqueue(new CallbackApiResponse() { Override public void onResponse(CallApiResponse call, ResponseApiResponse response) { if (response.isSuccessful() response.body() ! null response.body().isSuccess()) { // 选课成功写本地库 刷新列表 courseDao.insert(new SelectedCourse(course.getId(), studentId)); loadCourses(); } else { Toast.makeText(CourseListActivity.this, 选课失败 response.body().getMessage(), Toast.LENGTH_SHORT).show(); } } Override public void onFailure(CallApiResponse call, Throwable t) { Toast.makeText(CourseListActivity.this, 网络异常请稍后重试, Toast.LENGTH_SHORT).show(); } }); }这段代码的关键点在enqueue的异步回调网络请求不能放在主线程否则直接抛NetworkOnMainThreadException。response.isSuccessful()判断的是 HTTP 状态码 2xx而body().isSuccess()判断的是业务层返回的 success 字段两者要分开看很多项目只判断前者结果服务端返回 200 但业务失败时界面还提示成功这是典型的黑匣子问题。参数上studentId从 SessionManager 取courseId从列表项带过来两个 ID 缺一不可。3. 跑起来从导入工程到选课成功的完整步骤3.1 环境准备与工程导入拿到源码后第一件事不是急着点运行而是对齐环境。Android Studio 版本、Gradle 版本、JDK 版本三者要匹配。我一般会先看项目根目录的gradle/wrapper/gradle-wrapper.properties里面写着这个项目期望的 Gradle 版本然后确认本地 JDK 是 17 还是 11。如果 Gradle 版本和 JDK 不匹配同步阶段就会报错这时候改distributionUrl比升级 Android Studio 更省事。导入步骤按顺序来打开 Android Studio选择 Open 而不是 Import定位到项目根目录有settings.gradle的那一层等待 Gradle 同步。同步过程中如果卡在「Downloading」很久多半是仓库地址访问慢可以在settings.gradle里把google()和mavenCentral()的顺序调整一下或者配置国内镜像。同步成功后检查local.properties里是否有sdk.dir没有的话手动指向本地 SDK 路径。3.2 后端服务与数据库初始化选课系统离不开数据。如果项目自带一个本地服务端常见的是 Node.js 或 Spring Boot 写的简易接口先把它跑起来。以 Node 为例进入server目录执行依赖安装和启动# 进入服务端目录 cd server # 安装依赖 npm install # 初始化数据库建表 插入测试课程数据 npm run init-db # 启动服务默认监听 3000 端口 npm start启动后先用浏览器或 curl 访问一下http://localhost:3000/api/courses确认能返回课程 JSON 列表。这一步很关键因为 Android 端连不上服务时报错信息往往只有一句「连接失败」你无法判断是服务没起、端口不对还是 IP 写错。先验证服务端独立可用再排查客户端能省一半时间。如果项目没有独立服务端而是用本地 SQLite 模拟数据那就跳过这一步直接看db包下的初始化逻辑确认首次启动时会插入测试课程。常见做法是在AppDatabase的onCreate回调里执行建表和种子数据插入。3.3 模拟器网络配置与首次登录Android 模拟器访问宿主机服务有个坑不能用localhost或127.0.0.1因为那是模拟器自己的回环地址。要用10.0.2.2指向宿主机。所以RetrofitClient里的 BASE_URL 通常写成http://10.0.2.2:3000/。如果你用真机调试则要改成电脑在局域网里的 IP比如http://192.168.1.100:3000/并且手机和电脑要在同一个网络下。// RetrofitClient.java private static final String BASE_URL http://10.0.2.2:3000/; public static ApiService getApiService() { OkHttpClient client new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) // 连接超时 .readTimeout(10, TimeUnit.SECONDS) // 读取超时 .build(); Retrofit retrofit new Retrofit.Builder() .baseUrl(BASE_URL) .client(client) .addConverterFactory(GsonConverterFactory.create()) .build(); return retrofit.create(ApiService.class); }超时时间设 10 秒是折中值太短在慢网络下容易误判失败太长用户等得焦躁。另外 Android 9 以后默认禁止明文 HTTP 请求如果 BASE_URL 是http://开头需要在AndroidManifest.xml的application标签里加android:usesCleartextTraffictrue否则请求直接失败日志里只有一句Cleartext HTTP traffic not permitted这是新手最容易卡住的地方。配置好之后用测试账号登录通常服务端种子数据里会有admin/123456或student1/123456这类账号进入课程列表点一门课选上再去课表页确认。整条链路通了说明环境没问题可以开始改代码了。4. 核心模块拆解登录、选课、课表三条主线的实现细节4.1 登录鉴权与 Session 管理登录模块看起来简单但它是整个系统的入口做不好后面全是麻烦。典型实现是用户输入学号和密码客户端 POST 到/api/login服务端校验后返回一个 token 或直接返回学生信息客户端把学生 ID 和 token 存到 SharedPreferences后续请求带上。// LoginActivity 中的登录逻辑 private void doLogin(String studentId, String password) { apiService.login(studentId, password) .enqueue(new CallbackLoginResponse() { Override public void onResponse(CallLoginResponse call, ResponseLoginResponse response) { if (response.isSuccessful() response.body() ! null response.body().getToken() ! null) { // 持久化登录态 SessionManager.saveSession(LoginActivity.this, response.body().getStudentId(), response.body().getToken()); startActivity(new Intent(LoginActivity.this, CourseListActivity.class)); finish(); // 避免返回键回到登录页 } else { Toast.makeText(LoginActivity.this, 账号或密码错误, Toast.LENGTH_SHORT).show(); } } Override public void onFailure(CallLoginResponse call, Throwable t) { Toast.makeText(LoginActivity.this, 网络异常, Toast.LENGTH_SHORT).show(); } }); }这里有个细节登录成功后调用finish()否则用户按返回键会回到登录页体验很怪。SessionManager内部用SharedPreferences的apply()异步写入避免主线程阻塞。token 的作用是后续请求的鉴权凭证常见做法是在 OkHttp 里加一个 Interceptor统一往请求头里塞Authorization: Bearer token这样每个接口方法不用重复传 token。4.2 课程列表与 RecyclerView 适配器课程列表是用户停留最久的页面性能和数据准确性都要顾。RecyclerView 的 Adapter 负责把ListCourse绑定到 item 布局上每个 item 显示课程名、教师、学分、余量和一个选课按钮。余量为 0 时按钮置灰这个判断要放在onBindViewHolder里做。// CourseAdapter.java 核心绑定逻辑 Override public void onBindViewHolder(NonNull CourseViewHolder holder, int position) { Course course courseList.get(position); holder.tvName.setText(course.getName()); holder.tvTeacher.setText(course.getTeacher()); holder.tvCredit.setText(学分 course.getCredit()); holder.tvRemain.setText(余量 course.getRemain()); // 余量为 0 或已选过禁用按钮 boolean canSelect course.getRemain() 0 !course.isSelected(); holder.btnSelect.setEnabled(canSelect); holder.btnSelect.setText(course.isSelected() ? 已选 : 选课); holder.btnSelect.setOnClickListener(v - { if (listener ! null) { listener.onSelectClicked(course); } }); }canSelect这个判断把「余量」和「是否已选」两个条件合并避免用户重复选同一门课。listener是 Activity 传进来的回调接口Adapter 不直接持有 Activity 引用防止内存泄漏。列表数据刷新用notifyDataSetChanged()简单粗暴数据量大时换成DiffUtil更平滑但课程数量一般几十条前者够用。4.3 选课状态流转与本地课表落库选课成功后除了刷新列表还要把记录写到本地数据库这样课表页和断网场景都能用。Room 的实体和 DAO 定义如下// SelectedCourse.java 实体 Entity(tableName selected_course) public class SelectedCourse { PrimaryKey(autoGenerate true) public int id; public String studentId; public String courseId; public String courseName; public String teacher; } // CourseDao.java Dao public interface CourseDao { Insert(onConflict OnConflictStrategy.REPLACE) void insert(SelectedCourse course); Query(SELECT * FROM selected_course WHERE studentId :sid) ListSelectedCourse getByStudent(String sid); Query(DELETE FROM selected_course WHERE studentId :sid AND courseId :cid) void delete(String sid, String cid); }OnConflictStrategy.REPLACE保证同一门课重复插入时覆盖而不是报错。退课走delete方法删完再刷新课表。课表页按星期和节次排列常见做法是在SelectedCourse里加weekday和section字段查询后按这两个字段排序再用 GridLayout 或自定义 View 画出来。状态流转的核心是「服务端为准、本地为辅」选课结果以接口返回为准本地库只是缓存避免两端不一致时用户看到错误信息。5. 避坑与排查选课系统最容易翻车的五个地方5.1 模拟器连不上后端现象登录页一直提示网络异常Logcat 里是ConnectException。原因多半是 BASE_URL 写成了localhost或127.0.0.1模拟器访问的是自己。解决改成10.0.2.2真机则用电脑局域网 IP并确认服务端监听的是0.0.0.0而不是仅127.0.0.1。5.2 明文 HTTP 被拦截现象请求发出但立刻失败日志提示Cleartext HTTP traffic not permitted。原因是 Android 9 以后默认禁止明文流量。解决在AndroidManifest.xml的application标签加android:usesCleartextTraffictrue或者把服务端换成 HTTPS。教学项目用前者更快。5.3 主线程网络请求崩溃现象点击按钮后 App 直接闪退日志是NetworkOnMainThreadException。原因是有同学把 Retrofit 的execute()同步方法直接写在按钮回调里。解决一律用enqueue()异步方式回调里再更新 UI如果确实需要同步放到子线程或协程里。5.4 选课成功但列表不刷新现象Toast 提示选课成功但课程余量和按钮状态没变。原因是只更新了服务端没有重新拉取列表或更新本地数据源。解决在成功回调里调用loadCourses()重新请求或者直接修改courseList中对应对象的remain和selected字段后notifyItemChanged(position)。5.5 数据库版本升级导致崩溃现象改了实体字段后重新运行App 启动即崩日志是IllegalStateException: Room cannot verify the data integrity。原因是 Room 检测到 schema 变化但版本号没升。解决把Database的version加 1并提供Migration或者开发阶段直接用fallbackToDestructiveMigration()重建表。生产环境别用后者会丢数据。6. 二次开发把课程设计改成能用的选课工具拿到这套代码后最值得做的改造不是换 UI而是把「选课冲突检测」补上。原始项目通常只判断余量不判断时间冲突用户能选上两门同一时段的课这在真实场景里是硬伤。改造思路在SelectedCourse里补weekday和section字段选课前先查本地库判断新课程的时间段是否与已选课程重叠。// 选课前的冲突检测 private boolean hasConflict(Course newCourse, ListSelectedCourse selected) { for (SelectedCourse sc : selected) { if (sc.weekday newCourse.getWeekday() sc.section newCourse.getSection()) { return true; // 同一时段已有课 } } return false; }调用时机放在onSelectClicked里、发网络请求之前冲突就直接 Toast 提示不发请求。这样既减少无效请求也让交互更跟手。验证方法很简单选一门周一第 1-2 节的课再选另一门同样时段的课应该被拦住选不同时段的课则正常通过。第二个改造点是登录态过期处理。真实使用中 token 会失效接口返回 401 时应该跳回登录页而不是弹一个看不懂的错误。常见做法是在 OkHttp 的 Interceptor 里统一拦截 401清空 Session 并启动 LoginActivity。这个改动不大但能让项目从「能演示」变成「能交付」。第三个点是列表分页。课程多了以后一次性拉全量会慢给接口加page和size参数RecyclerView 滚动到底部时加载下一页。改造时注意维护一个currentPage变量加载中要加锁防止重复请求这是分页最常见的翻车点。我自己的习惯是每次改完一个模块先在模拟器上把「登录 → 选课 → 退课 → 查课表」这条主链路完整走一遍再提交代码。有一次偷懒只测了选课没测退课结果退课后本地库删了但列表没刷新演示时被当场问住。从那以后我每次改完选课逻辑都强制把主链路走一遍再收工。希望这套拆解能帮你少走几个弯路把这份源码真正跑起来、改下去。本文还有配套的精品资源点击获取